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

175 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/iic-jku/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# 

24import allure 

25import pytest 

26import unittest 

27 

28from klayout_pex.rcx25.extraction_results import * 

29 

30 

31@allure.parent_suite("Unit Tests") 

32class NetCoupleKeyTest(unittest.TestCase): 

33 def test_normed_ascending(self): 

34 k = NetCoupleKey('net_bottom', 'net_top') 

35 obtained_k = k.normed() 

36 expected_k = NetCoupleKey('net_bottom', 'net_top') 

37 self.assertEqual(expected_k, obtained_k) 

38 

39 def test_normed_descending(self): 

40 k = NetCoupleKey('net_top', 'net_bottom') 

41 obtained_k = k.normed() 

42 expected_k = NetCoupleKey('net_bottom', 'net_top') 

43 self.assertEqual(expected_k, obtained_k) 

44 

45 

46@allure.parent_suite("Unit Tests") 

47class CellExtractionResultsTest(unittest.TestCase): 

48 def test_summarize_overlap(self): 

49 results = CellExtractionResults(cell_name='Cell') 

50 

51 ovk1a = OverlapKey(layer_top='m2', 

52 net_top='net_top', 

53 layer_bot='m1', 

54 net_bot='net_bot') 

55 

56 ovk1b = OverlapKey(layer_top='m2', 

57 net_top='net_top', 

58 layer_bot='m1', 

59 net_bot='net_bot') 

60 

61 ovk2 = OverlapKey(layer_top='m3', 

62 net_top='net_top', 

63 layer_bot='m1', 

64 net_bot='net_bot') 

65 

66 ovc1a = OverlapCap(key=ovk1a, 

67 cap_value=10.0, 

68 shielded_area=20.0, 

69 unshielded_area=30.0, 

70 tech_spec=None) 

71 

72 ovc1b = OverlapCap(key=ovk1b, 

73 cap_value=11.0, 

74 shielded_area=21.0, 

75 unshielded_area=31.0, 

76 tech_spec=None) 

77 

78 ovc2 = OverlapCap(key=ovk2, 

79 cap_value=12.0, 

80 shielded_area=22.0, 

81 unshielded_area=32.0, 

82 tech_spec=None) 

83 

84 results.add_overlap_cap(ovc1a) 

85 results.add_overlap_cap(ovc2) 

86 results.add_overlap_cap(ovc1b) 

87 

88 summary = results.summarize() 

89 obtained_cap_value = summary.capacitances[NetCoupleKey('net_top', 'net_bot').normed()] 

90 expected_cap_value = ovc1a.cap_value + ovc1b.cap_value + ovc2.cap_value 

91 self.assertEqual(expected_cap_value, obtained_cap_value) 

92 

93 def test_summarize_sidewall(self): 

94 results = CellExtractionResults(cell_name='Cell') 

95 

96 k1a = SidewallKey(layer='m2', 

97 net1='net1', 

98 net2='net2') 

99 

100 k1b = SidewallKey(layer='m2', 

101 net1='net2', 

102 net2='net1') 

103 

104 k2 = SidewallKey(layer='m3', 

105 net1='net1', 

106 net2='net3') 

107 

108 c1a = SidewallCap(key=k1a, 

109 cap_value=10.0, 

110 distance=20.0, 

111 length=30.0, 

112 tech_spec=None) 

113 

114 c1b = SidewallCap(key=k1b, 

115 cap_value=11.0, 

116 distance=21.0, 

117 length=31.0, 

118 tech_spec=None) 

119 

120 c2 = SidewallCap(key=k2, 

121 cap_value=12.0, 

122 distance=22.0, 

123 length=32.0, 

124 tech_spec=None) 

125 

126 results.add_sidewall_cap(c1a) 

127 results.add_sidewall_cap(c1b) 

128 results.add_sidewall_cap(c2) 

129 

130 summary = results.summarize() 

131 

132 obtained_cap_value = summary.capacitances[NetCoupleKey('net1', 'net2').normed()] 

133 expected_cap_value = c1a.cap_value + c1b.cap_value 

134 self.assertEqual(expected_cap_value, obtained_cap_value) 

135 

136 obtained_cap_value = summary.capacitances[NetCoupleKey('net1', 'net3').normed()] 

137 expected_cap_value = c2.cap_value 

138 self.assertEqual(expected_cap_value, obtained_cap_value) 

139 

140 def test_summarize_sideoverlap(self): 

141 results = CellExtractionResults(cell_name='Cell') 

142 

143 k1a = SideOverlapKey(layer_inside='m2', 

144 net_inside='net2', 

145 layer_outside='m1', 

146 net_outside='net1') 

147 

148 k1b = SideOverlapKey(layer_inside='m1', 

149 net_inside='net1', 

150 layer_outside='m2', 

151 net_outside='net2') 

152 

153 k2 = SideOverlapKey(layer_inside='m3', 

154 net_inside='net3', 

155 layer_outside='m1', 

156 net_outside='net1') 

157 

158 c1a = SideOverlapCap(key=k1a, 

159 cap_value=10.0) 

160 

161 c1b = SideOverlapCap(key=k1b, 

162 cap_value=10.0) 

163 

164 c2 = SideOverlapCap(key=k2, 

165 cap_value=10.0) 

166 

167 results.add_sideoverlap_cap(c1a) 

168 results.add_sideoverlap_cap(c1b) 

169 results.add_sideoverlap_cap(c2) 

170 

171 summary = results.summarize() 

172 

173 obtained_cap_value = summary.capacitances[NetCoupleKey('net1', 'net2').normed()] 

174 expected_cap_value = c1a.cap_value + c1b.cap_value 

175 self.assertEqual(expected_cap_value, obtained_cap_value) 

176 

177 obtained_cap_value = summary.capacitances[NetCoupleKey('net1', 'net3').normed()] 

178 expected_cap_value = c2.cap_value 

179 self.assertEqual(expected_cap_value, obtained_cap_value) 

180 

181 def test_summarize_resistances_between_the_same_nodes_are_in_parallel(self): 

182 # a wire with pin A at both ends, and pin B at a stub in the middle: 

183 # both pins A are one node in the netlist, so each half of the wire is in parallel to the other 

184 results = CellExtractionResults(cell_name='Cell') 

185 network = results.r_extraction_result.networks.add() 

186 network.net_name = 'A,B' 

187 for node_id, kind, node_name in ((1, r_network_pb2.RNode.Kind.KIND_PIN, 'A'), 

188 (2, r_network_pb2.RNode.Kind.KIND_PIN, 'A'), 

189 (3, r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION, '$1.16'), 

190 (4, r_network_pb2.RNode.Kind.KIND_PIN, 'B')): 

191 node = network.nodes.add(node_id=node_id, node_kind=kind, node_name=node_name) 

192 if kind == r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION: 

193 node.net_name = f"{network.net_name}.{node_name}" 

194 for node_a, node_b, resistance in ((1, 3, 426.667), (2, 3, 413.867), (3, 4, 72.533)): 

195 element = network.elements.add(resistance=resistance) 

196 element.node_a.node_id = node_a 

197 element.node_b.node_id = node_b 

198 

199 summary = results.summarize() 

200 

201 # NOTE: no pin carries the net's name A,B, so the wire junction does 

202 self.assertEqual({NetCoupleKey('A', 'A,B'), NetCoupleKey('A,B', 'B')}, 

203 set(summary.resistances.keys())) 

204 self.assertAlmostEqual(426.667 * 413.867 / (426.667 + 413.867), 

205 summary.resistances[NetCoupleKey('A', 'A,B')]) 

206 self.assertAlmostEqual(72.533, summary.resistances[NetCoupleKey('A,B', 'B')]) 

207 

208 def test_summarize_skips_resistances_between_pins_with_the_same_label(self): 

209 # e.g. output Y of sky130_fd_sc_hd__inv_1, which has two labels: 

210 # both pins Y are one node in the netlist, so the element between them is shorted 

211 results = CellExtractionResults(cell_name='Cell') 

212 network = results.r_extraction_result.networks.add() 

213 network.net_name = 'Y' 

214 for node_id, kind, node_name in ((1, r_network_pb2.RNode.Kind.KIND_PIN, 'Y'), 

215 (2, r_network_pb2.RNode.Kind.KIND_PIN, 'Y'), 

216 (3, r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL, '$0.3')): 

217 node = network.nodes.add(node_id=node_id, node_kind=kind, node_name=node_name) 

218 if kind == r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL: 

219 node.net_name = f"{network.net_name}.{node_name}" 

220 for node_a, node_b, resistance in ((1, 2, 18.981), (2, 3, 5.0)): 

221 element = network.elements.add(resistance=resistance) 

222 element.node_a.node_id = node_a 

223 element.node_b.node_id = node_b 

224 

225 summary = results.summarize() 

226 

227 self.assertEqual({NetCoupleKey('Y', 'Y.$0.3'): 5.0}, summary.resistances) 

228 

229 @staticmethod 

230 def add_network(results: CellExtractionResults, 

231 net_name: str, 

232 nodes: List[Tuple], 

233 elements: List[Tuple[int, int, float]]): 

234 """ 

235 :param nodes: (node ID, kind, node name[, (device ID, terminal ID)]) 

236 """ 

237 network = results.r_extraction_result.networks.add(net_name=net_name) 

238 for node_id, kind, node_name, *terminal in nodes: 

239 node = network.nodes.add(node_id=node_id, node_kind=kind, node_name=node_name) 

240 if kind != r_network_pb2.RNode.Kind.KIND_PIN: 

241 node.net_name = f"{net_name}.{node_name}" 

242 if terminal: 

243 node.device_terminal.device_id, node.device_terminal.terminal_id = terminal[0] 

244 for node_a, node_b, resistance in elements: 

245 element = network.elements.add(resistance=resistance) 

246 element.node_a.node_id = node_a 

247 element.node_b.node_id = node_b 

248 

249 def test_summarize_device_terminals_and_the_node_carrying_the_net_name(self): 

250 # a net without a pin: device 3 and device 5 (with 2 ports for one terminal) on a wire 

251 K = r_network_pb2.RNode.Kind 

252 results = CellExtractionResults(cell_name='Cell') 

253 self.add_network(results, '$2', 

254 nodes=[(1, K.KIND_DEVICE_TERMINAL, 'P0.12', (5, 2)), 

255 (2, K.KIND_WIRE_JUNCTION, '$0.12'), 

256 (3, K.KIND_DEVICE_TERMINAL, 'P0.13', (3, 0)), 

257 (4, K.KIND_DEVICE_TERMINAL, 'P1.12', (5, 2))], 

258 elements=[(1, 2, 10.0), (2, 3, 20.0), (4, 2, 30.0)]) 

259 

260 summary = results.summarize() 

261 

262 # NOTE: the first device terminal carries the net's name, 

263 # and the ports of one terminal are one node (so their elements are in parallel) 

264 self.assertEqual({DeviceTerminalKey(3, 0): '$2', 

265 DeviceTerminalKey(5, 2): '$2.P0.12'}, summary.device_terminal_nodes) 

266 self.assertEqual({NetCoupleKey('$2', '$2.$0.12'): 20.0, 

267 NetCoupleKey('$2.$0.12', '$2.P0.12'): 7.5}, summary.resistances) 

268 

269 def test_summarize_a_pin_carries_the_net_name(self): 

270 K = r_network_pb2.RNode.Kind 

271 results = CellExtractionResults(cell_name='Cell') 

272 self.add_network(results, 'G', 

273 nodes=[(1, K.KIND_PIN, 'G'), 

274 (2, K.KIND_DEVICE_TERMINAL, 'P0.16', (1, 1))], 

275 elements=[(1, 2, 100.0)]) 

276 

277 summary = results.summarize() 

278 

279 self.assertEqual({DeviceTerminalKey(1, 1): 'G.P0.16'}, summary.device_terminal_nodes) 

280 self.assertEqual({NetCoupleKey('G', 'G.P0.16'): 100.0}, summary.resistances) 

281 

282 def test_summarize_the_pins_of_a_net_of_one_label_are_its_node(self): 

283 # NOTE: another net has the label D too, so this one is called D$1 (#211 §10) 

284 K = r_network_pb2.RNode.Kind 

285 results = CellExtractionResults(cell_name='Cell') 

286 self.add_network(results, 'D$1', 

287 nodes=[(1, K.KIND_PIN, 'D'), 

288 (2, K.KIND_WIRE_JUNCTION, '$1.Metal1'), 

289 (3, K.KIND_DEVICE_TERMINAL, 'P0.nSD', (1, 2))], 

290 elements=[(1, 2, 0.488), (2, 3, 17.0)]) 

291 

292 summary = results.summarize() 

293 

294 self.assertEqual({DeviceTerminalKey(1, 2): 'D$1.P0.nSD'}, summary.device_terminal_nodes) 

295 self.assertEqual({NetCoupleKey('D$1', 'D$1.$1.Metal1'): 0.488, 

296 NetCoupleKey('D$1.$1.Metal1', 'D$1.P0.nSD'): 17.0}, summary.resistances) 

297 

298 def test_summarize_a_net_of_several_labels_has_a_port_per_label(self): 

299 # a drain wire with the labels D and X at its ends (#211 §11) 

300 K = r_network_pb2.RNode.Kind 

301 results = CellExtractionResults(cell_name='Cell') 

302 self.add_network(results, 'D,X', 

303 nodes=[(1, K.KIND_PIN, 'D'), 

304 (2, K.KIND_WIRE_JUNCTION, '$1.Metal1'), 

305 (3, K.KIND_PIN, 'X'), 

306 (4, K.KIND_DEVICE_TERMINAL, 'P0.nSD', (1, 2))], 

307 elements=[(1, 2, 0.488), (2, 3, 0.089), (2, 4, 17.0)]) 

308 

309 summary = results.summarize() 

310 

311 self.assertEqual({'D,X': ['D', 'X']}, summary.label_ports) 

312 # NOTE: no pin carries the net's name, so the device terminal does 

313 self.assertEqual({DeviceTerminalKey(1, 2): 'D,X'}, summary.device_terminal_nodes) 

314 self.assertEqual({NetCoupleKey('D', 'D,X.$1.Metal1'): 0.488, 

315 NetCoupleKey('D,X.$1.Metal1', 'X'): 0.089, 

316 NetCoupleKey('D,X', 'D,X.$1.Metal1'): 17.0}, summary.resistances) 

317 

318 def test_summarize_the_port_of_a_label_has_a_unique_name(self): 

319 # the label D is the name of another net too 

320 K = r_network_pb2.RNode.Kind 

321 results = CellExtractionResults(cell_name='Cell') 

322 self.add_network(results, 'D', 

323 nodes=[(1, K.KIND_PIN, 'D'), 

324 (2, K.KIND_WIRE_JUNCTION, '$1.Metal1')], 

325 elements=[(1, 2, 0.5)]) 

326 self.add_network(results, 'D,X', 

327 nodes=[(1, K.KIND_PIN, 'D'), 

328 (2, K.KIND_PIN, 'X')], 

329 elements=[(1, 2, 0.6)]) 

330 

331 summary = results.summarize() 

332 

333 self.assertEqual({'D,X': ['D$1', 'X']}, summary.label_ports) 

334 self.assertEqual({NetCoupleKey('D', 'D.$1.Metal1'): 0.5, 

335 NetCoupleKey('D$1', 'X'): 0.6}, summary.resistances) 

336 

337 def test_summarize_labels_on_one_node_are_tied(self): 

338 # NOTE: each label is a port, so the port of X is tied to the node of D (#211 §11) 

339 K = r_network_pb2.RNode.Kind 

340 results = CellExtractionResults(cell_name='Cell') 

341 self.add_network(results, 'D,X', 

342 nodes=[(1, K.KIND_PIN, 'D'), 

343 (2, K.KIND_PIN, 'X'), 

344 (3, K.KIND_WIRE_JUNCTION, '$1.Metal1')], 

345 elements=[(1, 2, 0.0), (2, 3, 0.488)]) 

346 

347 summary = results.summarize() 

348 

349 self.assertEqual({'D,X': ['D', 'X']}, summary.label_ports) 

350 self.assertEqual({NetCoupleKey('D', 'D,X'): 0.488, 

351 NetCoupleKey('D', 'X'): LABEL_TIE_RESISTANCE}, summary.resistances) 

352 

353 def test_summarize_nodes_joined_without_resistance_are_one_node(self): 

354 # NOTE: a simulator can't solve for a resistor of 0 Ω, so no such resistor must be written 

355 K = r_network_pb2.RNode.Kind 

356 results = CellExtractionResults(cell_name='Cell') 

357 self.add_network(results, '$2', 

358 nodes=[(1, K.KIND_DEVICE_TERMINAL, 'P0.12', (1, 2)), 

359 (2, K.KIND_WIRE_JUNCTION, '$0.12'), 

360 (3, K.KIND_WIRE_JUNCTION, '$1.17'), 

361 (4, K.KIND_DEVICE_TERMINAL, 'P1.12', (2, 0)), 

362 (5, K.KIND_WIRE_JUNCTION, '$2.12')], 

363 elements=[(1, 2, 0.0), (2, 3, 209.667), (3, 5, 100.0), (5, 4, 0.0)]) 

364 self.add_network(results, 'VGND', 

365 nodes=[(1, K.KIND_PIN, 'VGND'), 

366 (2, K.KIND_WIRE_JUNCTION, '$5.26'), 

367 (3, K.KIND_WIRE_JUNCTION, '$4.18')], 

368 elements=[(1, 2, 0.0), (2, 3, 9.3)]) 

369 

370 summary = results.summarize() 

371 

372 # NOTE: a group of nodes is named after its pin, otherwise its device terminal's port, 

373 # but the first device terminal carries the net's name 

374 self.assertEqual({DeviceTerminalKey(1, 2): '$2', 

375 DeviceTerminalKey(2, 0): '$2.P1.12'}, summary.device_terminal_nodes) 

376 self.assertEqual({NetCoupleKey('$2', '$2.$1.17'): 209.667, 

377 NetCoupleKey('$2.$1.17', '$2.P1.12'): 100.0, 

378 NetCoupleKey('VGND', 'VGND.$4.18'): 9.3}, summary.resistances) 

379 

380 

381@allure.parent_suite("Unit Tests") 

382class ExtractionSummaryTest(unittest.TestCase): 

383 def test_parallel_resistance(self): 

384 self.assertAlmostEqual(50.0, parallel_resistance(100.0, 100.0)) 

385 self.assertAlmostEqual(75.0, parallel_resistance(100.0, 300.0)) 

386 self.assertEqual(0.0, parallel_resistance(0.0, 100.0)) 

387 self.assertEqual(0.0, parallel_resistance(100.0, 0.0)) 

388 

389 def test_merged_resistances_between_the_same_nodes_are_in_parallel(self): 

390 summary = ExtractionSummary.merged([ 

391 ExtractionSummary(capacitances={NetCoupleKey('A', 'B'): 1.0}, 

392 resistances={NetCoupleKey('A', 'B'): 100.0}), 

393 ExtractionSummary(capacitances={NetCoupleKey('B', 'A'): 2.0}, 

394 resistances={NetCoupleKey('B', 'A'): 300.0, 

395 NetCoupleKey('B', 'C'): 10.0}), 

396 ]) 

397 self.assertEqual({NetCoupleKey('A', 'B'): 3.0}, dict(summary.capacitances)) 

398 self.assertEqual({NetCoupleKey('A', 'B'): 75.0, 

399 NetCoupleKey('B', 'C'): 10.0}, summary.resistances)