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

87 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 23:32 +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 glob 

29import io 

30import json 

31import os 

32import tempfile 

33from typing import * 

34 

35import allure 

36import csv_diff 

37 

38import klayout.db as kdb 

39import klayout.lay as klay 

40 

41from klayout_pex.kpex_cli import KpexCLI 

42from klayout_pex.rcx25.extraction_results import CellExtractionResults 

43from klayout_pex.rcx25.pex_mode import PEXMode 

44 

45 

46class PDKName(StrEnum): 

47 SKY130A = 'sky130A' 

48 IHP_SG13G2 = 'ihp-sg13g2' 

49 GF180MCUD = 'gf180mcuD' 

50 

51 

52@dataclass 

53class PDKTestConfig: 

54 name: PDKName 

55 

56 @property 

57 def kpex_pdk_dir(self) -> str: 

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

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

60 

61 @property 

62 def test_designs_dir(self) -> str: 

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

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

65 

66 @property 

67 def lyt_path(self) -> str: 

68 lyt_paths = glob.glob(os.path.join(self.kpex_pdk_dir, '*.lyt')) 

69 assert len(lyt_paths) == 1, f"Expected one KLayout technology file in {self.kpex_pdk_dir}" 

70 return os.path.abspath(lyt_paths[0]) 

71 

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

73 kdb.Technology.clear_technologies() 

74 tech = kdb.Technology.create_technology(self.name) 

75 tech.load(self.lyt_path) 

76 return tech 

77 

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

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

80 

81 

82@dataclass 

83class RCX25Extraction: 

84 pdk: PDKTestConfig 

85 pex_mode: PEXMode 

86 blackbox: bool 

87 

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

89 self.pdk.load_kdb_technology() 

90 lv = klay.LayoutView() 

91 lv.load_layout(gds_path) 

92 lv.max_hier() 

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

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

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

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

97 lv.save_image_with_options( 

98 output_png_path, 

99 width=4096, height=2160 

100 # , 

101 # linewidth=2, 

102 # resolution=0.25 # 4x as large fonts 

103 ) 

104 

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

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

107 

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

109 self.save_layout_preview(gds_path, preview_png_path) 

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

111 cli = KpexCLI() 

112 cli.main(['main', 

113 '--pdk', self.pdk.name, 

114 '--mode', self.pex_mode, 

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

116 '--gds', gds_path, 

117 '--out_dir', output_dir_path, 

118 '--2.5D', 

119 '--halo', '10000', 

120 '--scale', 'n']) 

121 assert cli.rcx25_extraction_results is not None 

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

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

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

125 return results, cli.rcx25_extracted_csv_path, preview_png_path 

126 

127 def written_device_lines(self, *path_components, prefixes: str = 'XDMQ') -> List[str]: 

128 """ 

129 Extracts the single cell and returns the lines of the written SPICE netlist with these prefixes 

130 (by default not the capacitors and resistors KPEX adds), with their continuation lines joined 

131 """ 

132 _, csv_path, _ = self.run_rcx25d_single_cell(*path_components) 

133 with open(f"{os.path.splitext(csv_path)[0]}.spice") as f: 

134 lines = f.read().replace('\n+', '').splitlines() 

135 return [l for l in lines if l and l[0] in prefixes] 

136 

137 def assert_expected_matches_obtained(self, 

138 *path_components, 

139 expected_csv_content: str) -> CellExtractionResults: 

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

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

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

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

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

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

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

147 human_diff = csv_diff.human_text( 

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

149 ) 

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

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

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

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

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

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

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

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

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

159 assert human_diff == '', 'Diff detected' 

160 return result