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

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# 

24 

25import allure 

26import csv 

27import pytest 

28 

29from rcx25_test_helpers import * 

30 

31parent_suite = "kpex/2.5D Extraction Tests [PDK gf180mcuD | mode R]" 

32tags = ("PEX", "2.5D") 

33 

34 

35pex_whiteboxed = RCX25Extraction(pdk=PDKTestConfig(PDKName.GF180MCUD), pex_mode=PEXMode.R, blackbox=False) 

36 

37 

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 ) 

48 

49 

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 ) 

61 

62 

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 ) 

73 

74 

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 ) 

91 

92 

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 ) 

110 

111 

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 

128 

129 

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 [Ω]']) 

138 

139 

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 

147 

148 

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] 

156 

157 

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'} 

169 

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'}]