495 lines
18 KiB
Python
495 lines
18 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, Polygon, PolyCollection, RectCollection
|
|
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 _canonical_polygon_key(vertices: object) -> tuple[tuple[int, int], ...]:
|
|
arr = numpy.rint(numpy.asarray(vertices, dtype=float)).astype(int)
|
|
rows = [tuple(tuple(row.tolist()) for row in numpy.roll(arr, -shift, axis=0)) for shift in range(arr.shape[0])]
|
|
rev = arr[::-1]
|
|
rows.extend(tuple(tuple(row.tolist()) for row in numpy.roll(rev, -shift, axis=0)) for shift in range(rev.shape[0]))
|
|
return min(rows)
|
|
|
|
|
|
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,
|
|
_canonical_polygon_key(poly.vertices),
|
|
_coord_key(poly.offset),
|
|
_annotations_key(poly.annotations),
|
|
))
|
|
return keys
|
|
|
|
|
|
def _ref_keys(target: str, ref: object) -> list[tuple[object, ...]]:
|
|
keys = []
|
|
for transform in ref.as_transforms():
|
|
keys.append((
|
|
target,
|
|
_coord_key(transform[:2]),
|
|
round(float(transform[2]), 8),
|
|
round(float(transform[4]), 8),
|
|
bool(int(round(float(transform[3])))),
|
|
_annotations_key(ref.annotations),
|
|
))
|
|
return keys
|
|
|
|
|
|
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: list[tuple[object, ...]] = []
|
|
for target, refs in pattern.refs.items():
|
|
for ref in refs:
|
|
ref_keys.extend(_ref_keys(target, ref))
|
|
|
|
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
|
|
|
|
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=(20, 0))
|
|
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))
|
|
fanout.ref('leaf', offset=(50, 0), repetition=Grid(a_vector=(6, 0), a_count=3, b_vector=(0, 8), b_count=2))
|
|
fanout.ref('leaf', offset=(60, 0), annotations={'19': ['fanout-sref']})
|
|
fanout.ref('child', offset=(70, 0), repetition=Grid(a_vector=(4, 0), a_count=2, b_vector=(0, 5), b_count=2),
|
|
annotations={'20': ['fanout-aref']})
|
|
lib['fanout'] = fanout
|
|
|
|
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.ref('fanout', offset=(250, -75))
|
|
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_degenerate_aref_decodes_as_single_transform(tmp_path: Path) -> None:
|
|
lib = Library()
|
|
leaf = Pattern()
|
|
leaf.polygon((1, 0), vertices=[[0, 0], [5, 0], [5, 5], [0, 5]])
|
|
lib['leaf'] = leaf
|
|
|
|
top = Pattern()
|
|
top.ref('leaf', offset=(100, 200), repetition=Grid(a_vector=(7, 0), a_count=1, b_vector=(0, 9), b_count=1))
|
|
lib['top'] = top
|
|
|
|
gds_file = tmp_path / 'degenerate_aref.gds'
|
|
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
|
|
|
canonical_lib, _ = gdsii.readfile(gds_file)
|
|
arrow_lib, _ = gdsii_arrow.readfile(gds_file)
|
|
assert _library_summary(arrow_lib) == _library_summary(canonical_lib)
|
|
|
|
decoded_ref = arrow_lib['top'].refs['leaf'][0]
|
|
assert decoded_ref.repetition is None
|
|
|
|
|
|
def test_gdsii_arrow_plain_srefs_decode_without_arbitrary(tmp_path: Path) -> None:
|
|
lib = _make_arrow_test_library()
|
|
gds_file = tmp_path / 'plain_srefs.gds'
|
|
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
|
|
|
arrow_lib, _ = gdsii_arrow.readfile(gds_file)
|
|
fanout = arrow_lib['fanout']
|
|
|
|
plain_leaf_refs = [
|
|
ref
|
|
for ref in fanout.refs['leaf']
|
|
if ref.annotations is None and ref.repetition is None
|
|
]
|
|
assert len(plain_leaf_refs) == 2
|
|
assert all(type(ref.repetition) is not Grid for ref in plain_leaf_refs)
|
|
|
|
|
|
def test_gdsii_arrow_degenerate_aref_schema_normalizes_to_sref(tmp_path: Path) -> None:
|
|
lib = Library()
|
|
leaf = Pattern()
|
|
leaf.polygon((1, 0), vertices=[[0, 0], [5, 0], [5, 5], [0, 5]])
|
|
lib['leaf'] = leaf
|
|
|
|
top = Pattern()
|
|
top.ref('leaf', offset=(100, 200), repetition=Grid(a_vector=(7, 0), a_count=1, b_vector=(0, 9), b_count=1))
|
|
lib['top'] = top
|
|
|
|
gds_file = tmp_path / 'degenerate_aref_schema.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()
|
|
top_index = next(ii for ii, cell_id in enumerate(cell_ids) if cell_names[cell_id] == 'top')
|
|
|
|
srefs = cells.field('srefs')[top_index].as_py()
|
|
arefs = cells.field('arefs')[top_index].as_py()
|
|
|
|
assert len(srefs) == 1
|
|
assert len(arefs) == 0
|
|
assert cell_names[srefs[0]['target']] == 'leaf'
|
|
|
|
|
|
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')
|
|
|
|
rect_batches = cells.field('rect_batches')[leaf_index].as_py()
|
|
boundary_batches = cells.field('boundary_batches')[leaf_index].as_py()
|
|
boundary_props = cells.field('boundary_props')[leaf_index].as_py()
|
|
|
|
assert len(rect_batches) == 2
|
|
assert len(boundary_batches) == 0
|
|
assert len(boundary_props) == 2
|
|
|
|
rects_by_layer = {tuple(layer_table[entry['layer']]): entry for entry in rect_batches}
|
|
assert rects_by_layer[(1, 0)]['rects'] == [20, 0, 30, 10, 80, 0, 90, 10]
|
|
assert rects_by_layer[(2, 0)]['rects'] == [40, 0, 50, 10]
|
|
|
|
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_gdsii_arrow_rect_batch_schema_for_mixed_layer(tmp_path: Path) -> None:
|
|
lib = Library()
|
|
top = Pattern()
|
|
top.shapes[(1, 0)].append(RectCollection(rects=[[0, 0, 10, 10], [20, 0, 30, 10], [40, 0, 50, 10], [60, 0, 70, 10]]))
|
|
top.polygon((1, 0), vertices=[[80, 0], [85, 10], [90, 0]])
|
|
top.polygon((1, 0), vertices=[[100, 0], [105, 10], [110, 0]])
|
|
lib['top'] = top
|
|
|
|
gds_file = tmp_path / 'arrow_rect_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()
|
|
]
|
|
top_index = next(ii for ii, cell_id in enumerate(cell_ids) if cell_names[cell_id] == 'top')
|
|
|
|
rect_batches = cells.field('rect_batches')[top_index].as_py()
|
|
boundary_batches = cells.field('boundary_batches')[top_index].as_py()
|
|
|
|
assert len(rect_batches) == 1
|
|
assert tuple(layer_table[rect_batches[0]['layer']]) == (1, 0)
|
|
assert rect_batches[0]['rects'] == [
|
|
0, 0, 10, 10,
|
|
20, 0, 30, 10,
|
|
40, 0, 50, 10,
|
|
60, 0, 70, 10,
|
|
]
|
|
|
|
assert len(boundary_batches) == 1
|
|
assert tuple(layer_table[boundary_batches[0]['layer']]) == (1, 0)
|
|
assert boundary_batches[0]['vertex_offsets'] == [0, 3]
|
|
|
|
|
|
def test_gdsii_arrow_ref_schema(tmp_path: Path) -> None:
|
|
lib = _make_arrow_test_library()
|
|
gds_file = tmp_path / 'arrow_ref_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()
|
|
|
|
fanout_index = next(ii for ii, cell_id in enumerate(cell_ids) if cell_names[cell_id] == 'fanout')
|
|
|
|
srefs = cells.field('srefs')[fanout_index].as_py()
|
|
arefs = cells.field('arefs')[fanout_index].as_py()
|
|
sref_props = cells.field('sref_props')[fanout_index].as_py()
|
|
aref_props = cells.field('aref_props')[fanout_index].as_py()
|
|
|
|
sref_target_ids = [entry['target'] for entry in srefs]
|
|
sref_targets = [cell_names[target] for target in sref_target_ids]
|
|
assert sorted(sref_targets) == ['child', 'leaf', 'leaf']
|
|
assert sref_target_ids == sorted(sref_target_ids)
|
|
sref_by_target = {}
|
|
for entry in srefs:
|
|
sref_by_target.setdefault(cell_names[entry['target']], []).append(entry)
|
|
assert [entry['invert_y'] for entry in sref_by_target['child']] == [True]
|
|
assert [entry['scale'] for entry in sref_by_target['child']] == pytest.approx([1.1])
|
|
assert len(sref_by_target['leaf']) == 2
|
|
|
|
aref_target_ids = [entry['target'] for entry in arefs]
|
|
aref_targets = [cell_names[target] for target in aref_target_ids]
|
|
assert sorted(aref_targets) == ['child', 'leaf', 'leaf']
|
|
assert aref_target_ids == sorted(aref_target_ids)
|
|
aref_by_target = {}
|
|
for entry in arefs:
|
|
aref_by_target.setdefault(cell_names[entry['target']], []).append(entry)
|
|
assert [entry['invert_y'] for entry in aref_by_target['child']] == [True]
|
|
assert [entry['scale'] for entry in aref_by_target['child']] == pytest.approx([1.2])
|
|
assert len(aref_by_target['leaf']) == 2
|
|
|
|
assert len(sref_props) == 1
|
|
assert cell_names[sref_props[0]['target']] == 'leaf'
|
|
assert sref_props[0]['properties'][0]['value'] == 'fanout-sref'
|
|
|
|
assert len(aref_props) == 1
|
|
assert cell_names[aref_props[0]['target']] == 'child'
|
|
assert aref_props[0]['properties'][0]['value'] == 'fanout-aref'
|
|
|
|
|
|
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_poly = Polygon._from_raw(
|
|
vertices=numpy.array([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 5.0]]),
|
|
annotations={'1': ['poly']},
|
|
)
|
|
public_poly = Polygon(
|
|
vertices=[[0, 0], [5, 0], [5, 5], [0, 5]],
|
|
annotations={'1': ['poly']},
|
|
)
|
|
assert raw_poly == public_poly
|
|
|
|
raw_poly_collection = PolyCollection._from_raw(
|
|
vertex_lists=numpy.array([
|
|
[0.0, 0.0], [2.0, 0.0], [2.0, 2.0],
|
|
[10.0, 10.0], [12.0, 10.0], [12.0, 12.0],
|
|
]),
|
|
vertex_offsets=numpy.array([0, 3], dtype=numpy.uint32),
|
|
annotations={'2': ['pc']},
|
|
)
|
|
public_poly_collection = PolyCollection(
|
|
vertex_lists=[[0, 0], [2, 0], [2, 2], [10, 10], [12, 10], [12, 12]],
|
|
vertex_offsets=[0, 3],
|
|
annotations={'2': ['pc']},
|
|
)
|
|
assert raw_poly_collection == public_poly_collection
|
|
assert [tuple(s.indices(len(raw_poly_collection.vertex_lists))) for s in raw_poly_collection.vertex_slices] == [(0, 3, 1), (3, 6, 1)]
|
|
|
|
raw_rect_collection = RectCollection._from_raw(
|
|
rects=numpy.array([[10.0, 10.0, 12.0, 12.0], [0.0, 0.0, 5.0, 5.0]]),
|
|
annotations={'3': ['rects']},
|
|
)
|
|
public_rect_collection = RectCollection(
|
|
rects=[[0, 0, 5, 5], [10, 10, 12, 12]],
|
|
annotations={'3': ['rects']},
|
|
)
|
|
assert raw_rect_collection == public_rect_collection
|
|
|
|
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())
|