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
« 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#
25import allure
26import glob
27import os
28import pytest
29from unittest import mock
31from rcx25_test_helpers import *
34CSVPath = str
35PNGPath = str
36parent_suite = "kpex/2.5D Extraction Tests [PDK sky130A | mode R]"
37tags = ("PEX", "2.5D", "MAGIC")
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)
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 )
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 )
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 )
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 )
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 )
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 )
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
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 )
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 ]
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] == []
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
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 ]
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 ]
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
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'}
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'}]