Skip to content

Commit c24ee38

Browse files
feat(tidy3d): FXC-4413-get-cell-name-from-violation-marker-in-klayout-plugin
1 parent 3e3526a commit c24ee38

File tree

3 files changed

+156
-34
lines changed

3 files changed

+156
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- Added `symmetrize_mirror`, `symmetrize_rotation`, `symmetrize_diagonal` functions to the autograd plugin. They can be used for enforcing symmetries in topology optimization.
12+
- Get cell-related information from violation markers in `DRCResults` and `DRCViolation` to the klayout plugin: Use for example `DRCResults.violations_by_cell` to group them.
1213

1314
### Changed
1415
- Removed validator that would warn if `PerturbationMedium` values could become numerically unstable, since an error will anyway be raised if this actually happens when the medium is converted using actual perturbation data.

tests/test_plugins/klayout/drc/test_drc.py

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
import tidy3d as td
1313
from tidy3d.exceptions import FileError
1414
from tidy3d.plugins.klayout.drc.drc import DRCConfig, DRCRunner, run_drc_on_gds
15-
from tidy3d.plugins.klayout.drc.results import DRCResults, parse_violation_value
15+
from tidy3d.plugins.klayout.drc.results import (
16+
DRCResults,
17+
DRCViolation,
18+
EdgeMarker,
19+
parse_violation_value,
20+
)
1621
from tidy3d.plugins.klayout.util import check_installation
1722

1823
filepath = Path(os.path.dirname(os.path.abspath(__file__)))
@@ -41,6 +46,7 @@ def _write_results_file(
4146
category: str = "min_width",
4247
num_items: int = 1,
4348
filename: str = "many_results.lyrdb",
49+
cells: tuple[str, ...] | None = None,
4450
) -> Path:
4551
"""Write a simple DRC results file with the requested number of items."""
4652

@@ -65,7 +71,7 @@ def _write_results_file(
6571
<item>
6672
<tags/>
6773
<category>{category}</category>
68-
<cell>TOP</cell>
74+
<cell>{cell}</cell>
6975
<visited>false</visited>
7076
<multiplicity>1</multiplicity>
7177
<comment/>
@@ -76,7 +82,11 @@ def _write_results_file(
7682
</item>
7783
"""
7884
contents = [template_header]
79-
contents.extend(item.format(category=category) for _ in range(num_items))
85+
for idx in range(num_items):
86+
cell = "TOP"
87+
if cells is not None and idx < len(cells):
88+
cell = cells[idx]
89+
contents.append(item.format(category=category, cell=cell))
8090
contents.append(template_footer)
8191
path = tmp_path / filename
8292
path.write_text("".join(contents))
@@ -574,6 +584,62 @@ def test_drc_result_markers(self, drc_results):
574584
(-0.206, 0.342),
575585
(-0.206, 0.24),
576586
)
587+
for violation in drc_results.violations_by_category.values():
588+
for marker in violation.markers:
589+
assert marker.cell == "TOP"
590+
591+
def test_drc_violation_cell_helpers(self):
592+
"""DRCViolation provides cell-aware helpers."""
593+
violation = DRCViolation(
594+
category="cat_a",
595+
markers=(
596+
EdgeMarker(cell="CELL_A", edge=((0.0, 0.0), (1.0, 1.0))),
597+
EdgeMarker(cell="CELL_B", edge=((1.0, 1.0), (2.0, 2.0))),
598+
EdgeMarker(cell="CELL_A", edge=((0.5, 0.5), (1.5, 1.5))),
599+
),
600+
)
601+
assert violation.violated_cells == ("CELL_A", "CELL_B")
602+
603+
by_cell = violation.violations_by_cell
604+
assert set(by_cell) == {"CELL_A", "CELL_B"}
605+
assert by_cell["CELL_B"].count == 1
606+
607+
markers_cell_a = by_cell["CELL_A"].markers
608+
assert all(marker.cell == "CELL_A" for marker in markers_cell_a)
609+
610+
def test_drc_results_cell_helpers(self):
611+
"""DRCResults aggregates violations across cells."""
612+
violation_a = DRCViolation(
613+
category="cat_a",
614+
markers=(
615+
EdgeMarker(cell="CELL_A", edge=((0.0, 0.0), (1.0, 1.0))),
616+
EdgeMarker(cell="CELL_B", edge=((1.0, 1.0), (2.0, 2.0))),
617+
),
618+
)
619+
violation_b = DRCViolation(
620+
category="cat_b",
621+
markers=(
622+
EdgeMarker(cell="CELL_B", edge=((2.0, 2.0), (3.0, 3.0))),
623+
EdgeMarker(cell="CELL_C", edge=((3.0, 3.0), (4.0, 4.0))),
624+
),
625+
)
626+
results = DRCResults(
627+
violations_by_category={
628+
"cat_a": violation_a,
629+
"cat_b": violation_b,
630+
}
631+
)
632+
assert results.violated_cells == ("CELL_A", "CELL_B", "CELL_C")
633+
634+
violations_by_cell = results.violations_by_cell
635+
assert set(violations_by_cell) == {"CELL_A", "CELL_B", "CELL_C"}
636+
assert len(violations_by_cell["CELL_A"]) == 1
637+
assert len(violations_by_cell["CELL_C"]) == 1
638+
639+
cell_b_violations = violations_by_cell["CELL_B"]
640+
assert {violation.category for violation in cell_b_violations} == {"cat_a", "cat_b"}
641+
for violation in cell_b_violations:
642+
assert all(marker.cell == "CELL_B" for marker in violation.markers)
577643

578644
@pytest.mark.parametrize(
579645
"edge_value, expected_edge",
@@ -584,30 +650,34 @@ def test_drc_result_markers(self, drc_results):
584650
)
585651
def test_parse_edge(self, edge_value, expected_edge):
586652
"""Test parsing edge violation values."""
587-
edge_result = parse_violation_value(edge_value)
653+
edge_result = parse_violation_value(edge_value, cell="TEST_CELL")
588654
assert edge_result.edge == expected_edge
655+
assert edge_result.cell == "TEST_CELL"
589656

590657
def test_parse_edge_pair(self):
591658
"""Test parsing edge-pair violation values."""
592659
edge_pair_value = "edge-pair: (1.0,2.0;3.0,4.0)|(5.0,6.0;7.0,8.0)"
593-
edge_pair_result = parse_violation_value(edge_pair_value)
660+
edge_pair_result = parse_violation_value(edge_pair_value, cell="TEST_CELL")
594661
assert edge_pair_result.edge_pair[0] == ((1.0, 2.0), (3.0, 4.0))
595662
assert edge_pair_result.edge_pair[1] == ((5.0, 6.0), (7.0, 8.0))
663+
assert edge_pair_result.cell == "TEST_CELL"
596664

597665
def test_parse_polygon(self):
598666
"""Test parsing a single polygon violation string."""
599667
polygon_value = "polygon: (1.0,2.0;3.0,4.0;5.0,6.0;1.0,2.0)"
600-
polygon_result = parse_violation_value(polygon_value)
668+
polygon_result = parse_violation_value(polygon_value, cell="TEST_CELL")
601669
assert polygon_result.polygons[0] == ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0))
670+
assert polygon_result.cell == "TEST_CELL"
602671

603672
def test_parse_multiple_polygons(self):
604673
"""Test parsing multiple polygons violation string."""
605674
polygon_value = (
606675
"polygon: (1.0,2.0;3.0,4.0;5.0,6.0;1.0,2.0/7.0,8.0;9.0,10.0;11.0,12.0;7.0,8.0)"
607676
)
608-
polygon_result = parse_violation_value(polygon_value)
677+
polygon_result = parse_violation_value(polygon_value, cell="TEST_CELL")
609678
assert polygon_result.polygons[0] == ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0))
610679
assert polygon_result.polygons[1] == ((7.0, 8.0), (9.0, 10.0), (11.0, 12.0), (7.0, 8.0))
680+
assert polygon_result.cell == "TEST_CELL"
611681

612682
@pytest.mark.parametrize(
613683
"invalid_edge",
@@ -623,7 +693,7 @@ def test_parse_multiple_polygons(self):
623693
def test_parse_invalid_edge_format(self, invalid_edge):
624694
"""Test parsing invalid violation format."""
625695
with pytest.raises(ValueError):
626-
parse_violation_value(invalid_edge)
696+
parse_violation_value(invalid_edge, cell="TEST_CELL")
627697

628698
@pytest.mark.parametrize(
629699
"invalid_edge_pair",
@@ -636,7 +706,7 @@ def test_parse_invalid_edge_format(self, invalid_edge):
636706
def test_parse_invalid_edge_pair_format(self, invalid_edge_pair):
637707
"""Test parsing invalid edge-pair violation format."""
638708
with pytest.raises(ValueError):
639-
parse_violation_value(invalid_edge_pair)
709+
parse_violation_value(invalid_edge_pair, cell="TEST_CELL")
640710

641711
@pytest.mark.parametrize(
642712
"invalid_polygon",
@@ -649,7 +719,7 @@ def test_parse_invalid_edge_pair_format(self, invalid_edge_pair):
649719
def test_parse_invalid_polygon_format(self, invalid_polygon):
650720
"""Test parsing invalid polygon violation format."""
651721
with pytest.raises(ValueError):
652-
parse_violation_value(invalid_polygon)
722+
parse_violation_value(invalid_polygon, cell="TEST_CELL")
653723

654724
@pytest.mark.parametrize(
655725
"invalid_polygons",
@@ -662,12 +732,12 @@ def test_parse_invalid_polygon_format(self, invalid_polygon):
662732
def test_parse_invalid_polygon_format_multiple_polygons(self, invalid_polygons):
663733
"""Test parsing invalid polygon violation format with multiple polygons."""
664734
with pytest.raises(ValueError) as e:
665-
parse_violation_value(invalid_polygons)
735+
parse_violation_value(invalid_polygons, cell="TEST_CELL")
666736

667737
def test_parse_violation_value_unknown_type(self):
668738
"""Test parsing unknown violation type."""
669739
with pytest.raises(ValueError):
670-
parse_violation_value("unknown: (1.0,2.0)")
740+
parse_violation_value("unknown: (1.0,2.0)", cell="TEST_CELL")
671741

672742
def test_results_warn_without_limit(self, monkeypatch, tmp_path):
673743
"""Warn when no limit is set and a category exceeds the threshold."""

0 commit comments

Comments
 (0)