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
« 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)
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"""
32from __future__ import annotations
34from collections import Counter, defaultdict
35from typing import *
37import klayout.db as kdb
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)
50NET_ID_PROPERTY = 'kpex_check_net_id'
53class RCNetlistCheckError(Exception):
54 """
55 The RC netlist is inconsistent with the LVS netlist (see check_rc_netlist)
56 """
57 pass
59# NOTE: relative, the capacitances of the summary and the netlist differ by rounding only
60CAPACITANCE_TOLERANCE = 1e-9
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'}"
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)
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"]
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)
98 nets = list(rc_circuit.each_net())
99 for net_id, net in enumerate(nets):
100 net.set_property(NET_ID_PROPERTY, net_id)
102 def id_of(net: kdb.Net) -> int:
103 return net.property(NET_ID_PROPERTY)
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 }
113 problems: List[str] = []
115 # the pieces of the netlist, DC-connected by the parasitic resistors (union-find by net ID)
116 parent = list(range(len(nets)))
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
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))
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()}")
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())
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")
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)}")
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()}")
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")
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")
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}
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)
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
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)
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
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")
260 return problems