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
« 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
28from klayout_pex.rcx25.extraction_results import *
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)
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)
46@allure.parent_suite("Unit Tests")
47class CellExtractionResultsTest(unittest.TestCase):
48 def test_summarize_overlap(self):
49 results = CellExtractionResults(cell_name='Cell')
51 ovk1a = OverlapKey(layer_top='m2',
52 net_top='net_top',
53 layer_bot='m1',
54 net_bot='net_bot')
56 ovk1b = OverlapKey(layer_top='m2',
57 net_top='net_top',
58 layer_bot='m1',
59 net_bot='net_bot')
61 ovk2 = OverlapKey(layer_top='m3',
62 net_top='net_top',
63 layer_bot='m1',
64 net_bot='net_bot')
66 ovc1a = OverlapCap(key=ovk1a,
67 cap_value=10.0,
68 shielded_area=20.0,
69 unshielded_area=30.0,
70 tech_spec=None)
72 ovc1b = OverlapCap(key=ovk1b,
73 cap_value=11.0,
74 shielded_area=21.0,
75 unshielded_area=31.0,
76 tech_spec=None)
78 ovc2 = OverlapCap(key=ovk2,
79 cap_value=12.0,
80 shielded_area=22.0,
81 unshielded_area=32.0,
82 tech_spec=None)
84 results.add_overlap_cap(ovc1a)
85 results.add_overlap_cap(ovc2)
86 results.add_overlap_cap(ovc1b)
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)
93 def test_summarize_sidewall(self):
94 results = CellExtractionResults(cell_name='Cell')
96 k1a = SidewallKey(layer='m2',
97 net1='net1',
98 net2='net2')
100 k1b = SidewallKey(layer='m2',
101 net1='net2',
102 net2='net1')
104 k2 = SidewallKey(layer='m3',
105 net1='net1',
106 net2='net3')
108 c1a = SidewallCap(key=k1a,
109 cap_value=10.0,
110 distance=20.0,
111 length=30.0,
112 tech_spec=None)
114 c1b = SidewallCap(key=k1b,
115 cap_value=11.0,
116 distance=21.0,
117 length=31.0,
118 tech_spec=None)
120 c2 = SidewallCap(key=k2,
121 cap_value=12.0,
122 distance=22.0,
123 length=32.0,
124 tech_spec=None)
126 results.add_sidewall_cap(c1a)
127 results.add_sidewall_cap(c1b)
128 results.add_sidewall_cap(c2)
130 summary = results.summarize()
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)
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)
140 def test_summarize_sideoverlap(self):
141 results = CellExtractionResults(cell_name='Cell')
143 k1a = SideOverlapKey(layer_inside='m2',
144 net_inside='net2',
145 layer_outside='m1',
146 net_outside='net1')
148 k1b = SideOverlapKey(layer_inside='m1',
149 net_inside='net1',
150 layer_outside='m2',
151 net_outside='net2')
153 k2 = SideOverlapKey(layer_inside='m3',
154 net_inside='net3',
155 layer_outside='m1',
156 net_outside='net1')
158 c1a = SideOverlapCap(key=k1a,
159 cap_value=10.0)
161 c1b = SideOverlapCap(key=k1b,
162 cap_value=10.0)
164 c2 = SideOverlapCap(key=k2,
165 cap_value=10.0)
167 results.add_sideoverlap_cap(c1a)
168 results.add_sideoverlap_cap(c1b)
169 results.add_sideoverlap_cap(c2)
171 summary = results.summarize()
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)
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)
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
199 summary = results.summarize()
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')])
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
225 summary = results.summarize()
227 self.assertEqual({NetCoupleKey('Y', 'Y.$0.3'): 5.0}, summary.resistances)
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
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)])
260 summary = results.summarize()
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)
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)])
277 summary = results.summarize()
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)
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)])
292 summary = results.summarize()
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)
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)])
309 summary = results.summarize()
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)
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)])
331 summary = results.summarize()
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)
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)])
347 summary = results.summarize()
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)
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)])
370 summary = results.summarize()
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)
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))
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)