1212import tidy3d as td
1313from tidy3d .exceptions import FileError
1414from 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+ )
1621from tidy3d .plugins .klayout .util import check_installation
1722
1823filepath = 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