[gdsii_arrow] more performance work

This commit is contained in:
Jan Petykiewicz 2026-04-02 19:42:52 -07:00
commit 85aed4388e
4 changed files with 558 additions and 96 deletions

View file

@ -5,7 +5,7 @@ from numpy.testing import assert_allclose
from ..pattern import Pattern
from ..library import Library
from ..shapes import Path as MPath, Circle, Polygon
from ..shapes import Path as MPath, Circle, Polygon, RectCollection
from ..repetition import Grid, Arbitrary
def create_test_library(for_gds: bool = False) -> Library:
@ -109,3 +109,30 @@ def test_oasis_full_roundtrip(tmp_path: Path) -> None:
assert poly.repetition is not None
assert isinstance(poly.repetition, Grid)
assert poly.repetition.a_count == 5
def test_gdsii_rect_collection_roundtrip(tmp_path: Path) -> None:
from ..file import gdsii
lib = Library()
pat = Pattern()
pat.shapes[(5, 0)].append(
RectCollection(
rects=[[0, 0, 10, 5], [20, -5, 30, 10]],
annotations={'1': ['rects']},
)
)
lib['rects'] = pat
gds_file = tmp_path / 'rect_collection.gds'
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
read_lib, _ = gdsii.readfile(gds_file)
polys = read_lib['rects'].shapes[(5, 0)]
assert len(polys) == 2
assert all(isinstance(poly, Polygon) for poly in polys)
assert_allclose(polys[0].vertices, [[0, 0], [0, 5], [10, 5], [10, 0]])
assert_allclose(polys[1].vertices, [[20, -5], [20, 10], [30, 10], [30, -5]])
assert polys[0].annotations == {'1': ['rects']}
assert polys[1].annotations == {'1': ['rects']}

View file

@ -9,7 +9,7 @@ from .. import Ref, Label
from ..library import Library
from ..pattern import Pattern
from ..repetition import Grid
from ..shapes import Path as MPath
from ..shapes import Path as MPath, Polygon, PolyCollection, RectCollection
from ..file import gdsii, gdsii_arrow
from ..file.gdsii_perf import write_fixture
@ -31,6 +31,14 @@ def _coord_key(values: object) -> tuple[int, ...] | tuple[tuple[int, int], ...]:
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)
@ -50,31 +58,25 @@ def _shape_key(shape: object, layer: tuple[int, int]) -> list[tuple[object, ...]
keys.append((
'polygon',
layer,
_coord_key(poly.vertices),
_canonical_polygon_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 _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, ...]:
@ -92,11 +94,10 @@ def _pattern_summary(pattern: Pattern) -> dict[str, object]:
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
]
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)
@ -151,9 +152,23 @@ def _make_arrow_test_library() -> Library:
)
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
@ -184,6 +199,71 @@ def test_gdsii_arrow_reads_small_perf_fixture(tmp_path: Path) -> None:
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'
@ -200,17 +280,17 @@ def test_gdsii_arrow_boundary_batch_schema(tmp_path: Path) -> None:
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(boundary_batches) == 2
assert len(rect_batches) == 2
assert len(boundary_batches) == 0
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
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)]
@ -218,6 +298,92 @@ def test_gdsii_arrow_boundary_batch_schema(tmp_path: Path) -> None:
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]),
@ -228,6 +394,42 @@ def test_raw_ref_grid_label_constructors_match_public() -> None:
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,