Coverage for klayout_pex/rcx25/netlist_checks.py: 94%

143 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""" 

25Consistency checks of the RC netlist against the LVS netlist it's made of (#215) 

26 

27NOTE: the defects of #211 gave RC netlists that simulate to plausible, but wrong numbers, 

28 so these checks compare the RC netlist with the LVS netlist by identity 

29 (the devices by ID, the nets by cluster ID), not by the names of the nodes 

30""" 

31 

32from __future__ import annotations 

33 

34from collections import Counter, defaultdict 

35from typing import * 

36 

37import klayout.db as kdb 

38 

39from ..device_models import DeviceModels 

40from .extraction_results import ExtractionSummary 

41from .netlist_expander import RCX25NetlistExpander, SUBSTRATE 

42from .types import NetName 

43from ..klayout.parasitic_device_classes import ( 

44 PARASITIC_CAPACITOR_CLASS_NAME, 

45 PARASITIC_DEVICE_CLASS_NAMES, 

46 PARASITIC_RESISTOR_CLASS_NAME, 

47) 

48 

49 

50NET_ID_PROPERTY = 'kpex_check_net_id' 

51 

52 

53class RCNetlistCheckError(Exception): 

54 """ 

55 The RC netlist is inconsistent with the LVS netlist (see check_rc_netlist) 

56 """ 

57 pass 

58 

59# NOTE: relative, the capacitances of the summary and the netlist differ by rounding only 

60CAPACITANCE_TOLERANCE = 1e-9 

61 

62 

63def first_names(names: List[str]) -> str: 

64 """ 

65 :return: the first three names, e.g. 'a, b, c and 2 more' 

66 """ 

67 return f"{', '.join(names[:3])}{'' if len(names) <= 3 else f' and {len(names) - 3} more'}" 

68 

69 

70def check_rc_netlist(lvs_netlist: kdb.Netlist, 

71 rc_netlist: kdb.Netlist, 

72 top_cell_name: str, 

73 summary: ExtractionSummary, 

74 blackbox_devices: bool, 

75 device_models: DeviceModels, 

76 substrate_net_name: Optional[str] = None) -> List[str]: 

77 """ 

78 :param blackbox_devices: whether the RC netlist keeps the devices, rather than the whiteboxed ones 

79 removed (see RCX25NetlistExpander) 

80 :param device_models: the device models of the tech info, which tell the whiteboxed devices 

81 :param substrate_net_name: the net of the capacitances to the substrate (see RCX25NetlistExpander) 

82 :return: the problems of the RC netlist, e.g. a device terminal that is not on 

83 the resistor network of its net, so that the resistances don't reach it 

84 """ 

85 lvs_circuit: kdb.Circuit = lvs_netlist.circuit_by_name(top_cell_name) 

86 

87 # NOTE: the nets of an extracted netlist have unique cluster IDs, which the copies keep 

88 cluster_ids = [net.cluster_id for net in lvs_circuit.each_net()] 

89 if 0 in cluster_ids or len(set(cluster_ids)) != len(cluster_ids): 

90 return ["the nets of the LVS netlist have no unique cluster IDs, to find them in the RC netlist"] 

91 

92 # NOTE: KLayout's net objects are no Python identities, so the nets get an ID (on a copy) 

93 rc_device_ids = [(device.id(), device.expanded_name()) 

94 for device in rc_netlist.circuit_by_name(top_cell_name).each_device()] 

95 rc_netlist = rc_netlist.dup() 

96 rc_circuit: kdb.Circuit = rc_netlist.circuit_by_name(top_cell_name) 

97 

98 nets = list(rc_circuit.each_net()) 

99 for net_id, net in enumerate(nets): 

100 net.set_property(NET_ID_PROPERTY, net_id) 

101 

102 def id_of(net: kdb.Net) -> int: 

103 return net.property(NET_ID_PROPERTY) 

104 

105 # NOTE: the copy numbers its devices anew (in their order), which closes the gaps of the removed ones 

106 # (e.g. whiteboxed), so that a device would have the ID of another one of the LVS netlist, 

107 # hence the devices go by the IDs and names of the RC netlist, which are the ones of the LVS netlist 

108 devices: Dict[int, Tuple[str, kdb.Device]] = { 

109 device_id: (device_name, device) 

110 for (device_id, device_name), device in zip(rc_device_ids, rc_circuit.each_device()) 

111 } 

112 

113 problems: List[str] = [] 

114 

115 # the pieces of the netlist, DC-connected by the parasitic resistors (union-find by net ID) 

116 parent = list(range(len(nets))) 

117 

118 def piece_of(net_id: int) -> int: 

119 while parent[net_id] != net_id: 

120 parent[net_id] = parent[parent[net_id]] 

121 net_id = parent[net_id] 

122 return net_id 

123 

124 has_resistor: Set[int] = set() 

125 for device in rc_circuit.each_device(): 

126 if device.device_class().name != PARASITIC_RESISTOR_CLASS_NAME: 

127 continue 

128 net_a, net_b = device.net_for_terminal('A'), device.net_for_terminal('B') 

129 has_resistor.update((id_of(net_a), id_of(net_b))) 

130 parent[piece_of(id_of(net_a))] = piece_of(id_of(net_b)) 

131 

132 # NOTE: each LVS net keeps its net in the RC netlist, which its resistor network hangs off 

133 lvs_net_by_piece: Dict[int, NetName] = {} 

134 for lvs_net in lvs_circuit.each_net(): 

135 rc_net = rc_circuit.net_by_cluster_id(lvs_net.cluster_id) 

136 if rc_net is None: 

137 problems.append(f"net {lvs_net.expanded_name()} is missing") 

138 continue 

139 piece = piece_of(id_of(rc_net)) 

140 other = lvs_net_by_piece.setdefault(piece, lvs_net.expanded_name()) 

141 if other != lvs_net.expanded_name(): 

142 problems.append(f"resistors join the nets {other} and {lvs_net.expanded_name()}") 

143 

144 def lvs_net_name(rc_net: kdb.Net) -> NetName: 

145 # NOTE: a net of no LVS net, e.g. the substrate of the capacitances 

146 return lvs_net_by_piece.get(piece_of(id_of(rc_net)), rc_net.expanded_name()) 

147 

148 floating_pieces = {piece_of(net_id) for net_id in has_resistor} - set(lvs_net_by_piece) 

149 names_by_floating_piece: Dict[int, List[NetName]] = defaultdict(list) 

150 for net_id, net in enumerate(nets): 

151 if piece_of(net_id) in floating_pieces: 

152 names_by_floating_piece[piece_of(net_id)].append(net.expanded_name()) 

153 for names in sorted(sorted(names) for names in names_by_floating_piece.values()): 

154 problems.append(f"resistors of the nodes {first_names(names)} touch no net") 

155 

156 # the RC netlist has the devices of the LVS netlist, but the whiteboxed ones, 

157 # whose capacitances are extracted from their plates and fingers instead, so they'd count twice 

158 device_counts: Counter[str] = Counter() 

159 missing_devices: Dict[str, List[str]] = defaultdict(list) 

160 kept_whiteboxed_devices: Dict[str, List[str]] = defaultdict(list) 

161 for lvs_device in lvs_circuit.each_device(): 

162 class_name = lvs_device.device_class().name 

163 device_counts[class_name] += 1 

164 _, device = devices.get(lvs_device.id(), (None, None)) 

165 # NOTE: the ID of a removed device may be given to a parasitic one 

166 in_rc_netlist = device is not None and device.device_class().name == class_name 

167 if blackbox_devices or not RCX25NetlistExpander.is_whiteboxed(lvs_device.device_class(), device_models): 

168 if not in_rc_netlist: 

169 missing_devices[class_name].append(lvs_device.expanded_name()) 

170 elif in_rc_netlist: 

171 kept_whiteboxed_devices[class_name].append(lvs_device.expanded_name()) 

172 for class_name, names in sorted(missing_devices.items()): 

173 problems.append(f"the RC netlist lacks {len(names)} of the {device_counts[class_name]} devices " 

174 f"of class {class_name}: {first_names(names)}") 

175 for class_name, names in sorted(kept_whiteboxed_devices.items()): 

176 problems.append(f"the RC netlist keeps {len(names)} of the {device_counts[class_name]} whiteboxed devices " 

177 f"of class {class_name}, which counts their capacitances twice: {first_names(names)}") 

178 

179 # every device terminal is on the resistor network of its net 

180 for device_id, (device_name, device) in devices.items(): 

181 if device.device_class().name in PARASITIC_DEVICE_CLASS_NAMES: 

182 continue 

183 lvs_device = lvs_circuit.device_by_id(device_id) 

184 if lvs_device is None: 

185 problems.append(f"device {device_name} is not in the LVS netlist") 

186 continue 

187 for terminal in device.device_class().terminal_definitions(): 

188 lvs_net = lvs_device.net_for_terminal(terminal.id()) 

189 rc_net = device.net_for_terminal(terminal.id()) 

190 if lvs_net is None: 

191 continue 

192 if rc_net is None or lvs_net_name(rc_net) != lvs_net.expanded_name(): 

193 problems.append(f"terminal {terminal.name} of device {device_name} " 

194 f"is not on the resistor network of its net {lvs_net.expanded_name()}") 

195 

196 # every device terminal with a port is on the node of its port (#211 §6) 

197 for key, node_name in sorted(summary.device_terminal_nodes.items()): 

198 device_name, device = devices.get(key.device_id, (None, None)) 

199 # NOTE: e.g. removed, as whiteboxed, and its ID given to a parasitic device 

200 if device is None or device.device_class().name in PARASITIC_DEVICE_CLASS_NAMES: 

201 continue 

202 rc_net = device.net_for_terminal(key.terminal_id) 

203 if rc_net is None or rc_net.expanded_name() != node_name: 

204 terminal_name = device.device_class().terminal_definitions()[key.terminal_id].name 

205 problems.append(f"terminal {terminal_name} of device {device_name} " 

206 f"is not on the node {node_name} of its port") 

207 

208 # every port touches something, and every net with a pin in the LVS netlist keeps a port 

209 ported_lvs_nets: Set[NetName] = set() 

210 for pin in rc_circuit.each_pin(): 

211 rc_net = rc_circuit.net_for_pin(pin.id()) 

212 if rc_net is None or rc_net.terminal_count() == 0: 

213 problems.append(f"port {pin.name()} touches nothing") 

214 if rc_net is not None: 

215 ported_lvs_nets.add(lvs_net_name(rc_net)) 

216 for pin in lvs_circuit.each_pin(): 

217 lvs_net = lvs_circuit.net_for_pin(pin.id()) 

218 if lvs_net is not None and lvs_net.expanded_name() not in ported_lvs_nets: 

219 problems.append(f"net {lvs_net.expanded_name()} has no port") 

220 

221 # the capacitances between each pair of nets are the ones of the summary 

222 # NOTE: the summary names the nodes, which must be unique for that 

223 name_counts = Counter(net.expanded_name() for net in nets) 

224 duplicate_names = sorted(name for name, count in name_counts.items() if count > 1) 

225 if duplicate_names: 

226 problems.append(f"nets have the same name: {', '.join(duplicate_names)}") 

227 rc_net_by_name = {net.expanded_name(): net for net in nets} 

228 

229 def net_pair(net_a: NetName, net_b: NetName) -> Tuple[NetName, NetName]: 

230 return (net_a, net_b) if net_a <= net_b else (net_b, net_a) 

231 

232 netlist_capacitances: Dict[Tuple[NetName, NetName], float] = defaultdict(float) 

233 for device in rc_circuit.each_device(): 

234 if device.device_class().name != PARASITIC_CAPACITOR_CLASS_NAME: 

235 continue 

236 pair = net_pair(lvs_net_name(device.net_for_terminal('A')), lvs_net_name(device.net_for_terminal('B'))) 

237 netlist_capacitances[pair] += device.parameter('C') * 1e15 # fF 

238 

239 def rc_net(node_name: NetName) -> Optional[kdb.Net]: 

240 if node_name == SUBSTRATE: 

241 node_name = substrate_net_name or SUBSTRATE 

242 return rc_net_by_name.get(node_name) 

243 

244 summary_capacitances: Dict[Tuple[NetName, NetName], float] = defaultdict(float) 

245 for key, capacitance in summary.capacitances.items(): 

246 net1, net2 = rc_net(key.net1), rc_net(key.net2) 

247 if net1 is None or net2 is None: 

248 problems.append(f"the capacitance between {key.net1} and {key.net2} has no nodes") 

249 continue 

250 if id_of(net1) == id_of(net2) and SUBSTRATE in (key.net1, key.net2): 

251 continue # e.g. the metal of the substrate net itself (VSS), shorted 

252 summary_capacitances[net_pair(lvs_net_name(net1), lvs_net_name(net2))] += capacitance 

253 

254 for pair in sorted(set(netlist_capacitances) | set(summary_capacitances)): 

255 expected, obtained = summary_capacitances.get(pair, 0.0), netlist_capacitances.get(pair, 0.0) 

256 if abs(obtained - expected) > CAPACITANCE_TOLERANCE * max(abs(expected), abs(obtained)): 

257 problems.append(f"the capacitance between {pair[0]} and {pair[1]} is {'%.12g' % obtained} fF, " 

258 f"but {'%.12g' % expected} fF in the summary") 

259 

260 return problems