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

120 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/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# 

24 

25from __future__ import annotations 

26 

27import os 

28import tempfile 

29from typing import * 

30from unittest import mock 

31 

32import allure 

33import klayout.db as kdb 

34import pytest 

35 

36from klayout_pex.klayout.lvsdb_extractor import KLayoutExtractionContext 

37from klayout_pex.kpex_cli import KpexCLI 

38from klayout_pex.pdk_config import PDK 

39from klayout_pex.tech_info import TechInfo 

40 

41parent_suite = "kpex/2.5D Extraction Tests [PDK ihp-sg13g2 | mode R]" 

42tags = ("PEX", "2.5D", "RF MOS") 

43 

44TEST_DESIGNS_DIR = os.path.realpath(os.path.join(__file__, '..', '..', '..', 'testdata', 'designs', 'ihp-sg13g2')) 

45 

46 

47@allure.parent_suite(parent_suite) 

48@allure.tag(*tags) 

49@pytest.mark.slow 

50def test_rf_mos_keeps_its_layout_geometry(): 

51 # The IHP LVS script replaced each extracted RF MOS by a newly created device 

52 # of the base class (sg13_lv_nmos with rfmode=1), which has no layout geometry, 

53 # so the resistance extraction could not connect it (#211) 

54 cell_name = 'rfnmos_w1u_l0u72' 

55 gds_path = os.path.join(TEST_DESIGNS_DIR, 'rfnmos', f"{cell_name}.gds.gz") 

56 

57 with tempfile.TemporaryDirectory() as out_dir: 

58 cli = KpexCLI() 

59 cli.main(['main', 

60 '--pdk', PDK.IHP_SG13G2, 

61 '--mode', 'R', 

62 '--gds', gds_path, 

63 '--out_dir', out_dir, 

64 '--2.5D']) 

65 assert cli.rcx25_extraction_results is not None 

66 

67 lvsdb = kdb.LayoutVsSchematic() 

68 lvsdb.read(os.path.join(out_dir, f"{cell_name}__{cell_name}", f"{cell_name}.lvsdb.gz")) 

69 

70 assert [c.name for c in lvsdb.internal_layout().top_cells()] == [cell_name] 

71 

72 tech = TechInfo.from_json(PDK.IHP_SG13G2.config.tech_pb_json_path, dielectric_filter=None) 

73 pex_context = KLayoutExtractionContext.prepare_extraction(lvsdb=lvsdb, 

74 top_cell=cell_name, 

75 tech=tech, 

76 blackbox_devices=False) 

77 devices = list(pex_context.devices_by_name.values()) 

78 assert [(d.device_class_name, d.device_abstract_name) for d in devices] == [('rfnmos', 'D$rfnmos')] 

79 assert {t.name: [r.layer.canonical_layer_name for r in t.region_by_layer] for t in devices[0].terminals} \ 

80 == {'S': ['nSD'], 'G': ['GatPoly'], 'D': ['nSD'], 'B': ['PWell']} 

81 

82 

83@allure.parent_suite(parent_suite) 

84@allure.tag("PEX", "2.5D") 

85@pytest.mark.slow 

86def test_device_terminals_without_tech_layer_are_where_they_overlap_their_net(): 

87 # The terminals of the MOM capacitor are on its ports (cap_cmomi_m5_ports, the Metal5 pins within its marker), 

88 # an LVS layer the tech info has no layer for (#217), so they are where they overlap the Metal5 of their nets 

89 cell_name = 'cmomi_w5u_l5u_m1_m5' 

90 gds_path = os.path.join(TEST_DESIGNS_DIR, cell_name, f"{cell_name}.gds.gz") 

91 

92 with tempfile.TemporaryDirectory() as out_dir, \ 

93 mock.patch('klayout_pex.klayout.lvsdb_extractor.warning') as warning_mock: 

94 KpexCLI().main(['main', 

95 '--pdk', PDK.IHP_SG13G2, 

96 '--mode', 'R', 

97 '--gds', gds_path, 

98 '--out_dir', out_dir, 

99 '--2.5D']) 

100 

101 lvsdb = kdb.LayoutVsSchematic() 

102 lvsdb.read(os.path.join(out_dir, f"{cell_name}__{cell_name}", f"{cell_name}.lvsdb.gz")) 

103 

104 assert [c.args[0] for c in warning_mock.call_args_list if 'device terminals' in c.args[0]] == [] 

105 

106 tech = TechInfo.from_json(PDK.IHP_SG13G2.config.tech_pb_json_path, dielectric_filter=None) 

107 pex_context = KLayoutExtractionContext.prepare_extraction(lvsdb=lvsdb, 

108 top_cell=cell_name, 

109 tech=tech, 

110 blackbox_devices=True) 

111 mom_cap, = [d for d in pex_context.devices_by_name.values() if d.device_class_name == 'cap_cmomi'] 

112 assert {t.name: [r.layer.canonical_layer_name for r in t.region_by_layer] for t in mom_cap.terminals} \ 

113 == {'mim_top': ['Metal5'], 'mim_btm': ['Metal5']} 

114 

115 

116@allure.parent_suite(parent_suite) 

117@allure.tag("PEX", "2.5D") 

118@pytest.mark.slow 

119@pytest.mark.parametrize('design_dir, cell_name, substrate_net_name', [ 

120 # no label names the substrate, so it's the global net sub! of the PEX-LVS script 

121 ('rfnmos', 'rfnmos_w1u_l0u72', 'sub!'), 

122 # the taps connect the substrate to VSS, which names it, so it's found by pwell_sub 

123 ('sg13g2_a21o_1', 'sg13g2_a21o_1', 'VSS'), 

124]) 

125def test_substrate_net_is_found_by_its_name_or_its_layer(design_dir: str, cell_name: str, substrate_net_name: str): 

126 gds_path = os.path.join(TEST_DESIGNS_DIR, design_dir, f"{cell_name}.gds.gz") 

127 with tempfile.TemporaryDirectory() as out_dir: 

128 KpexCLI().main(['main', 

129 '--pdk', PDK.IHP_SG13G2, 

130 '--mode', 'R', 

131 '--gds', gds_path, 

132 '--out_dir', out_dir, 

133 '--2.5D']) 

134 lvsdb = kdb.LayoutVsSchematic() 

135 lvsdb.read(os.path.join(out_dir, f"{cell_name}__{cell_name}", f"{cell_name}.lvsdb.gz")) 

136 

137 tech = TechInfo.from_json(PDK.IHP_SG13G2.config.tech_pb_json_path, dielectric_filter=None) 

138 pex_context = KLayoutExtractionContext.prepare_extraction(lvsdb=lvsdb, 

139 top_cell=cell_name, 

140 tech=tech, 

141 blackbox_devices=False) 

142 assert pex_context.substrate_net_name == substrate_net_name 

143 

144 

145def extract_test_pattern(cell_name: str) -> Tuple[List[str], List[str]]: 

146 """ 

147 :return: the lines of the CSV and of the SPICE netlist (with continuation lines joined) 

148 """ 

149 gds_path = os.path.join(TEST_DESIGNS_DIR, 'test_patterns', f"{cell_name}.gds.gz") 

150 with tempfile.TemporaryDirectory() as out_dir: 

151 cli = KpexCLI() 

152 cli.main(['main', 

153 '--pdk', PDK.IHP_SG13G2, 

154 '--mode', 'R', 

155 '--gds', gds_path, 

156 '--out_dir', out_dir, 

157 '--2.5D']) 

158 csv_path = cli.rcx25_extracted_csv_path 

159 with open(csv_path) as f: 

160 csv_lines = f.read().splitlines() 

161 with open(csv_path[:-len('.csv')] + '.spice') as f: 

162 spice_lines = f.read().replace('\n+', ' ').splitlines() 

163 return csv_lines, spice_lines 

164 

165 

166def subckt_ports(spice_lines: List[str]) -> List[str]: 

167 subckt_line, = [line for line in spice_lines if line.startswith('.SUBCKT ')] 

168 return [port.replace('\\', '') for port in subckt_line.split()[2:]] 

169 

170 

171def ports_touching_nothing(spice_lines: List[str]) -> List[str]: 

172 element_nodes: Set[str] = set() 

173 for line in spice_lines: 

174 tokens = line.split() 

175 if not tokens or tokens[0][0] not in 'CRX': 

176 continue 

177 if tokens[0][0] == 'X': # the nodes, the subcircuit, then the parameters 

178 nodes = tokens[1:next(i for i, t in enumerate(tokens) if '=' in t) - 1] 

179 else: 

180 nodes = tokens[1:3] 

181 element_nodes.update(node.replace('\\', '') for node in nodes) 

182 return [port for port in subckt_ports(spice_lines) if port not in element_nodes] 

183 

184 

185@allure.parent_suite(parent_suite) 

186@allure.tag("PEX", "2.5D") 

187@pytest.mark.slow 

188def test_nets_of_the_same_name_stay_apart(): 

189 # Two transistors, each with its own metal islands labelled D and S, which are not connected 

190 # (like the islands of a supply, joined only in the parent). The IHP LVS script connects no nets implicitly, 

191 # so they are distinct nets of the same name, which KLayout's SPICE writer calls D, D$1, S, S$1 (#211 §10) 

192 csv_lines, _ = extract_test_pattern('nmos_metal1_redux_twice') 

193 assert csv_lines == """Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

194R1;$1;$1.$0.GatPoly;;33.654 

195R2;$1.$0.GatPoly;$1.$1.Metal1;;15.0 

196R3;$4;$4.$0.GatPoly;;33.654 

197R4;$4.$0.GatPoly;$4.$1.Metal1;;15.0 

198R5;D;D.$1.Metal1;;0.488 

199R6;D$1;D$1.$1.Metal1;;0.488 

200R7;D$1.$1.Metal1;D$1.P0.nSD;;17.0 

201R8;D.$1.Metal1;D.P0.nSD;;17.0 

202R9;S;S.$1.Metal1;;0.598 

203R10;S$1;S$1.$1.Metal1;;0.598 

204R11;S$1.$1.Metal1;S$1.P0.nSD;;17.0 

205R12;S.$1.Metal1;S.P0.nSD;;17.0""".splitlines() 

206 

207 

208@allure.parent_suite(parent_suite) 

209@allure.tag("PEX", "2.5D") 

210@pytest.mark.slow 

211def test_a_net_of_several_labels_has_a_port_per_label(): 

212 # The drain wire of the transistor has the labels D and X at its ends, so LVS has one net D,X with one pin, 

213 # which the netlist replaces by the ports D and X at the labels, where the parent connects (#211 §11) 

214 csv_lines, spice_lines = extract_test_pattern('nmos_metal1_redux_two_drain_labels') 

215 assert csv_lines == """Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

216R1;$1;$1.$0.GatPoly;;33.654 

217R2;$1.$0.GatPoly;$1.$1.Metal1;;15.0 

218R3;D;D,X.$1.Metal1;;0.488 

219R4;D,X;D,X.$1.Metal1;;17.0 

220R5;D,X.$1.Metal1;X;;0.089 

221R6;S;S.$1.Metal1;;0.598 

222R7;S.$1.Metal1;S.P0.nSD;;17.0""".splitlines() 

223 assert subckt_ports(spice_lines) == ['D', 'X', 'S', 'sub!'] # sub!: the substrate, the bulk of the transistor 

224 assert ports_touching_nothing(spice_lines) == [] 

225 

226 

227@allure.parent_suite(parent_suite) 

228@allure.tag("PEX", "2.5D") 

229@pytest.mark.slow 

230def test_labels_on_one_node_are_tied(): 

231 # The labels D and X are on the same pin of the drain wire, so both ports are on one node, 

232 # tied by 1 mΩ, rather than one of them touching nothing (#211 §11) 

233 csv_lines, spice_lines = extract_test_pattern('nmos_metal1_redux_two_labels_one_node') 

234 assert csv_lines == """Device;Net1;Net2;Capacitance [fF];Resistance [Ω] 

235R1;$1;$1.$0.GatPoly;;33.654 

236R2;$1.$0.GatPoly;$1.$1.Metal1;;15.0 

237R3;D;D,X.$1.Metal1;;0.488 

238R4;D;X;;0.001 

239R5;D,X;D,X.$1.Metal1;;17.0 

240R6;S;S.$1.Metal1;;0.598 

241R7;S.$1.Metal1;S.P0.nSD;;17.0""".splitlines() 

242 assert subckt_ports(spice_lines) == ['D', 'X', 'S', 'sub!'] # sub!: the substrate, the bulk of the transistor 

243 assert ports_touching_nothing(spice_lines) == [] 

244 

245 

246@allure.parent_suite(parent_suite) 

247@allure.tag("PEX", "2.5D") 

248@pytest.mark.slow 

249def test_mim_cap__whiteboxed(): 

250 # The white-box extraction keeps the MIM cap layers, so the top plate (MIM, LVS layer cmim_top) is a conductor 

251 # of its net, joined to TopMetal1 by the TopVia1 on it (mim_via), 5 cuts per row (2.2 Ω / 5). 

252 # It stopped with "unhandled layer purpose PURPOSE_MIM_CAP". 

253 # The bottom plate (Metal5) has no network, as nothing connects to it 

254 gds_path = os.path.join(TEST_DESIGNS_DIR, 'sg13g2_pr__cmim', 'cmim.gds.gz') 

255 with tempfile.TemporaryDirectory() as out_dir: 

256 cli = KpexCLI() 

257 cli.main(['main', 

258 '--pdk', PDK.IHP_SG13G2, 

259 '--mode', 'R', 

260 '--gds', gds_path, 

261 '--out_dir', out_dir, 

262 '--2.5D']) 

263 results, = cli.rcx25_extraction_results.cell_extraction_results.values() 

264 

265 # NOTE: the nets have no labels, so their names ($1, $2, ...) depend on the LVS run 

266 network, = [n for n in results.r_extraction_result.networks if n.elements] 

267 assert {node.layer_name for node in network.nodes} == {'MIM', 'TopMetal1'} 

268 assert sorted(round(r, 3) for r in results.summarize().resistances.values()) == \ 

269 [0.004] * 4 + [0.44] * 5 + [2.408] * 4