Coverage for klayout_pex/rcx25/extraction_results.py: 96%
243 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-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/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#
25from __future__ import annotations
26from collections import defaultdict
27from dataclasses import dataclass, field
28from typing import *
30from .types import NetName, LayerName, CellName
31from ..klayout.lvsdb_extractor import unique_name
32from ..log import debug, error
34import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2
35import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2
36import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2
39@dataclass
40class NodeRegion:
41 layer_name: LayerName
42 net_name: NetName
43 cap_to_gnd: float
44 perimeter: float
45 area: float
48@dataclass(frozen=True)
49class SidewallKey:
50 layer: LayerName
51 net1: NetName
52 net2: NetName
55@dataclass
56class SidewallCap: # see Magic EdgeCap, extractInt.c L444
57 key: SidewallKey
58 cap_value: float # femto farad
59 distance: float # distance in µm
60 length: float # length in µm
61 tech_spec: process_parasitics_pb2.CapacitanceInfo.SidewallCapacitance
64@dataclass(frozen=True)
65class OverlapKey:
66 layer_top: LayerName
67 net_top: NetName
68 layer_bot: LayerName
69 net_bot: NetName
72@dataclass
73class OverlapCap:
74 key: OverlapKey
75 cap_value: float # femto farad
76 shielded_area: float # in µm^2
77 unshielded_area: float # in µm^2
78 tech_spec: process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance
81@dataclass(frozen=True)
82class SideOverlapKey:
83 layer_inside: LayerName
84 net_inside: NetName
85 layer_outside: LayerName
86 net_outside: NetName
88 def __repr__(self) -> str:
89 return f"{self.layer_inside}({self.net_inside})-"\
90 f"{self.layer_outside}({self.net_outside})"
92 def __post_init__(self):
93 if self.layer_inside is None:
94 raise ValueError("layer_inside cannot be None")
95 if self.net_inside is None:
96 raise ValueError("net_inside cannot be None")
97 if self.layer_outside is None:
98 raise ValueError("layer_outside cannot be None")
99 if self.net_outside is None:
100 raise ValueError("net_outside cannot be None")
103@dataclass
104class SideOverlapCap:
105 key: SideOverlapKey
106 cap_value: float # femto farad
108 def __str__(self) -> str:
109 return f"(Side Overlap): {self.key} = {round(self.cap_value, 6)}fF"
112@dataclass(frozen=True)
113class NetCoupleKey:
114 net1: NetName
115 net2: NetName
117 def __repr__(self) -> str:
118 return f"{self.net1}-{self.net2}"
120 def __lt__(self, other) -> bool:
121 if not isinstance(other, NetCoupleKey):
122 raise NotImplemented
123 return (self.net1.casefold(), self.net2.casefold()) < (other.net1.casefold(), other.net2.casefold())
125 def __post_init__(self):
126 if self.net1 is None:
127 raise ValueError("net1 cannot be None")
128 if self.net2 is None:
129 raise ValueError("net2 cannot be None")
131 # NOTE: we norm net names alphabetically
132 def normed(self) -> NetCoupleKey:
133 if self.net1 < self.net2:
134 return self
135 else:
136 return NetCoupleKey(self.net2, self.net1)
139def parallel_resistance(r1: float, r2: float) -> float:
140 if r1 == 0.0 or r2 == 0.0:
141 return 0.0
142 return r1 * r2 / (r1 + r2)
145def add_resistance(resistance_table: Dict[NetCoupleKey, float],
146 key: NetCoupleKey,
147 resistance: float):
148 # NOTE: resistances between the same pair of nodes are in parallel
149 # (unlike capacitances, they must not be summed up)
150 if key in resistance_table:
151 resistance_table[key] = parallel_resistance(resistance_table[key], resistance)
152 else:
153 resistance_table[key] = resistance
156@dataclass(frozen=True, order=True)
157class DeviceTerminalKey:
158 device_id: int
159 terminal_id: int
162# NOTE: ties the port of a label to the node of another label it's on (#211 §11),
163# not 0 Ω, which a simulator can't solve for
164LABEL_TIE_RESISTANCE = 1e-3 # Ω
167@dataclass
168class ExtractionSummary:
169 capacitances: Dict[NetCoupleKey, float]
170 resistances: Dict[NetCoupleKey, float]
172 # node of the resistor network each device terminal connects to
173 device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = field(default_factory=dict)
175 # the ports of each net of several labels, a node per label (#211 §11)
176 label_ports: Dict[NetName, List[NetName]] = field(default_factory=dict)
178 @classmethod
179 def merged(cls, summaries: List[ExtractionSummary]) -> ExtractionSummary:
180 merged_capacitances = defaultdict(float)
181 merged_resistances: Dict[NetCoupleKey, float] = {}
182 merged_device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = {}
183 merged_label_ports: Dict[NetName, List[NetName]] = {}
184 for s in summaries:
185 for couple_key, cap in s.capacitances.items():
186 merged_capacitances[couple_key.normed()] += cap
187 for couple_key, res in s.resistances.items():
188 add_resistance(merged_resistances, couple_key.normed(), res)
189 merged_device_terminal_nodes.update(s.device_terminal_nodes)
190 merged_label_ports.update(s.label_ports)
191 return ExtractionSummary(capacitances=merged_capacitances,
192 resistances=merged_resistances,
193 device_terminal_nodes=merged_device_terminal_nodes,
194 label_ports=merged_label_ports)
197@dataclass
198class NetworkNodeNames:
199 """
200 The names of the netlist nodes of a resistor network
201 """
202 by_node_id: Dict[int, NetName]
204 # the IDs of the nodes of each device terminal (its ports)
205 terminal_node_ids: Dict[DeviceTerminalKey, List[int]]
207 @classmethod
208 def from_network(cls,
209 network: r_network_pb2.RNetwork,
210 port_names: Optional[Dict[str, NetName]] = None) -> NetworkNodeNames:
211 """
212 Nodes that are one electrical node get one name: the ports of a device terminal,
213 and nodes joined by an element without resistance (which a simulator can't solve for).
215 :param port_names: the port name of each label, for a net of several labels
216 """
217 K = r_network_pb2.RNode.Kind
219 pin_labels = {n.node_name for n in network.nodes if n.node_kind == K.KIND_PIN}
221 def default_name(node: r_network_pb2.RNode) -> NetName:
222 if node.node_kind == K.KIND_PIN:
223 # NOTE: the pins of a net with one label are the node of the net,
224 # which is named after the label, unless other nets are too (e.g. vss$1, #211 §10)
225 if len(pin_labels) == 1:
226 return network.net_name
227 # NOTE: if we have an electrical short between 2 pins A and B
228 # and a parasitic resistance between the two,
229 # KLayout will call the net of both pins "A,B"
230 # but we really want the pin as the node, a port of its own (#211 §11)
231 return (port_names or {}).get(node.node_name, node.node_name)
232 if not node.net_name or ',' in node.net_name:
233 # NOTE: network prefix, as node name is only unique per network
234 return f"{network.net_name}.{node.node_name}"
235 return node.net_name
237 # NOTE: ordered by name, as the node IDs differ from run to run
238 nodes = sorted(network.nodes, key=lambda n: n.node_name)
240 terminal_node_ids: Dict[DeviceTerminalKey, List[int]] = defaultdict(list)
241 for n in nodes:
242 if n.node_kind == K.KIND_DEVICE_TERMINAL:
243 key = DeviceTerminalKey(n.device_terminal.device_id, n.device_terminal.terminal_id)
244 terminal_node_ids[key].append(n.node_id)
246 # groups of nodes that are one electrical node (union-find, by node ID)
247 parent: Dict[int, int] = {n.node_id: n.node_id for n in nodes}
249 def find(node_id: int) -> int:
250 while parent[node_id] != node_id:
251 parent[node_id] = parent[parent[node_id]]
252 node_id = parent[node_id]
253 return node_id
255 def union(node_id_a: int, node_id_b: int):
256 parent[find(node_id_a)] = find(node_id_b)
258 for node_ids in terminal_node_ids.values():
259 for node_id in node_ids[1:]:
260 union(node_id, node_ids[0])
261 for element in network.elements:
262 if element.resistance == 0.0:
263 union(element.node_a.node_id, element.node_b.node_id)
265 groups: Dict[int, List[r_network_pb2.RNode]] = defaultdict(list) # by root node ID
266 for n in nodes:
267 groups[find(n.node_id)].append(n)
269 def has_pin(root: int) -> bool:
270 return any(n.node_kind == K.KIND_PIN for n in groups[root])
272 def group_name(root: int) -> NetName:
273 pin_names = sorted({default_name(n) for n in groups[root] if n.node_kind == K.KIND_PIN})
274 if network.net_name in pin_names:
275 return network.net_name
276 if pin_names:
277 return pin_names[0]
278 # NOTE: preferably the name of a device terminal's port
279 ports = [n for n in groups[root] if n.node_kind == K.KIND_DEVICE_TERMINAL]
280 return default_name((ports or groups[root])[0])
282 name_by_root: Dict[int, NetName] = {root: group_name(root) for root in groups}
284 # NOTE: one node carries the name of the net, so that what stays connected to the net
285 # (its capacitances, device terminals without a port, e.g. a MOS bulk) is on the resistor network:
286 # a pin of that name, otherwise the first device terminal, otherwise the first node (but a pin)
287 if network.net_name not in name_by_root.values():
288 candidate_roots = [find(terminal_node_ids[key][0]) for key in sorted(terminal_node_ids)] + \
289 [find(n.node_id) for n in nodes]
290 root = next((r for r in candidate_roots if not has_pin(r)), None)
291 if root is not None:
292 name_by_root[root] = network.net_name
294 return cls(by_node_id={n.node_id: name_by_root[find(n.node_id)] for n in nodes},
295 terminal_node_ids=terminal_node_ids)
298@dataclass
299class CellExtractionResults:
300 cell_name: CellName
302 overlap_table: Dict[OverlapKey, List[OverlapCap]] = field(default_factory=lambda: defaultdict(list))
303 sidewall_table: Dict[SidewallKey, List[SidewallCap]] = field(default_factory=lambda: defaultdict(list))
304 sideoverlap_table: Dict[SideOverlapKey, List[SideOverlapCap]] = field(default_factory=lambda: defaultdict(list))
306 r_extraction_result: pex_result_pb2.RExtractionResult = field(default_factory=lambda: pex_result_pb2.RExtractionResult())
308 def add_overlap_cap(self, cap: OverlapCap):
309 self.overlap_table[cap.key].append(cap)
311 def add_sidewall_cap(self, cap: SidewallCap):
312 self.sidewall_table[cap.key].append(cap)
314 def add_sideoverlap_cap(self, cap: SideOverlapCap):
315 self.sideoverlap_table[cap.key].append(cap)
317 def label_port_names(self) -> Dict[NetName, Dict[str, NetName]]:
318 """
319 The port names of the nets of several labels, by label
321 NOTE: KLayout names a net of several labels after all of them (e.g. "A,B"),
322 but each label is a port of its own (#211 §11), named after the label,
323 unless a net or another port is (e.g. A$1, like KLayout's SPICE writer)
324 """
325 K = r_network_pb2.RNode.Kind
326 networks = sorted(self.r_extraction_result.networks, key=lambda n: n.net_name)
327 taken_names: Set[NetName] = {network.net_name for network in networks}
328 port_names_by_net: Dict[NetName, Dict[str, NetName]] = {}
329 for network in networks:
330 labels = sorted({n.node_name for n in network.nodes if n.node_kind == K.KIND_PIN})
331 if len(labels) < 2:
332 continue
333 port_names: Dict[str, NetName] = {}
334 for label in labels:
335 port_names[label] = unique_name(label, taken_names)
336 taken_names.add(port_names[label])
337 port_names_by_net[network.net_name] = port_names
338 return port_names_by_net
340 def summarize(self) -> ExtractionSummary:
341 normalized_overlap_table: Dict[NetCoupleKey, float] = defaultdict(float)
342 for key, entries in self.overlap_table.items():
343 normalized_key = NetCoupleKey(key.net_bot, key.net_top).normed()
344 normalized_overlap_table[normalized_key] += sum((e.cap_value for e in entries))
345 overlap_summary = ExtractionSummary(capacitances=normalized_overlap_table,
346 resistances={})
348 normalized_sidewall_table: Dict[NetCoupleKey, float] = defaultdict(float)
349 for key, entries in self.sidewall_table.items():
350 normalized_key = NetCoupleKey(key.net1, key.net2).normed()
351 normalized_sidewall_table[normalized_key] += sum((e.cap_value for e in entries))
352 sidewall_summary = ExtractionSummary(capacitances=normalized_sidewall_table,
353 resistances={})
355 normalized_sideoverlap_table: Dict[NetCoupleKey, float] = defaultdict(float)
356 for key, entries in self.sideoverlap_table.items():
357 normalized_key = NetCoupleKey(key.net_inside, key.net_outside).normed()
358 normalized_sideoverlap_table[normalized_key] += sum((e.cap_value for e in entries))
359 sideoverlap_summary = ExtractionSummary(capacitances=normalized_sideoverlap_table,
360 resistances={})
362 normalized_resistance_table: Dict[NetCoupleKey, float] = {}
363 device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = {}
365 port_names_by_net = self.label_port_names()
367 for network in self.r_extraction_result.networks:
368 port_names = port_names_by_net.get(network.net_name, {})
369 node_names = NetworkNodeNames.from_network(network, port_names)
370 for terminal_key, node_ids in node_names.terminal_node_ids.items():
371 device_terminal_nodes[terminal_key] = node_names.by_node_id[node_ids[0]]
373 # NOTE: the port of a label on the node of another label (e.g. both on one via) is tied to it
374 ties = {NetCoupleKey(port_names[n.node_name], node_names.by_node_id[n.node_id]).normed()
375 for n in network.nodes
376 if n.node_kind == r_network_pb2.RNode.Kind.KIND_PIN
377 and n.node_name in port_names
378 and port_names[n.node_name] != node_names.by_node_id[n.node_id]}
379 for key in sorted(ties):
380 add_resistance(normalized_resistance_table, key, LABEL_TIE_RESISTANCE)
382 for element in network.elements:
383 resistance = element.resistance
384 normalized_key = NetCoupleKey(node_names.by_node_id[element.node_a.node_id],
385 node_names.by_node_id[element.node_b.node_id]).normed()
386 # NOTE: different nodes can have the same name, e.g. pins with the same label,
387 # which are one node in the netlist, so their elements are in parallel,
388 # and an element between two of them is shorted
389 if normalized_key.net1 == normalized_key.net2:
390 debug(f"Skipping resistor between nodes of the same name {normalized_key.net1} "
391 f"with value {'%.12g' % resistance}")
392 continue
393 add_resistance(normalized_resistance_table, normalized_key, resistance)
395 resistance_summary = ExtractionSummary(capacitances={},
396 resistances=normalized_resistance_table,
397 device_terminal_nodes=device_terminal_nodes,
398 label_ports={net_name: [port_names[label] for label in sorted(port_names)]
399 for net_name, port_names in port_names_by_net.items()})
401 return ExtractionSummary.merged([
402 overlap_summary, sidewall_summary, sideoverlap_summary,
403 resistance_summary
404 ])
407@dataclass
408class ExtractionResults:
409 cell_extraction_results: Dict[CellName, CellExtractionResults] = field(default_factory=dict)
411 def summarize(self) -> ExtractionSummary:
412 subsummaries = [s.summarize() for s in self.cell_extraction_results.values()]
413 return ExtractionSummary.merged(subsummaries)