Skip to content

Commit 7db47cf

Browse files
feat(tidy3d): FXC-4413-get-cell-name-from-violation-marker-in-klayout-plugin
1 parent 8c4906e commit 7db47cf

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
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Klayout plugin automatically finds klayout installation path at common locations.
1313
- Added autograd support for `TriangleMesh`, allowing gradient computation with respect to mesh vertices for inverse design.
1414
- Added `EMESimulationData.coeffs` to store coefficients from the EME solver, including mode overlaps, interface S matrices, and effective propagation indices.
15+
- 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.
1516

1617
### Changed
1718
- 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

1722
filepath = Path(os.path.dirname(os.path.abspath(__file__)))
1823
KLAYOUT_PLUGIN_PATH = "tidy3d.plugins.klayout"
@@ -40,6 +45,7 @@ def _write_results_file(
4045
category: str = "min_width",
4146
num_items: int = 1,
4247
filename: str = "many_results.lyrdb",
48+
cells: tuple[str, ...] | None = None,
4349
) -> Path:
4450
"""Write a simple DRC results file with the requested number of items."""
4551

@@ -64,7 +70,7 @@ def _write_results_file(
6470
<item>
6571
<tags/>
6672
<category>{category}</category>
67-
<cell>TOP</cell>
73+
<cell>{cell}</cell>
6874
<visited>false</visited>
6975
<multiplicity>1</multiplicity>
7076
<comment/>
@@ -75,7 +81,11 @@ def _write_results_file(
7581
</item>
7682
"""
7783
contents = [template_header]
78-
contents.extend(item.format(category=category) for _ in range(num_items))
84+
for idx in range(num_items):
85+
cell = "TOP"
86+
if cells is not None and idx < len(cells):
87+
cell = cells[idx]
88+
contents.append(item.format(category=category, cell=cell))
7989
contents.append(template_footer)
8090
path = tmp_path / filename
8191
path.write_text("".join(contents))
@@ -556,6 +566,62 @@ def test_drc_result_markers(self, drc_results):
556566
(-0.206, 0.342),
557567
(-0.206, 0.24),
558568
)
569+
for violation in drc_results.violations_by_category.values():
570+
for marker in violation.markers:
571+
assert marker.cell == "TOP"
572+
573+
def test_drc_violation_cell_helpers(self):
574+
"""DRCViolation provides cell-aware helpers."""
575+
violation = DRCViolation(
576+
category="cat_a",
577+
markers=(
578+
EdgeMarker(cell="CELL_A", edge=((0.0, 0.0), (1.0, 1.0))),
579+
EdgeMarker(cell="CELL_B", edge=((1.0, 1.0), (2.0, 2.0))),
580+
EdgeMarker(cell="CELL_A", edge=((0.5, 0.5), (1.5, 1.5))),
581+
),
582+
)
583+
assert violation.violated_cells == ("CELL_A", "CELL_B")
584+
585+
by_cell = violation.violations_by_cell
586+
assert set(by_cell) == {"CELL_A", "CELL_B"}
587+
assert by_cell["CELL_B"].count == 1
588+
589+
markers_cell_a = by_cell["CELL_A"].markers
590+
assert all(marker.cell == "CELL_A" for marker in markers_cell_a)
591+
592+
def test_drc_results_cell_helpers(self):
593+
"""DRCResults aggregates violations across cells."""
594+
violation_a = DRCViolation(
595+
category="cat_a",
596+
markers=(
597+
EdgeMarker(cell="CELL_A", edge=((0.0, 0.0), (1.0, 1.0))),
598+
EdgeMarker(cell="CELL_B", edge=((1.0, 1.0), (2.0, 2.0))),
599+
),
600+
)
601+
violation_b = DRCViolation(
602+
category="cat_b",
603+
markers=(
604+
EdgeMarker(cell="CELL_B", edge=((2.0, 2.0), (3.0, 3.0))),
605+
EdgeMarker(cell="CELL_C", edge=((3.0, 3.0), (4.0, 4.0))),
606+
),
607+
)
608+
results = DRCResults(
609+
violations_by_category={
610+
"cat_a": violation_a,
611+
"cat_b": violation_b,
612+
}
613+
)
614+
assert results.violated_cells == ("CELL_A", "CELL_B", "CELL_C")
615+
616+
violations_by_cell = results.violations_by_cell
617+
assert set(violations_by_cell) == {"CELL_A", "CELL_B", "CELL_C"}
618+
assert len(violations_by_cell["CELL_A"]) == 1
619+
assert len(violations_by_cell["CELL_C"]) == 1
620+
621+
cell_b_violations = violations_by_cell["CELL_B"]
622+
assert {violation.category for violation in cell_b_violations} == {"cat_a", "cat_b"}
623+
for violation in cell_b_violations:
624+
assert all(marker.cell == "CELL_B" for marker in violation.markers)
559625

560626
@pytest.mark.parametrize(
561627
"edge_value, expected_edge",
@@ -566,30 +632,34 @@ def test_drc_result_markers(self, drc_results):
566632
)
567633
def test_parse_edge(self, edge_value, expected_edge):
568634
"""Test parsing edge violation values."""
569-
edge_result = parse_violation_value(edge_value)
635+
edge_result = parse_violation_value(edge_value, cell="TEST_CELL")
570636
assert edge_result.edge == expected_edge
637+
assert edge_result.cell == "TEST_CELL"
571638

572639
def test_parse_edge_pair(self):
573640
"""Test parsing edge-pair violation values."""
574641
edge_pair_value = "edge-pair: (1.0,2.0;3.0,4.0)|(5.0,6.0;7.0,8.0)"
575-
edge_pair_result = parse_violation_value(edge_pair_value)
642+
edge_pair_result = parse_violation_value(edge_pair_value, cell="TEST_CELL")
576643
assert edge_pair_result.edge_pair[0] == ((1.0, 2.0), (3.0, 4.0))
577644
assert edge_pair_result.edge_pair[1] == ((5.0, 6.0), (7.0, 8.0))
645+
assert edge_pair_result.cell == "TEST_CELL"
578646

579647
def test_parse_polygon(self):
580648
"""Test parsing a single polygon violation string."""
581649
polygon_value = "polygon: (1.0,2.0;3.0,4.0;5.0,6.0;1.0,2.0)"
582-
polygon_result = parse_violation_value(polygon_value)
650+
polygon_result = parse_violation_value(polygon_value, cell="TEST_CELL")
583651
assert polygon_result.polygons[0] == ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0))
652+
assert polygon_result.cell == "TEST_CELL"
584653

585654
def test_parse_multiple_polygons(self):
586655
"""Test parsing multiple polygons violation string."""
587656
polygon_value = (
588657
"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)"
589658
)
590-
polygon_result = parse_violation_value(polygon_value)
659+
polygon_result = parse_violation_value(polygon_value, cell="TEST_CELL")
591660
assert polygon_result.polygons[0] == ((1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0))
592661
assert polygon_result.polygons[1] == ((7.0, 8.0), (9.0, 10.0), (11.0, 12.0), (7.0, 8.0))
662+
assert polygon_result.cell == "TEST_CELL"
593663

594664
@pytest.mark.parametrize(
595665
"invalid_edge",
@@ -605,7 +675,7 @@ def test_parse_multiple_polygons(self):
605675
def test_parse_invalid_edge_format(self, invalid_edge):
606676
"""Test parsing invalid violation format."""
607677
with pytest.raises(ValueError):
608-
parse_violation_value(invalid_edge)
678+
parse_violation_value(invalid_edge, cell="TEST_CELL")
609679

610680
@pytest.mark.parametrize(
611681
"invalid_edge_pair",
@@ -618,7 +688,7 @@ def test_parse_invalid_edge_format(self, invalid_edge):
618688
def test_parse_invalid_edge_pair_format(self, invalid_edge_pair):
619689
"""Test parsing invalid edge-pair violation format."""
620690
with pytest.raises(ValueError):
621-
parse_violation_value(invalid_edge_pair)
691+
parse_violation_value(invalid_edge_pair, cell="TEST_CELL")
622692

623693
@pytest.mark.parametrize(
624694
"invalid_polygon",
@@ -631,7 +701,7 @@ def test_parse_invalid_edge_pair_format(self, invalid_edge_pair):
631701
def test_parse_invalid_polygon_format(self, invalid_polygon):
632702
"""Test parsing invalid polygon violation format."""
633703
with pytest.raises(ValueError):
634-
parse_violation_value(invalid_polygon)
704+
parse_violation_value(invalid_polygon, cell="TEST_CELL")
635705

636706
@pytest.mark.parametrize(
637707
"invalid_polygons",
@@ -644,12 +714,12 @@ def test_parse_invalid_polygon_format(self, invalid_polygon):
644714
def test_parse_invalid_polygon_format_multiple_polygons(self, invalid_polygons):
645715
"""Test parsing invalid polygon violation format with multiple polygons."""
646716
with pytest.raises(ValueError) as e:
647-
parse_violation_value(invalid_polygons)
717+
parse_violation_value(invalid_polygons, cell="TEST_CELL")
648718

649719
def test_parse_violation_value_unknown_type(self):
650720
"""Test parsing unknown violation type."""
651721
with pytest.raises(ValueError):
652-
parse_violation_value("unknown: (1.0,2.0)")
722+
parse_violation_value("unknown: (1.0,2.0)", cell="TEST_CELL")
653723

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

0 commit comments

Comments
 (0)