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

137 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# 

24from __future__ import annotations 

25 

26import allure 

27from typing import * 

28import unittest 

29 

30import klayout.db as kdb 

31 

32from klayout_pex.device_models import DeviceModels 

33from klayout_pex.klayout.parasitic_device_classes import ( 

34 PARASITIC_CAPACITOR_CLASS_NAME, 

35 PARASITIC_RESISTOR_CLASS_NAME, 

36) 

37from klayout_pex.rcx25.extraction_results import DeviceTerminalKey, ExtractionSummary, NetCoupleKey 

38from klayout_pex.rcx25.netlist_checks import check_rc_netlist 

39import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

40 

41 

42def lvs_netlist(mim_cap: bool = False) -> kdb.Netlist: 

43 """ 

44 An nmos M1, with a pin on D, G and S 

45 

46 :param mim_cap: with a MIM cap C1 between G and S, before M1 

47 """ 

48 netlist = kdb.Netlist() 

49 circuit = kdb.Circuit() 

50 circuit.name = 'chip' 

51 netlist.add(circuit) 

52 nmos = kdb.DeviceClassMOS4Transistor() 

53 nmos.name = 'nmos' 

54 netlist.add(nmos) 

55 if mim_cap: 

56 mim = kdb.DeviceClassCapacitor() 

57 mim.name = 'mim' 

58 netlist.add(mim) 

59 c1 = circuit.create_device(mim, 'C1') 

60 m1 = circuit.create_device(nmos, 'M1') 

61 for cluster_id, terminal in enumerate(('D', 'G', 'S', 'B'), start=1): 

62 net = circuit.create_net(terminal) 

63 net.cluster_id = cluster_id # like extracted nets 

64 m1.connect_terminal(terminal, net) 

65 if terminal != 'B': 

66 circuit.connect_pin(circuit.create_pin(terminal), net) 

67 if mim_cap: 

68 c1.connect_terminal('A', circuit.net_by_name('G')) 

69 c1.connect_terminal('B', circuit.net_by_name('S')) 

70 return netlist 

71 

72 

73class RCNetlist: 

74 """ 

75 The LVS netlist, with parasitic resistors and capacitors added, like the netlist expansion does 

76 """ 

77 def __init__(self, lvs: kdb.Netlist): 

78 self.netlist = lvs.dup() 

79 self.circuit = self.netlist.circuit_by_name('chip') 

80 self.res = kdb.DeviceClassResistor() 

81 self.res.name = PARASITIC_RESISTOR_CLASS_NAME 

82 self.netlist.add(self.res) 

83 self.cap = kdb.DeviceClassCapacitor() 

84 self.cap.name = PARASITIC_CAPACITOR_CLASS_NAME 

85 self.netlist.add(self.cap) 

86 

87 def net(self, name: str) -> kdb.Net: 

88 return self.circuit.net_by_name(name) or self.circuit.create_net(name) 

89 

90 def add(self, device_class: kdb.DeviceClass, net_a: str, net_b: str, parameter: str, value: float): 

91 device = self.circuit.create_device(device_class, '') 

92 device.connect_terminal('A', self.net(net_a)) 

93 device.connect_terminal('B', self.net(net_b)) 

94 device.set_parameter(parameter, value) 

95 

96 def add_resistor(self, net_a: str, net_b: str, resistance: float = 10.0): 

97 self.add(self.res, net_a, net_b, 'R', resistance) 

98 

99 def add_capacitor(self, net_a: str, net_b: str, capacitance_femto: float): 

100 self.add(self.cap, net_a, net_b, 'C', capacitance_femto / 1e15) 

101 

102 def connect(self, device_name: str, terminal: str, net_name: str): 

103 self.circuit.device_by_name(device_name).connect_terminal(terminal, self.net(net_name)) 

104 

105 

106@allure.parent_suite("Unit Tests") 

107@allure.tag("RCX25", "Netlist Checks") 

108class Test(unittest.TestCase): 

109 @staticmethod 

110 def check(rc: RCNetlist, 

111 capacitances: Dict[Tuple[str, str], float], 

112 device_terminal_nodes: Optional[Dict[str, str]] = None, 

113 substrate_net_name: Optional[str] = None, 

114 blackbox_devices: bool = False) -> List[str]: 

115 """ 

116 :param device_terminal_nodes: the node of the port of each terminal of M1 (e.g. D), 

117 or of another device (e.g. C1.A) 

118 """ 

119 def key(terminal: str) -> DeviceTerminalKey: 

120 device_name, terminal = terminal.split('.') if '.' in terminal else ('M1', terminal) 

121 # NOTE: the device IDs of the summary are the ones of the LVS netlist 

122 device = rc.lvs.circuit_by_name('chip').device_by_name(device_name) 

123 return DeviceTerminalKey(device.id(), device.device_class().terminal_id(terminal)) 

124 

125 summary = ExtractionSummary( 

126 capacitances={NetCoupleKey(*k): c for k, c in capacitances.items()}, 

127 resistances={}, 

128 device_terminal_nodes={key(terminal): node for terminal, node in (device_terminal_nodes or {}).items()} 

129 ) 

130 # NOTE: the MIM cap is a metal capacitor, which white-box mode removes 

131 device_models = device_models_pb2.DeviceModelsInfo() 

132 device_models.device_model_mappings.add(lvs_device_class_name='mim', 

133 kind=device_models_pb2.DeviceModelMapping.KIND_METAL_CAPACITOR) 

134 return check_rc_netlist(lvs_netlist=rc.lvs, rc_netlist=rc.netlist, top_cell_name='chip', summary=summary, 

135 blackbox_devices=blackbox_devices, device_models=DeviceModels(device_models), 

136 substrate_net_name=substrate_net_name) 

137 

138 def rc_netlist(self, mim_cap: bool = False) -> RCNetlist: 

139 lvs = lvs_netlist(mim_cap) 

140 rc = RCNetlist(lvs) 

141 rc.lvs = lvs 

142 return rc 

143 

144 def test_consistent_netlist(self): 

145 # the drain is on the end of the drain wire, the wire has a capacitance to the gate 

146 rc = self.rc_netlist() 

147 rc.add_resistor('D', 'D.$1.li1') 

148 rc.add_resistor('D.$1.li1', 'D.P0.nsdm') 

149 rc.connect('M1', 'D', 'D.P0.nsdm') 

150 rc.add_capacitor('D.$1.li1', 'G', 1.0) 

151 self.assertEqual([], self.check(rc, {('D.$1.li1', 'G'): 1.0}, {'D': 'D.P0.nsdm'})) 

152 

153 def test_devices_after_a_whiteboxed_one(self): 

154 # the whiteboxed C1 is removed, the devices after it keep their IDs, the ones of the LVS netlist 

155 # NOTE: a copy of the RC netlist numbers them anew, which would make M1 the C1 of the LVS netlist 

156 rc = self.rc_netlist(mim_cap=True) 

157 rc.circuit.remove_device(rc.circuit.device_by_name('C1')) 

158 rc.add_resistor('D', 'D.P0.nsdm') 

159 rc.add_resistor('G', 'G.P0.metal1') 

160 rc.connect('M1', 'D', 'D.P0.nsdm') 

161 self.assertEqual([], self.check(rc, {}, {'D': 'D.P0.nsdm', 'C1.A': 'G.P0.metal1'})) 

162 

163 def test_blackboxed_device_is_kept(self): 

164 rc = self.rc_netlist(mim_cap=True) 

165 self.assertEqual([], self.check(rc, {}, blackbox_devices=True)) 

166 

167 def test_lost_device(self): 

168 # black-box mode keeps the MIM cap, as its plates are not extracted 

169 rc = self.rc_netlist(mim_cap=True) 

170 rc.circuit.remove_device(rc.circuit.device_by_name('C1')) 

171 self.assertEqual(['the RC netlist lacks 1 of the 1 devices of class mim: C1'], 

172 self.check(rc, {}, blackbox_devices=True)) 

173 

174 def test_whiteboxed_device_is_counted_twice(self): 

175 # like the whiteboxed devices after the first one, which the netlist expansion didn't remove, 

176 # but whose capacitances it extracted from their plates and fingers 

177 rc = self.rc_netlist(mim_cap=True) 

178 self.assertEqual(['the RC netlist keeps 1 of the 1 whiteboxed devices of class mim, ' 

179 'which counts their capacitances twice: C1'], 

180 self.check(rc, {})) 

181 

182 def test_floating_resistor_network(self): 

183 # like #211 §6, on a net without a pin: the drain on the net, its wire floating 

184 rc = self.rc_netlist() 

185 rc.add_resistor('D.$1.li1', 'D.P0.nsdm') 

186 self.assertEqual(['resistors of the nodes D.$1.li1, D.P0.nsdm touch no net', 

187 'terminal D of device M1 is not on the node D.P0.nsdm of its port'], 

188 self.check(rc, {}, {'D': 'D.P0.nsdm'})) 

189 

190 def test_terminal_not_on_its_port(self): 

191 # like #211 §6, on a net with a pin: the drain on the pin, not the end of its wire 

192 rc = self.rc_netlist() 

193 rc.add_resistor('D', 'D.P0.nsdm') 

194 self.assertEqual(['terminal D of device M1 is not on the node D.P0.nsdm of its port'], 

195 self.check(rc, {}, {'D': 'D.P0.nsdm'})) 

196 

197 def test_terminal_on_another_net(self): 

198 rc = self.rc_netlist() 

199 rc.add_resistor('D', 'D.$1.li1') 

200 rc.add_resistor('S', 'S.$1.li1') 

201 rc.connect('M1', 'D', 'S.$1.li1') 

202 self.assertEqual(['terminal D of device M1 is not on the resistor network of its net D'], 

203 self.check(rc, {})) 

204 

205 def test_resistors_joining_nets(self): 

206 rc = self.rc_netlist() 

207 rc.add_resistor('D', 'S') 

208 problems = self.check(rc, {}) 

209 self.assertIn('resistors join the nets D and S', problems) 

210 self.assertIn('terminal S of device M1 is not on the resistor network of its net S', problems) 

211 

212 def test_port_touching_nothing(self): 

213 # like #211 §11: the pin of D replaced by a port on a node of nothing 

214 rc = self.rc_netlist() 

215 pin_d = rc.circuit.pin_by_name('D') 

216 rc.circuit.disconnect_pin(pin_d.id()) 

217 rc.circuit.connect_pin(pin_d.id(), rc.net('X')) 

218 self.assertEqual(['port D touches nothing', 'net D has no port'], self.check(rc, {})) 

219 

220 def test_lost_capacitance(self): 

221 rc = self.rc_netlist() 

222 rc.add_capacitor('D', 'G', 1.0) 

223 self.assertEqual(['the capacitance between D and G is 1 fF, but 1.5 fF in the summary', 

224 'the capacitance between D and S is 0 fF, but 0.5 fF in the summary'], 

225 self.check(rc, {('D', 'G'): 1.5, ('D', 'S'): 0.5})) 

226 

227 def test_nets_of_the_same_name(self): 

228 # like #211 §10: the islands of D, joined only in the parent 

229 rc = self.rc_netlist() 

230 rc.circuit.create_net('D') 

231 self.assertEqual(['nets have the same name: D'], self.check(rc, {})) 

232 

233 def test_capacitances_to_the_substrate_are_on_its_net(self): 

234 # the summary has them on VSUBS, the netlist on the substrate net B, but the one of B itself, shorted 

235 rc = self.rc_netlist() 

236 rc.add_capacitor('D', 'B', 1.0) 

237 self.assertEqual([], self.check(rc, {('D', 'VSUBS'): 1.0, ('B', 'VSUBS'): 2.0}, substrate_net_name='B'))