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

112 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# 

24 

25import allure 

26import glob 

27import os 

28import pytest 

29from unittest import mock 

30 

31from rcx25_test_helpers import * 

32 

33 

34CSVPath = str 

35PNGPath = str 

36parent_suite = "kpex/2.5D Extraction Tests [PDK sky130A | mode R]" 

37tags = ("PEX", "2.5D", "MAGIC") 

38 

39 

40pex_whiteboxed = RCX25Extraction(pdk=PDKTestConfig(PDKName.SKY130A), pex_mode=PEXMode.R, blackbox=False) 

41pex_blackboxed = RCX25Extraction(pdk=PDKTestConfig(PDKName.SKY130A), pex_mode=PEXMode.R, blackbox=True) 

42 

43 

44@allure.parent_suite(parent_suite) 

45@allure.tag(*tags) 

46@pytest.mark.slow 

47def test_single_wire_li1(): 

48 # MAGIC GIVES (8.3 revision 540): 

49 #_______________________________ NOTE: with halo=8µm __________________________________ 

50 # R0 A B 840.534 

51 # R1 B A 840.534 # reported twice! 

52 pex_whiteboxed.assert_expected_matches_obtained( 

53 'test_patterns', 'r_single_wire_li1.gds.gz', 

54 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

55R1;A;B;;840.533""" 

56 ) 

57 

58 

59@allure.parent_suite(parent_suite) 

60@allure.tag(*tags) 

61@pytest.mark.slow 

62def test_contact_1x1_minsize_mcon(): 

63 # MAGIC GIVES (8.3 revision 540): 

64 #_______________________________ NOTE: with halo=8µm __________________________________ 

65 # R0 TOP BOT 15.763 (why not 9? Magic takes the bottom-left of each port) 

66 # but in the debug version we see 9.3 is calculated for the via 

67 pex_whiteboxed.assert_expected_matches_obtained( 

68 'test_patterns', 'r_contact_1x1_minsize_mcon.gds.gz', 

69 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

70R1;BOT;TOP;;9.3""" 

71 ) 

72 

73 

74@allure.parent_suite(parent_suite) 

75@allure.tag(*tags) 

76@pytest.mark.slow 

77def test_wire_voltage_divider_li1(): 

78 # MAGIC GIVES (8.3 revision 540): 

79 #_______________________________ NOTE: with halo=8µm __________________________________ 

80 # R0 A B 840.534 

81 # R1 B A 840.534 # reported twice! 

82 pex_whiteboxed.assert_expected_matches_obtained( 

83 'test_patterns', 'r_wire_voltage_divider_li1.gds.gz', 

84 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

85R1;A;A,B,C;;426.667 

86R2;A,B,C;B;;413.867 

87R3;A,B,C;C;;72.533""" 

88 ) 

89 

90 

91@allure.parent_suite(parent_suite) 

92@allure.tag(*tags) 

93@pytest.mark.slow 

94def test_wire_same_label_at_both_ends_li1(): 

95 # Same layout as r_wire_voltage_divider_li1, but pin A is at both ends of the wire 

96 # (and B at the stub in the middle). Both pins A are one node in the netlist, 

97 # so the two halves of the wire (426.667 Ω, 413.867 Ω) are in parallel, not in series (#211) 

98 pex_whiteboxed.assert_expected_matches_obtained( 

99 'test_patterns', 'r_wire_same_label_at_both_ends_li1.gds.gz', 

100 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

101R1;A;A,B;;210.085 

102R2;A,B;B;;72.533""" 

103 ) 

104 

105@allure.parent_suite(parent_suite) 

106@allure.tag(*tags) 

107@pytest.mark.slow 

108def test_via_stack_1x1_minsize_poly_to_met5(): 

109 # MAGIC GIVES (8.3 revision 540): 

110 #_______________________________ NOTE: with halo=8µm __________________________________ 

111 # R0 li1.n3 poly 175.37 # poly contact, should be 152Ω 

112 # R1 li1.n4 li1.n3 9.3005 # mcon via, should be 9.3Ω 

113 # R2 li1.n3 li1 6.4005 

114 # R3 li1.n4 li1.n2 4.5005 # via, should be 4.5Ω 

115 # R7 met1 li1.n4 0.063 

116 # R4 li1.n1 li1.n0 3.4105 # via2, should be 3.41Ω 

117 # R8 li1.n2 met2 0.0545541 

118 # R6 li1.n0 met5 0.3834 # via4, should be 0.38Ω 

119 # R9 li1.n1 met3 0.0232879 

120 # R10 li1.n0 met4 0.00687288 

121 # R5 li1.n2 li1.n1 3.4105 # via3, should be 3.41Ω 

122 # (and some redundant listings of the same) 

123 pex_whiteboxed.assert_expected_matches_obtained( 

124 'test_patterns', 'r_via_stack_1x1_minsize_poly_to_met5.gds.gz', 

125 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

126R1;li1;met1;;9.3 

127R2;li1;poly;;152.0 

128R3;met1;met2;;4.5 

129R4;met2;met3;;3.41 

130R5;met3;met4;;3.41 

131R6;met4;met5;;0.38""" 

132 ) 

133 

134 

135@allure.parent_suite(parent_suite) 

136@allure.tag(*tags) 

137@pytest.mark.slow 

138def test_nfet_li1_redux(): 

139 # MAGIC GIVES (8.3 revision 540): 

140 #_______________________________ NOTE: with halo=8µm __________________________________ 

141 pex_whiteboxed.assert_expected_matches_obtained( 

142 'test_patterns', 'nfet_li1_redux.gds.gz', 

143 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

144R1;$2;$2.$1.li1;;209.667 

145R2;$3;$3.$1.li1;;209.667 

146R3;G;G.$1.li1;;2.418 

147R4;G.$0.poly;G.$1.li1;;152.0 

148R5;G.$0.poly;G.P0.poly;;316.321""" 

149 ) 

150 

151 

152 

153@allure.parent_suite(parent_suite) 

154@allure.tag(*tags) 

155@pytest.mark.slow 

156def test_nfet_terminals_are_on_the_resistor_network(): 

157 # The transistor is connected to the nodes of its terminals (#211 §6): the gate to the end of the poly, 

158 # drain and source to the nodes that carry the names of their nets (which have no pin) 

159 pex_whiteboxed.run_rcx25d_single_cell('test_patterns', 'nfet_li1_redux.gds.gz') 

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

161 netlist_path, = glob.glob(os.path.join(output_dir_path, 'nfet_li1_redux__*', '*_k25d_pex_netlist.spice')) 

162 with open(netlist_path) as f: 

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

164 nfet_line, = [l for l in lines if l.startswith('X$1 ')] 

165 assert nfet_line.split()[1:5] == ['\\$3', 'G.P0.poly', '\\$2', 'sky130_gnd'] # D G S B 

166 

167 

168@allure.parent_suite(parent_suite) 

169@allure.tag(*tags) 

170@pytest.mark.slow 

171def test_nwell_pfet_ntap_li1(): 

172 # The bulk of the PMOS is on the nwell, which the licon over the ntap at its far end joins to VPWR. 

173 # The extraction left out both, so the bulk was on VPWR, without resistance: 

174 # - licon over ntap: 185 Ω per cut, 1 cut 

175 # - nwell: 1700 Ω/sq, from the licon to the middle of the gate (9.25 µm) along the 3 µm wide nwell, 3.083 sq 

176 pex_whiteboxed.assert_expected_matches_obtained( 

177 'test_patterns', 'r_nwell_pfet_ntap_li1.gds.gz', 

178 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

179R1;D;D.P0.psdm;;585.0 

180R2;G;G.P0.poly;;363.107 

181R3;S;S.P0.psdm;;585.0 

182R4;VPWR;VPWR.$0.nwell;;185.0 

183R5;VPWR.$0.nwell;VPWR.P0.nwell;;5241.667""" 

184 ) 

185 

186 

187@allure.parent_suite(parent_suite) 

188@allure.tag(*tags) 

189@pytest.mark.slow 

190def test_nwell_joined_by_a_label_only(): 

191 # Like r_nwell_pfet_ntap_li1, without the licon over the ntap, but with the label VPWR on the nwell, 

192 # which joins it to VPWR. So the nwell is a piece of the resistance network that joins nothing else, 

193 # and the extraction leaves it out, so that the bulk of the PMOS is on VPWR, without resistance 

194 with mock.patch('klayout_pex.rcx25.r.r_extractor.warning') as warning_mock: 

195 pex_whiteboxed.assert_expected_matches_obtained( 

196 'test_patterns', 'r_nwell_pfet_label_li1.gds.gz', 

197 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

198R1;D;D.P0.psdm;;585.0 

199R2;G;G.P0.poly;;363.107 

200R3;S;S.P0.psdm;;585.0""" 

201 ) 

202 assert [c.args[0] for c in warning_mock.call_args_list] == [ 

203 "The resistance network leaves out the wells of these nets, as no contact joins them " 

204 "to the rest of their net (e.g. a label of the net on the well does), " 

205 "so the device terminals on them (e.g. the PMOS bulks) are ideally on their nets: VPWR" 

206 ] 

207 

208 

209@allure.parent_suite(parent_suite) 

210@allure.tag(*tags) 

211@pytest.mark.slow 

212def test_no_resistor_between_pins_with_the_same_label(): 

213 # Output Y has two labels, which are one node in the netlist, so the resistance between them is shorted. 

214 # It used to be a resistor from Y to Y, with a warning "Invalid attempt to create resistor ... between same net" 

215 with mock.patch('klayout_pex.rcx25.netlist_expander.warning') as warning_mock: 

216 results, _, _ = pex_whiteboxed.run_rcx25d_single_cell('sky130_fd_sc_hd__inv_1', 'sky130_fd_sc_hd__inv_1.gds.gz') 

217 warning_mock.assert_not_called() 

218 assert [k for k in results.summarize().resistances.keys() if k.net1 == k.net2] == [] 

219 

220 

221@allure.parent_suite(parent_suite) 

222@allure.tag(*tags) 

223@pytest.mark.slow 

224def test_vias_within_mom_cap_l1m1m2(): 

225 # The vias within the MOM capacitor (mcon_vpp, via1_vpp) are on the GDS pairs of the regular vias, 

226 # so they are part of the resistance network (#228 reported them as unmodeled) 

227 results, _, _ = pex_whiteboxed.run_rcx25d_single_cell('cap_vpp_04p4x04p6_l1m1m2_noshield', 

228 'cap_vpp_04p4x04p6_l1m1m2_noshield.gds.gz') 

229 assert {n.net_name for n in results.r_extraction_result.networks} >= {'C0', 'C1'} 

230 assert results.summarize().resistances 

231 

232 

233@allure.parent_suite(parent_suite) 

234@allure.tag(*tags) 

235@pytest.mark.slow 

236def test_device_terminals_overlapping_no_conductor_of_their_net_are_reported_once(): 

237 # The substrate terminal B of the MOM capacitor (vpp_sub, the marker of the capacitor) is on an LVS layer 

238 # the tech info has no layer for, and it overlaps no conductor of its net, as the substrate is none (#217) 

239 with mock.patch('klayout_pex.klayout.lvsdb_extractor.warning') as warning_mock: 

240 pex_whiteboxed.run_rcx25d_single_cell('cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5', 

241 'cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5.gds.gz') 

242 messages = [c.args[0] for c in warning_mock.call_args_list if 'device terminals' in c.args[0]] 

243 assert messages == [ 

244 "The resistance network has no nodes for these device terminals, " 

245 "as the tech info has no layer for their LVS layer, and they overlap no conductor of their net " 

246 "(e.g. a terminal on the substrate or a well):\n" 

247 " - sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5 terminal B on LVS layer vpp_sub: $1" 

248 ] 

249 

250 

251@allure.parent_suite(parent_suite) 

252@allure.tag(*tags) 

253@pytest.mark.slow 

254def test_via_without_conductor_below_is_an_error(): 

255 # The via1 has no met1 below it, so it joins nothing there. The extraction report crashed on it with a KeyError, 

256 # as with gf180mcuD before #229, whose tech info put the layer below its vias on another GDS pair 

257 with mock.patch('klayout_pex.kpex_cli.error') as error_mock, pytest.raises(SystemExit): 

258 pex_whiteboxed.run_rcx25d_single_cell('test_patterns', 'r_via1_without_met1.gds.gz') 

259 assert [c.args[0] for c in error_mock.call_args_list if 'vias' in c.args[0]] == [ 

260 "These vias have no conductor to join on one side, " 

261 "so their connections would be missing from the resistance network:\n" 

262 " - via (LVS via1_con): the layer below it, met1 (68, 20), has no shapes" 

263 ] 

264 

265 

266@allure.parent_suite(parent_suite) 

267@allure.tag(*tags) 

268@pytest.mark.slow 

269def test_vias_within_mom_cap_l1m1m2m3m4(): 

270 # via3_vpp and via4_vpp must be on the GDS pairs of via3_ncap and via4_ncap 

271 results, _, _ = pex_whiteboxed.run_rcx25d_single_cell('cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5', 

272 'cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5.gds.gz') 

273 assert {n.net_name for n in results.r_extraction_result.networks} >= {'C0', 'C1'} 

274 assert results.summarize().resistances 

275 

276 

277@allure.parent_suite(parent_suite) 

278@allure.tag(*tags) 

279@pytest.mark.slow 

280def test_mim_cap__whiteboxed(): 

281 # The white-box extraction keeps the MIM cap layers, so the top plate (capm) is a conductor of its net, 

282 # joined to met4 by the vias on it (via3_cap). It stopped with "unhandled layer purpose PURPOSE_MIM_CAP" 

283 results, _, _ = pex_whiteboxed.run_rcx25d_single_cell('cap_mim_m3_w18p9_l5p1', 'cap_mim_m3_w18p9_l5p1.gds.gz') 

284 networks = {n.net_name: n for n in results.r_extraction_result.networks} 

285 assert {node.layer_name for node in networks['mimcap_top'].nodes} == {'capm', 'met3', 'met4'} 

286 assert {node.layer_name for node in networks['mimcap_bot'].nodes} == {'met3', 'met4'} 

287 

288 top_layer_by_node_id = {node.node_id: node.layer_name for node in networks['mimcap_top'].nodes} 

289 assert [e for e in networks['mimcap_top'].elements 

290 if {top_layer_by_node_id[e.node_a.node_id], top_layer_by_node_id[e.node_b.node_id]} == {'capm', 'met4'}]