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

214 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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), 

26and of the LVS netlist against the schematic (#345) 

27 

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

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

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

31""" 

32 

33from __future__ import annotations 

34 

35from collections import Counter, defaultdict 

36import math 

37from typing import * 

38 

39import klayout.db as kdb 

40 

41from ..device_models import DeviceModels 

42from ..device_totals import DeviceTotalParameters 

43from ..log import info, warning 

44from ..schematic_devices import SchematicDevices 

45from .extraction_results import ExtractionSummary 

46from .netlist_expander import RCX25NetlistExpander, SUBSTRATE 

47from ..types import NetName 

48from ..util.disjoint_sets import DisjointSets 

49from ..klayout.parasitic_device_classes import ( 

50 PARASITIC_CAPACITOR_CLASS_NAME, 

51 PARASITIC_DEVICE_CLASS_NAMES, 

52 PARASITIC_RESISTOR_CLASS_NAME, 

53) 

54 

55 

56NET_ID_PROPERTY = 'kpex_check_net_id' 

57 

58 

59class RCNetlistCheckError(Exception): 

60 """ 

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

62 """ 

63 pass 

64 

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

66CAPACITANCE_TOLERANCE = 1e-9 

67 

68# NOTE: relative, the device totals of a schematic and its layout differ by rounding only (e.g. of µm in m and back) 

69DEVICE_TOTAL_TOLERANCE = 1e-6 

70 

71 

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

73 """ 

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

75 """ 

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

77 

78 

79def check_rc_netlist(lvs_netlist: kdb.Netlist, 

80 rc_netlist: kdb.Netlist, 

81 top_cell_name: str, 

82 summary: ExtractionSummary, 

83 blackbox_devices: bool, 

84 device_models: DeviceModels, 

85 substrate_net_name: Optional[str] = None, 

86 removed_device_ids: Collection[int] = ()) -> List[str]: 

87 """ 

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

89 removed (see RCX25NetlistExpander) 

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

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

92 :param removed_device_ids: the devices the RC netlist leaves out, as a model has them (of cut cells) 

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

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

95 """ 

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

97 

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

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

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

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

102 

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

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

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

106 rc_netlist = rc_netlist.dup() 

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

108 

109 nets = list(rc_circuit.each_net()) 

110 for net_id, net in enumerate(nets): 

111 net.set_property(NET_ID_PROPERTY, net_id) 

112 

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

114 return net.property(NET_ID_PROPERTY) 

115 

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

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

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

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

120 device_id: (device_name, device) 

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

122 } 

123 

124 problems: List[str] = [] 

125 

126 # the pieces of the netlist, DC-connected by the parasitic resistors, by net ID 

127 pieces = DisjointSets(range(len(nets))) 

128 piece_of = pieces.find 

129 

130 has_resistor: Set[int] = set() 

131 for device in rc_circuit.each_device(): 

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

133 continue 

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

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

136 pieces.union(id_of(net_a), id_of(net_b)) 

137 

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

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

140 for lvs_net in lvs_circuit.each_net(): 

141 rc_net = rc_circuit.net_by_cluster_id(lvs_net.cluster_id) 

142 if rc_net is None: 

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

144 continue 

145 piece = piece_of(id_of(rc_net)) 

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

147 if other != lvs_net.expanded_name(): 

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

149 

150 # NOTE: the model of what an exclusion marker stands for joins the pieces of a net at their ports there 

151 for lvs_net_name_of_ports, port_names in sorted(summary.exclusion_ports.items()): 

152 for port_name in port_names: 

153 port_net = rc_circuit.net_by_name(port_name) 

154 if port_net is not None: 

155 lvs_net_by_piece.setdefault(piece_of(id_of(port_net)), lvs_net_name_of_ports) 

156 

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

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

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

160 

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

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

163 for net_id, net in enumerate(nets): 

164 if piece_of(net_id) in floating_pieces: 

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

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

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

168 

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

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

171 device_counts: Counter[str] = Counter() 

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

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

174 for lvs_device in lvs_circuit.each_device(): 

175 if lvs_device.id() in removed_device_ids: 

176 continue 

177 class_name = lvs_device.device_class().name 

178 device_counts[class_name] += 1 

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

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

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

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

183 if not in_rc_netlist: 

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

185 elif in_rc_netlist: 

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

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

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

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

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

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

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

193 

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

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

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

197 continue 

198 lvs_device = lvs_circuit.device_by_id(device_id) 

199 if lvs_device is None: 

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

201 continue 

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

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

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

205 if lvs_net is None: 

206 continue 

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

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

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

210 

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

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

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

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

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

216 continue 

217 rc_net = device.net_for_terminal(key.terminal_id) 

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

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

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

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

222 

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

224 # NOTE: but a port at an exclusion marker of a part of a net that ends there (in R mode, which has no capacitances), 

225 # as the marker cuts metal, so it isn't a label off the metal, and the port is the same in each mode 

226 exclusion_port_names = {port_name for port_names in summary.exclusion_ports.values() for port_name in port_names} 

227 ported_lvs_nets: Set[NetName] = set() 

228 for pin in rc_circuit.each_pin(): 

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

230 if (rc_net is None or rc_net.terminal_count() == 0) and pin.name() not in exclusion_port_names: 

231 # NOTE: but for a net that touches nothing in the LVS netlist either (e.g. a wire of one pin, R mode), 

232 # which goes for the terminals of removed devices too (e.g. only the coil of a cut cell on sub!) 

233 lvs_net = lvs_circuit.net_by_name(lvs_net_name(rc_net)) if rc_net is not None else None 

234 if lvs_net is None or lvs_net.subcircuit_pin_count() > 0 \ 

235 or any(terminal.device().id() not in removed_device_ids for terminal in lvs_net.each_terminal()): 

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

237 if rc_net is not None: 

238 ported_lvs_nets.add(lvs_net_name(rc_net)) 

239 for pin in lvs_circuit.each_pin(): 

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

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

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

243 

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

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

246 # also regardless of case, as SPICE reads names (ngspice joins the nets d and D) 

247 names_by_key: Dict[str, List[NetName]] = defaultdict(list) 

248 for net in nets: 

249 names_by_key[net.expanded_name().casefold()].append(net.expanded_name()) 

250 duplicate_names = sorted(name for names in names_by_key.values() if len(names) > 1 for name in set(names)) 

251 if duplicate_names: 

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

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

254 

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

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

257 

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

259 for device in rc_circuit.each_device(): 

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

261 continue 

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

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

264 

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

266 if node_name == SUBSTRATE: 

267 node_name = substrate_net_name or SUBSTRATE 

268 return rc_net_by_name.get(node_name) 

269 

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

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

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

273 if net1 is None or net2 is None: 

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

275 continue 

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

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

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

279 

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

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

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

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

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

285 

286 return problems 

287 

288 

289def check_schematic_device_classes(lvs_netlist: kdb.Netlist, 

290 schematic: SchematicDevices) -> List[str]: 

291 """ 

292 :return: the problems: the device classes of the schematic that no device of the LVS netlist has, 

293 e.g. of a device the PEX-LVS doesn't recognize, which the extraction then misses 

294 """ 

295 lvs_class_names = {device.device_class().name 

296 for circuit in lvs_netlist.each_circuit() for device in circuit.each_device()} 

297 if lvs_class_names and not schematic.device_names_by_class_name: 

298 models = sorted(schematic.device_names_by_unknown_model) 

299 return [f"kpex reads no devices from the schematic, e.g. of models without device model mapping" 

300 f"{f' ({first_names(models)})' if models else ''}, so no device classes are compared"] 

301 return [f"the schematic has devices of class {class_name} ({first_names(device_names)}), the LVS netlist none" 

302 for class_name, device_names in sorted(schematic.device_names_by_class_name.items()) 

303 if class_name not in lvs_class_names] 

304 

305 

306class _DeviceTotals: 

307 """ 

308 The totals of the devices of a class, and the devices without value for a quantity 

309 """ 

310 

311 def __init__(self): 

312 self.sums: Dict[str, float] = defaultdict(float) 

313 self.devices_without_value: Dict[str, List[str]] = defaultdict(list) 

314 

315 def add(self, device_name: str, quantities: Dict[str, Optional[float]], count: int = 1): 

316 for name, value in quantities.items(): 

317 if value is None: 

318 self.devices_without_value[name].append(device_name) 

319 else: 

320 self.sums[name] += value * count 

321 

322 

323def _circuit_counts(netlist: kdb.Netlist) -> Dict[str, int]: 

324 """ 

325 :return: how often each circuit is in the top circuits, by name 

326 """ 

327 counts: Dict[str, int] = {} 

328 

329 def count(circuit: kdb.Circuit) -> int: 

330 if circuit.name not in counts: 

331 references = list(circuit.each_ref()) 

332 counts[circuit.name] = sum(count(r.circuit()) for r in references) if references else 1 

333 return counts[circuit.name] 

334 

335 for c in netlist.each_circuit(): 

336 count(c) 

337 return counts 

338 

339 

340def check_schematic_device_totals(lvs_netlist: kdb.Netlist, 

341 schematic: SchematicDevices, 

342 device_models: DeviceModels) -> List[str]: 

343 """ 

344 Compares the totals of the device classes of both (see device_totals), e.g. Σ W·L of a MOS, 

345 but those that a device has no value for, which it warns about, 

346 and logs the classes without totals, whose presence check_schematic_device_classes compares only 

347 

348 :return: the problems: the totals that differ, e.g. of a device the PEX-LVS doesn't recognize 

349 in a class it recognizes otherwise 

350 """ 

351 circuit_counts = _circuit_counts(lvs_netlist) 

352 parameters_by_class_name: Dict[str, DeviceTotalParameters] = {} 

353 lvs_totals_by_class_name: Dict[str, _DeviceTotals] = defaultdict(_DeviceTotals) 

354 for circuit in lvs_netlist.each_circuit(): 

355 for device in circuit.each_device(): 

356 device_class = device.device_class() 

357 mapping = device_models.mapping_by_lvs_device_class_name.get(device_class.name) 

358 if mapping is None or device_class.name not in schematic.devices_by_class_name: 

359 continue 

360 parameters = parameters_by_class_name.setdefault(device_class.name, 

361 DeviceTotalParameters.of(device_class, mapping)) 

362 values = {pd.name: device.parameter(pd.name) for pd in device_class.parameter_definitions()} 

363 lvs_totals_by_class_name[device_class.name].add(device.expanded_name(), parameters.quantities(values), 

364 count=circuit_counts[circuit.name]) 

365 

366 problems: List[str] = [] 

367 uncompared: List[str] = [] 

368 for class_name, lvs_totals in sorted(lvs_totals_by_class_name.items()): 

369 parameters = parameters_by_class_name[class_name] 

370 totals = _DeviceTotals() 

371 for device in schematic.devices_by_class_name[class_name]: 

372 totals.add(device.name, parameters.quantities(device.lvs_parameters, device.m)) 

373 for quantity in parameters.quantity_names: 

374 without_value = totals.devices_without_value[quantity] + lvs_totals.devices_without_value[quantity] 

375 if without_value: 

376 uncompared.append(f"Σ {quantity} of class {class_name}: no value of {first_names(without_value)}") 

377 elif not math.isclose(totals.sums[quantity], lvs_totals.sums[quantity], rel_tol=DEVICE_TOTAL_TOLERANCE): 

378 problems.append(f"the schematic has Σ {quantity} = {'%.9g' % totals.sums[quantity]} " 

379 f"of class {class_name}, the LVS netlist {'%.9g' % lvs_totals.sums[quantity]}") 

380 if uncompared: 

381 warning("The device class check leaves out these totals, as devices have no value for them " 

382 "(e.g. of the schematic, which leaves a parameter to the model's default):\n" + 

383 '\n'.join(f" - {u}" for u in uncompared)) 

384 presence_only = sorted(c for c, p in parameters_by_class_name.items() if not p.quantity_names) 

385 if presence_only: 

386 info(f"The device class check compares only the presence of these classes, " 

387 f"as their device model mappings give no totals: {', '.join(presence_only)}") 

388 return problems