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+ )
1621
1722filepath = Path (os .path .dirname (os .path .abspath (__file__ )))
1823KLAYOUT_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