Coverage for tests/rcx25/rcx25_test_helpers.py: 100%

78 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-31 20:14 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX  

7# (see https://github.com/martinjankoehler/klayout-pex). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21# SPDX-License-Identifier: GPL-3.0-or-later 

22# -------------------------------------------------------------------------------- 

23# 

24from __future__ import annotations 

25 

26from dataclasses import dataclass 

27from enum import StrEnum 

28import io 

29import json 

30import os 

31import tempfile 

32from typing import * 

33 

34import allure 

35import csv_diff 

36 

37import klayout.db as kdb 

38import klayout.lay as klay 

39 

40from klayout_pex.kpex_cli import KpexCLI 

41from klayout_pex.rcx25.extraction_results import CellExtractionResults 

42from klayout_pex.rcx25.pex_mode import PEXMode 

43 

44 

45class PDKName(StrEnum): 

46 SKY130A = 'sky130A' 

47 IHP_SG13G2 = 'ihp_sg13g2' 

48 

49 

50@dataclass 

51class PDKTestConfig: 

52 name: PDKName 

53 

54 @property 

55 def kpex_pdk_dir(self) -> str: 

56 return os.path.realpath(os.path.join(__file__, '..', '..', '..', 

57 'pdk', self.name, 'libs.tech', 'kpex')) 

58 

59 @property 

60 def test_designs_dir(self) -> str: 

61 return os.path.realpath(os.path.join(__file__, '..', '..', '..', 

62 'testdata', 'designs', self.name)) 

63 

64 @property 

65 def lyt_path(self) -> str: 

66 return os.path.abspath(os.path.join(self.kpex_pdk_dir, 'sky130A.lyt')) 

67 

68 def load_kdb_technology(self) -> kdb.Technology: 

69 kdb.Technology.clear_technologies() 

70 tech = kdb.Technology.create_technology('sky130A') 

71 tech.load(self.lyt_path) 

72 return tech 

73 

74 def gds_path(self, *path_components) -> str: 

75 return os.path.join(self.test_designs_dir, *path_components) 

76 

77 

78@dataclass 

79class RCX25Extraction: 

80 pdk: PDKTestConfig 

81 pex_mode: PEXMode 

82 blackbox: bool 

83 

84 def save_layout_preview(self, gds_path: str, output_png_path: str): 

85 self.pdk.load_kdb_technology() 

86 lv = klay.LayoutView() 

87 lv.load_layout(gds_path) 

88 lv.max_hier() 

89 lv.set_config('background-color', '#000000') 

90 lv.set_config('bitmap-oversampling', '1') 

91 lv.set_config('default-font-size', '4') 

92 lv.set_config('default-text-size', '0.1') 

93 lv.save_image_with_options( 

94 output_png_path, 

95 width=4096, height=2160 

96 # , 

97 # linewidth=2, 

98 # resolution=0.25 # 4x as large fonts 

99 ) 

100 

101 def run_rcx25d_single_cell(self, *path_components) -> Tuple[CellExtractionResults, CSVPath, PNGPath]: 

102 gds_path = self.pdk.gds_path(*path_components) 

103 

104 preview_png_path = tempfile.mktemp(prefix=f"layout_preview_", suffix=".png") 

105 self.save_layout_preview(gds_path, preview_png_path) 

106 output_dir_path = os.path.realpath(os.path.join(__file__, '..', '..', '..', f"output_{self.pdk.name}")) 

107 cli = KpexCLI() 

108 cli.main(['main', 

109 '--pdk', self.pdk.name, 

110 '--mode', self.pex_mode, 

111 '--blackbox', 'y' if self.blackbox else 'n', 

112 '--gds', gds_path, 

113 '--out_dir', output_dir_path, 

114 '--2.5D', 

115 '--halo', '10000', 

116 '--scale', 'n']) 

117 assert cli.rcx25_extraction_results is not None 

118 assert len(cli.rcx25_extraction_results.cell_extraction_results) == 1 # assume single cell test 

119 results = list(cli.rcx25_extraction_results.cell_extraction_results.values())[0] 

120 assert results.cell_name == path_components[-1][:-len('.gds.gz')] 

121 return results, cli.rcx25_extracted_csv_path, preview_png_path 

122 

123 def assert_expected_matches_obtained(self, 

124 *path_components, 

125 expected_csv_content: str) -> CellExtractionResults: 

126 result, csv, preview_png = self.run_rcx25d_single_cell(*path_components) 

127 allure.attach.file(csv, name='pex_obtained.csv', attachment_type=allure.attachment_type.CSV) 

128 allure.attach.file(preview_png, name='📸 layout_preview.png', attachment_type=allure.attachment_type.PNG) 

129 expected_csv = csv_diff.load_csv(io.StringIO(expected_csv_content), key='Device') 

130 with open(csv, 'r') as f: 

131 obtained_csv = csv_diff.load_csv(f, key='Device') 

132 diff = csv_diff.compare(expected_csv, obtained_csv, show_unchanged=False) 

133 human_diff = csv_diff.human_text( 

134 diff, current=obtained_csv, extras=(('Net1','{Net1}'),('Net2','{Net2}')) 

135 ) 

136 allure.attach(expected_csv_content, name='pex_expected.csv', attachment_type=allure.attachment_type.CSV) 

137 allure.attach(json.dumps(diff, sort_keys=True, indent=' ').encode("utf8"), 

138 name='pex_diff.json', attachment_type=allure.attachment_type.JSON) 

139 allure.attach(human_diff.encode("utf8"), name='‼️ pex_diff.txt', attachment_type=allure.attachment_type.TEXT) 

140 # assert diff['added'] == [] 

141 # assert diff['removed'] == [] 

142 # assert diff['changed'] == [] 

143 # assert diff['columns_added'] == [] 

144 # assert diff['columns_removed'] == [] 

145 assert human_diff == '', 'Diff detected' 

146 return result