Coverage for tests/rcx25/rcx25_R_gf180mcuD_test.py: 100%
71 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-2026 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#
25import allure
26import csv
27import pytest
29from rcx25_test_helpers import *
31parent_suite = "kpex/2.5D Extraction Tests [PDK gf180mcuD | mode R]"
32tags = ("PEX", "2.5D")
35pex_whiteboxed = RCX25Extraction(pdk=PDKTestConfig(PDKName.GF180MCUD), pex_mode=PEXMode.R, blackbox=False)
38@allure.parent_suite(parent_suite)
39@allure.tag(*tags)
40@pytest.mark.slow
41def test_single_wire_m1():
42 # Metal1: 90 mΩ/sq, (9.885 - 0.115) µm / 0.23 µm = 42.5 sq
43 pex_whiteboxed.assert_expected_matches_obtained(
44 'test_patterns', 'r_single_wire_m1.gds.gz',
45 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω]
46R1;A;B;;3.823"""
47 )
50@allure.parent_suite(parent_suite)
51@allure.tag(*tags)
52@pytest.mark.slow
53def test_wire_voltage_divider_m1():
54 pex_whiteboxed.assert_expected_matches_obtained(
55 'test_patterns', 'r_wire_voltage_divider_m1.gds.gz',
56 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω]
57R1;A;A,B,C;;1.912
58R2;A,B,C;B;;1.912
59R3;A,B,C;C;;0.346"""
60 )
63@allure.parent_suite(parent_suite)
64@allure.tag(*tags)
65@pytest.mark.slow
66def test_contact_1x1_minsize_via1():
67 # Via1: 4500 mΩ per cut
68 pex_whiteboxed.assert_expected_matches_obtained(
69 'test_patterns', 'r_contact_1x1_minsize_via1.gds.gz',
70 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω]
71R1;BOT;TOP;;4.5"""
72 )
75@allure.parent_suite(parent_suite)
76@allure.tag(*tags)
77@pytest.mark.slow
78def test_nfet_m1():
79 # contacts: 6300 mΩ (N+), 5900 mΩ (poly) per cut, Poly2: 7300 mΩ/sq
80 pex_whiteboxed.assert_expected_matches_obtained(
81 'test_patterns', 'nfet_m1.gds.gz',
82 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω]
83R1;D;D.$1.Metal1;;0.208
84R2;D.$1.Metal1;D.P0.Nplus;;6.3
85R3;G;G.$1.Metal1;;0.156
86R4;G.$0.Poly2;G.$1.Metal1;;5.9
87R5;G.$0.Poly2;G.P0.Poly2;;12.514
88R6;S;S.$1.Metal1;;0.208
89R7;S.$1.Metal1;S.P0.Nplus;;6.3"""
90 )
93@allure.parent_suite(parent_suite)
94@allure.tag(*tags)
95@pytest.mark.slow
96def test_nwell_pfet_ntap_m1():
97 # The bulk of the PMOS is on the nwell, which the contact over the ntap at its far end joins to VDD.
98 # The extraction left out both, so the bulk was on VDD, without resistance:
99 # - contact over ntap: 6300 mΩ per cut (M1-N+), 1 cut
100 # - Nwell: 1000 Ω/sq, from the contact to the middle of the gate (9.25 µm) along the 3 µm wide nwell, 3.083 sq
101 pex_whiteboxed.assert_expected_matches_obtained(
102 'test_patterns', 'r_nwell_pfet_ntap_m1.gds.gz',
103 expected_csv_content="""Device;Net1;Net2;Capacitance [fF];Resistance [Ω]
104R1;D;D.P0.Pplus;;5.2
105R2;G;G.P0.Poly2;;31.633
106R3;S;S.P0.Pplus;;5.2
107R4;VDD;VDD.$0.Nwell;;6.3
108R5;VDD.$0.Nwell;VDD.P0.Nwell;;3083.333"""
109 )
112@allure.parent_suite(parent_suite)
113@allure.tag(*tags)
114@pytest.mark.slow
115def test_nfet_10v0_asym_pair():
116 # 2 mirrored LDMOS sharing their drain (the MVSD side), each with its own source:
117 # the asymmetric device must be extracted with S and D in place, not swapped
118 cell_name = 'nfet_10v0_asym_pair'
119 _, csv_path, _ = pex_whiteboxed.run_rcx25d_single_cell('test_patterns', f"{cell_name}.gds.gz")
120 lvsdb = kdb.LayoutVsSchematic()
121 lvsdb.read(os.path.join(os.path.dirname(csv_path), f"{cell_name}.lvsdb.gz"))
122 devices = list(lvsdb.netlist().circuit_by_name(cell_name).each_device())
123 assert [d.device_class().name for d in devices] == ['nfet_10v0_asym'] * 2
124 drains = {d.net_for_terminal('D').expanded_name() for d in devices}
125 sources = {d.net_for_terminal('S').expanded_name() for d in devices}
126 assert len(drains) == 1
127 assert len(sources) == 2
130def obtained_resistances(*path_components) -> List[float]:
131 """
132 The resistances, sorted (for patterns with many internal nodes, whose names are not stable)
133 """
134 _, csv_path, _ = pex_whiteboxed.run_rcx25d_single_cell(*path_components)
135 with open(csv_path) as f:
136 return sorted(float(row['Resistance [Ω]']) for row in csv.DictReader(f, delimiter=';')
137 if row['Resistance [Ω]'])
140@allure.parent_suite(parent_suite)
141@allure.tag(*tags)
142@pytest.mark.slow
143def test_contact_2x2_minsize_via1():
144 # 4 cuts in parallel, each 4500 mΩ (2 cuts each join the same nodes, so they are one resistor)
145 assert obtained_resistances('test_patterns', 'r_contact_2x2_minsize_via1.gds.gz') == \
146 [0.026] * 4 + [2.25] * 2
149@allure.parent_suite(parent_suite)
150@allure.tag(*tags)
151@pytest.mark.slow
152def test_via_stack_1x1_minsize_poly_to_metal5():
153 # poly contact 5900 mΩ, Via1-Via4 4500 mΩ per cut
154 assert obtained_resistances('test_patterns', 'r_via_stack_1x1_minsize_poly_to_metal5.gds.gz') == \
155 [4.5] * 4 + [5.9]
158@allure.parent_suite(parent_suite)
159@allure.tag(*tags)
160@pytest.mark.slow
161def test_mim_cap__whiteboxed():
162 # The white-box extraction keeps the MIM cap layers, so the top plate (FuseTop) is a conductor of its net,
163 # joined to Metal5 by the vias on it (top_via_cap). It stopped with "unhandled layer purpose PURPOSE_MIM_CAP"
164 results, _, _ = pex_whiteboxed.run_rcx25d_single_cell('test_patterns', 'cap_mim_m4m5.gds.gz')
165 networks = {n.net_name: n for n in results.r_extraction_result.networks}
166 for top, bottom in (('TOP1', 'BOT1'), ('TOP2', 'BOT2')):
167 assert {node.layer_name for node in networks[top].nodes} == {'FuseTop', 'Metal5'}
168 assert {node.layer_name for node in networks[bottom].nodes} == {'Metal4'}
170 layer_by_node_id = {node.node_id: node.layer_name for node in networks[top].nodes}
171 assert [e for e in networks[top].elements
172 if {layer_by_node_id[e.node_a.node_id], layer_by_node_id[e.node_b.node_id]} == {'FuseTop', 'Metal5'}]