Coverage for klayout_pex/rcx25/extraction_results.py: 98%
400 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +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, replace
28from typing import *
30import klayout.db as kdb
32from .capacitance_distributor import CapacitanceDistributor, NodePair
33from .exclusion import Exclusions, edge_part
34from ..types import CellName, LayerName, NetName
35from ..klayout.lvsdb_extractor import unique_name
36from ..log import debug, error
37from ..util.disjoint_sets import DisjointSets
39import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2
40import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2
41import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2
44@dataclass
45class NodeRegion:
46 layer_name: LayerName
47 net_name: NetName
48 cap_to_gnd: float
49 perimeter: float
50 area: float
53@dataclass(frozen=True)
54class SidewallKey:
55 layer: LayerName
56 net1: NetName
57 net2: NetName
60@dataclass
61class SidewallCap: # see Magic EdgeCap, extractInt.c L444
62 key: SidewallKey
63 cap_value: float # femto farad
64 distance: float # distance in µm
65 length: float # length in µm
66 tech_spec: process_parasitics_pb2.CapacitanceInfo.SidewallCapacitance
68 # the edge of net1, and the one of net2 facing it (same direction), to distribute the capacitance (in DBU)
69 inside_edge: Optional[kdb.Edge] = None
70 outside_edge: Optional[kdb.Edge] = None
73@dataclass(frozen=True)
74class OverlapKey:
75 layer_top: LayerName
76 net_top: NetName
77 layer_bot: LayerName
78 net_bot: NetName
81@dataclass
82class OverlapCap:
83 key: OverlapKey
84 cap_value: float # femto farad
85 shielded_area: float # in µm^2
86 unshielded_area: float # in µm^2
87 tech_spec: process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance
89 # the overlap of both nets, to distribute the capacitance (in DBU)
90 area: Optional[kdb.Region] = None
93@dataclass(frozen=True)
94class SideOverlapKey:
95 layer_inside: LayerName
96 net_inside: NetName
97 layer_outside: LayerName
98 net_outside: NetName
100 def __repr__(self) -> str:
101 return f"{self.layer_inside}({self.net_inside})-"\
102 f"{self.layer_outside}({self.net_outside})"
104 def __post_init__(self):
105 if self.layer_inside is None:
106 raise ValueError("layer_inside cannot be None")
107 if self.net_inside is None:
108 raise ValueError("net_inside cannot be None")
109 if self.layer_outside is None:
110 raise ValueError("layer_outside cannot be None")
111 if self.net_outside is None:
112 raise ValueError("net_outside cannot be None")
115@dataclass
116class SideOverlapCap:
117 key: SideOverlapKey
118 cap_value: float # femto farad
120 # the edge of the inside net, and the middle of the polygon of the outside net along it (same direction),
121 # to distribute the capacitance (in DBU)
122 inside_edge: Optional[kdb.Edge] = None
123 outside_edge: Optional[kdb.Edge] = None
125 # the near side of that polygon along it, which tells the part of the outside net at exclusion blocks,
126 # as the fringe mostly ends there, also of a polygon that goes on across a block (see kept_edge_caps)
127 near_edge: Optional[kdb.Edge] = None
129 def __str__(self) -> str:
130 return f"(Side Overlap): {self.key} = {round(self.cap_value, 6)}fF"
133@dataclass(frozen=True)
134class NetCoupleKey:
135 net1: NetName
136 net2: NetName
138 def __repr__(self) -> str:
139 return f"{self.net1}-{self.net2}"
141 def __lt__(self, other) -> bool:
142 if not isinstance(other, NetCoupleKey):
143 raise NotImplemented
144 return (self.net1.casefold(), self.net2.casefold()) < (other.net1.casefold(), other.net2.casefold())
146 def __post_init__(self):
147 if self.net1 is None:
148 raise ValueError("net1 cannot be None")
149 if self.net2 is None:
150 raise ValueError("net2 cannot be None")
152 # NOTE: we norm net names alphabetically
153 def normed(self) -> NetCoupleKey:
154 if self.net1 < self.net2:
155 return self
156 else:
157 return NetCoupleKey(self.net2, self.net1)
160def parallel_resistance(r1: float, r2: float) -> float:
161 if r1 == 0.0 or r2 == 0.0:
162 return 0.0
163 return r1 * r2 / (r1 + r2)
166def add_resistance(resistance_table: Dict[NetCoupleKey, float],
167 key: NetCoupleKey,
168 resistance: float):
169 # NOTE: resistances between the same pair of nodes are in parallel
170 # (unlike capacitances, they must not be summed up)
171 if key in resistance_table:
172 resistance_table[key] = parallel_resistance(resistance_table[key], resistance)
173 else:
174 resistance_table[key] = resistance
177def add_distributed_capacitance(capacitance_table: Dict[NetCoupleKey, float],
178 cap_value: float,
179 fractions: Dict[NodePair, float]):
180 # NOTE: capacitances between the same pair of nodes are in parallel,
181 # and the ones between parts of a net with ports at exclusion markers can end on one node (e.g. at a cut)
182 for (node_a, node_b), fraction in fractions.items():
183 if node_a != node_b:
184 capacitance_table[NetCoupleKey(node_a, node_b).normed()] += cap_value * fraction
187# NOTE: a capacitance between nodes of resistor networks below this goes to a larger one between the same nets,
188# to keep the netlist small (like the minimum of the fringe capacitances, but keeping the sum)
189MIN_NODE_CAPACITANCE = 1e-3 # fF
192def merged_small_capacitances(capacitances: Dict[NetCoupleKey, float],
193 net_by_node: Dict[NetName, NetName],
194 min_capacitance: float) -> Dict[NetCoupleKey, float]:
195 """
196 Each capacitance below the minimum goes to a larger one between the same nets,
197 preferably one on a node of it, so that the capacitance between each pair of nets stays the same
199 :param net_by_node: the net of each node of the resistor networks
200 """
201 keys_by_nets: Dict[NetCoupleKey, List[NetCoupleKey]] = defaultdict(list)
202 for key in sorted(capacitances):
203 nets = NetCoupleKey(net_by_node.get(key.net1, key.net1), net_by_node.get(key.net2, key.net2)).normed()
204 keys_by_nets[nets].append(key)
206 merged: Dict[NetCoupleKey, float] = {}
207 for keys in keys_by_nets.values():
208 # NOTE: the largest one stays, if all are small
209 large = [k for k in keys if capacitances[k] >= min_capacitance] or [max(keys, key=capacitances.get)]
210 for key in large:
211 merged[key] = capacitances[key]
212 for key in keys:
213 if key in merged:
214 continue
215 neighbors = [k for k in large if {k.net1, k.net2} & {key.net1, key.net2}] or large
216 merged[max(neighbors, key=capacitances.get)] += capacitances[key]
217 return merged
220@dataclass(frozen=True, order=True)
221class DeviceTerminalKey:
222 device_id: int
223 terminal_id: int
226# NOTE: ties the port of a label to the node of another label it's on (#211 §11),
227# or a net of labels only to the node of its first label (see NetworkNodeNames.net_tie_node_id),
228# not 0 Ω, which a simulator can't solve for
229LABEL_TIE_RESISTANCE = 1e-3 # Ω
232@dataclass
233class ExtractionSummary:
234 capacitances: Dict[NetCoupleKey, float]
235 resistances: Dict[NetCoupleKey, float]
237 # node of the resistor network each device terminal connects to
238 device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = field(default_factory=dict)
240 # the ports of each net of several labels, a node per label (#211 §11),
241 # or with ports at exclusion markers, which are among them
242 label_ports: Dict[NetName, List[NetName]] = field(default_factory=dict)
244 # the ports of each net at exclusion markers, for a model that joins the pieces of the net
245 exclusion_ports: Dict[NetName, List[NetName]] = field(default_factory=dict)
247 @classmethod
248 def merged(cls, summaries: List[ExtractionSummary]) -> ExtractionSummary:
249 merged_capacitances = defaultdict(float)
250 merged_resistances: Dict[NetCoupleKey, float] = {}
251 merged_device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = {}
252 merged_label_ports: Dict[NetName, List[NetName]] = {}
253 merged_exclusion_ports: Dict[NetName, List[NetName]] = {}
254 for s in summaries:
255 for couple_key, cap in s.capacitances.items():
256 merged_capacitances[couple_key.normed()] += cap
257 for couple_key, res in s.resistances.items():
258 add_resistance(merged_resistances, couple_key.normed(), res)
259 merged_device_terminal_nodes.update(s.device_terminal_nodes)
260 merged_label_ports.update(s.label_ports)
261 merged_exclusion_ports.update(s.exclusion_ports)
262 return ExtractionSummary(capacitances=merged_capacitances,
263 resistances=merged_resistances,
264 device_terminal_nodes=merged_device_terminal_nodes,
265 label_ports=merged_label_ports,
266 exclusion_ports=merged_exclusion_ports)
269@dataclass
270class NetworkNodeNames:
271 """
272 The names of the netlist nodes of a resistor network
273 """
274 by_node_id: Dict[int, NetName]
276 # the IDs of the nodes of each device terminal (its ports)
277 terminal_node_ids: Dict[DeviceTerminalKey, List[int]]
279 # the IDs of the nodes tied to the net, one per piece of the network if every node of the piece is a pin's,
280 # so that none carries its name (e.g. a wire between the labels A and B of the net A,B)
281 net_tie_node_ids: List[int] = field(default_factory=list)
283 # whether only the substrate, a well or a tap joins pieces of the network to the net (see from_network)
284 joined_by_substrate: bool = False
286 @classmethod
287 def from_network(cls,
288 network: r_network_pb2.RNetwork,
289 port_names: Optional[Dict[str, NetName]] = None) -> NetworkNodeNames:
290 """
291 Nodes that are one electrical node get one name: the ports of a device terminal,
292 and nodes joined by an element without resistance (which a simulator can't solve for).
294 :param port_names: the port name of each label, for a net of several labels
295 """
296 K = r_network_pb2.RNode.Kind
298 pin_labels = {n.node_name for n in network.nodes if n.node_kind == K.KIND_PIN}
300 def is_port(node: r_network_pb2.RNode) -> bool:
301 return node.node_kind in (K.KIND_PIN, K.KIND_EXCLUSION_PORT)
303 def default_name(node: r_network_pb2.RNode) -> NetName:
304 if node.node_kind == K.KIND_EXCLUSION_PORT:
305 return (port_names or {}).get(node.node_name, node.node_name)
306 if node.node_kind == K.KIND_PIN:
307 # NOTE: the pins of a net with one label are the node of the net,
308 # which is named after the label, unless other nets are too (e.g. vss$1, #211 §10),
309 # or it has ports at exclusion blocks as well, whose labels may be in its name
310 if len(pin_labels) == 1 and node.node_name not in (port_names or {}):
311 return network.net_name
312 # NOTE: if we have an electrical short between 2 pins A and B
313 # and a parasitic resistance between the two,
314 # KLayout will call the net of both pins "A,B"
315 # but we really want the pin as the node, a port of its own (#211 §11)
316 return (port_names or {}).get(node.node_name, node.node_name)
317 if not node.net_name or ',' in node.net_name:
318 # NOTE: network prefix, as node name is only unique per network
319 return f"{network.net_name}.{node.node_name}"
320 return node.net_name
322 # NOTE: ordered by name, as the node IDs differ from run to run
323 nodes = sorted(network.nodes, key=lambda n: n.node_name)
325 terminal_node_ids: Dict[DeviceTerminalKey, List[int]] = defaultdict(list)
326 for n in nodes:
327 if n.node_kind == K.KIND_DEVICE_TERMINAL:
328 key = DeviceTerminalKey(n.device_terminal.device_id, n.device_terminal.terminal_id)
329 terminal_node_ids[key].append(n.node_id)
331 # groups of nodes that are one electrical node (by node ID)
332 electrical_nodes = DisjointSets(n.node_id for n in nodes)
333 find = electrical_nodes.find
335 for node_ids in terminal_node_ids.values():
336 for node_id in node_ids[1:]:
337 electrical_nodes.union(node_id, node_ids[0])
338 for element in network.elements:
339 if element.resistance == 0.0:
340 electrical_nodes.union(element.node_a.node_id, element.node_b.node_id)
342 groups: Dict[int, List[r_network_pb2.RNode]] = defaultdict(list) # by root node ID
343 for n in nodes:
344 groups[find(n.node_id)].append(n)
346 def has_pin(root: int) -> bool:
347 return any(is_port(n) for n in groups[root])
349 def group_name(root: int) -> NetName:
350 pin_names = sorted({default_name(n) for n in groups[root] if is_port(n)})
351 if network.net_name in pin_names:
352 return network.net_name
353 if pin_names:
354 return pin_names[0]
355 # NOTE: preferably the name of a device terminal's port
356 ports = [n for n in groups[root] if n.node_kind == K.KIND_DEVICE_TERMINAL]
357 return default_name((ports or groups[root])[0])
359 name_by_root: Dict[int, NetName] = {root: group_name(root) for root in groups}
361 # NOTE: one node carries the name of the net, so that what stays connected to the net
362 # (its capacitances, device terminals without a port, e.g. a MOS bulk) is on the resistor network:
363 # a pin of that name, otherwise the first device terminal, otherwise the first node (but a pin)
364 # or, if every node is a pin's, the first node is tied to the net (see summarize),
365 # rather than leaving the net off the resistor network
366 net_tie_node_ids: List[int] = []
368 def put_on_net(candidate_roots: List[int]):
369 root = next((r for r in candidate_roots if not has_pin(r)), None)
370 if root is not None:
371 name_by_root[root] = network.net_name
372 elif candidate_roots:
373 # NOTE: a pin of the top cell rather than a port for a model, which a model may leave open
374 net_tie_node_ids.append(next((root for root in candidate_roots
375 if any(n.node_kind == K.KIND_PIN for n in groups[root])),
376 candidate_roots[0]))
378 candidate_roots = [find(terminal_node_ids[key][0]) for key in sorted(terminal_node_ids)] + \
379 [find(n.node_id) for n in nodes]
381 # NOTE: the taps, the substrate and the wells joining pieces of the network are no conductors of it, they are
382 # ideal, like for the device terminals on them, so all that lands on them is one node, the net's.
383 # Each such piece gets one node on the net, by the rule above, among its nodes at the tap or on the well:
384 # - no resistor, as the resistance through the substrate is unknown, and any value would be made up
385 # - one node per piece, so no parallel path shunts the resistances within the piece
386 # The other pieces have no path to their net, which check_rc_netlist reports
387 joined_by_substrate = False
388 if network.substrate_joined_node_ids:
389 pieces = DisjointSets(n.node_id for n in nodes)
390 for element in network.elements:
391 pieces.union(element.node_a.node_id, element.node_b.node_id)
392 # NOTE: nodes of the same name are one node in the netlist, e.g. the pins of one label on 2 pieces
393 first_node_id_by_name: Dict[NetName, int] = {}
394 for n in nodes:
395 name = name_by_root[find(n.node_id)]
396 pieces.union(n.node_id, first_node_id_by_name.setdefault(name, n.node_id))
397 # NOTE: so does the model of an exclusion block, which an ideal node of each side would bypass
398 for join in network.model_joins:
399 for node_id in join.node_ids[1:]:
400 pieces.union(join.node_ids[0], node_id)
402 pieces_on_net = {pieces.find(n.node_id) for n in nodes if name_by_root[find(n.node_id)] == network.net_name}
403 joined_pieces = {pieces.find(node_id) for node_id in network.substrate_joined_node_ids}
404 joined_by_substrate = len(pieces_on_net | joined_pieces) > 1 and not joined_pieces <= pieces_on_net
406 joined_roots = {find(node_id) for node_id in network.substrate_joined_node_ids}
407 candidate_roots_by_piece: Dict[int, List[int]] = defaultdict(list)
408 for root in candidate_roots:
409 if root in joined_roots:
410 candidate_roots_by_piece[pieces.find(root)].append(root)
411 for piece, piece_candidate_roots in candidate_roots_by_piece.items():
412 if piece not in pieces_on_net:
413 put_on_net(piece_candidate_roots)
415 if network.net_name not in name_by_root.values() and not net_tie_node_ids:
416 put_on_net(candidate_roots)
418 return cls(by_node_id={n.node_id: name_by_root[find(n.node_id)] for n in nodes},
419 terminal_node_ids=terminal_node_ids,
420 net_tie_node_ids=net_tie_node_ids,
421 joined_by_substrate=joined_by_substrate)
424@dataclass
425class CellExtractionResults:
426 cell_name: CellName
428 # NOTE: the capacitances keep their geometry (OverlapCap.area, SidewallCap.inside_edge, ...) only if asked to,
429 # e.g. to place them on the resistor network (RC mode), as it takes memory on large layouts
430 keep_capacitance_geometry: bool = False
432 # NOTE: the substrate has no nodes of its own, so its side of a capacitance goes to the nodes of its wells
433 # (see CapacitanceDistributor.sites)
434 substrate_layer_name: Optional[LayerName] = None
435 substrate_well_layer_names: Set[LayerName] = field(default_factory=set)
437 # NOTE: the capacitances that a model of what they mark has are left out (see exclusion.py)
438 exclusions: Optional[Exclusions] = None
440 overlap_table: Dict[OverlapKey, List[OverlapCap]] = field(default_factory=lambda: defaultdict(list))
441 sidewall_table: Dict[SidewallKey, List[SidewallCap]] = field(default_factory=lambda: defaultdict(list))
442 sideoverlap_table: Dict[SideOverlapKey, List[SideOverlapCap]] = field(default_factory=lambda: defaultdict(list))
444 r_extraction_result: pex_result_pb2.RExtractionResult = field(default_factory=lambda: pex_result_pb2.RExtractionResult())
446 def net_of_capacitance(self, net_name: NetName) -> NetName:
447 return self.exclusions.net_of_capacitance(net_name, self.substrate_layer_name)
449 def has_net_pieces(self, net_name: NetName) -> bool:
450 """
451 :return: whether the capacitances within the net count, as parts of it are apart in the netlist (see NetPiece)
452 """
453 return self.exclusions is not None and self.exclusions.has_pieces(net_name)
455 @staticmethod
456 def are_apart(net_a: NetName, part_a: NetName, net_b: NetName, part_b: NetName) -> bool:
457 """
458 :param net_a: the net of a plate
459 :param part_a: its part of the net, a piece or the net itself
460 :param net_b: the net of the other plate
461 :param part_b: its part of the net, a piece or the net itself
462 :return: whether the netlist has the plates apart
463 """
464 return net_a != net_b or (part_a != net_a and part_b != net_b and part_a != part_b)
466 def kept_overlap_caps(self, cap: OverlapCap) -> List[OverlapCap]:
467 """
468 :return: the parts of the overlap capacitance that the exclusion markers don't leave out, by their nets
469 """
470 exclusions = self.exclusions
471 if exclusions is None:
472 return [cap]
474 key = cap.key
475 if exclusions.leaves_out_capacitance_of_net(key.net_top, key.net_bot):
476 cap.cap_value = 0.0
477 return []
479 capacitance_per_area = cap.cap_value / cap.area.area()
480 kept_area = cap.area - exclusions.excluded_area(cap.area, key.layer_top, key.layer_bot)
482 parts: List[OverlapCap] = []
483 top_areas = [] if kept_area.is_empty() else exclusions.area_parts(kept_area, key.net_top, key.layer_top)
484 for top_area, part_top in top_areas:
485 for area, part_bot in exclusions.area_parts(top_area, key.net_bot, key.layer_bot):
486 net_top, net_bot = self.net_of_capacitance(part_top), self.net_of_capacitance(part_bot)
487 if net_top != net_bot and self.are_apart(key.net_top, part_top, key.net_bot, part_bot):
488 parts.append(replace(cap,
489 key=replace(key, net_top=net_top, net_bot=net_bot),
490 cap_value=capacitance_per_area * area.area(),
491 area=area))
492 cap.cap_value = sum(part.cap_value for part in parts) # NOTE: also for the report, which has the capacitance
493 return parts
495 def add_overlap_cap(self, cap: OverlapCap):
496 for part in self.kept_overlap_caps(cap):
497 if not self.keep_capacitance_geometry:
498 part.area = None
499 self.overlap_table[part.key].append(part)
501 def kept_edge_caps(self,
502 cap: SidewallCap | SideOverlapCap,
503 inside_net_name: NetName,
504 inside_layer_name: LayerName,
505 outside_net_name: NetName,
506 outside_layer_name: LayerName,
507 facing_edge: kdb.Edge) -> List[Tuple[SidewallCap | SideOverlapCap, NetName, NetName]]:
508 """
509 :param facing_edge: of the outside net, facing the inside edge (same direction), which tells its part
510 :return: the parts of the capacitance between two edges that the exclusion markers don't leave out,
511 with their nets at the inside edge and at the outside edge
512 """
513 exclusions = self.exclusions
514 if exclusions is None:
515 return [(cap, inside_net_name, outside_net_name)]
517 if exclusions.leaves_out_capacitance_of_net(inside_net_name, outside_net_name):
518 cap.cap_value = 0.0
519 return []
521 inside_parts = exclusions.edge_parts(cap.inside_edge, -1, inside_net_name, inside_layer_name)
522 outside_parts = exclusions.edge_parts(facing_edge, 1, outside_net_name, outside_layer_name)
524 parts: List[Tuple[SidewallCap | SideOverlapCap, NetName, NetName]] = []
525 for kept in exclusions.kept_edge_parts(cap.inside_edge, inside_layer_name, facing_edge, outside_layer_name):
526 for inside, part_inside in inside_parts:
527 for outside, part_outside in outside_parts:
528 interval = max(kept[0], inside[0], outside[0]), min(kept[1], inside[1], outside[1])
529 net_inside = self.net_of_capacitance(part_inside)
530 net_outside = self.net_of_capacitance(part_outside)
531 if interval[0] < interval[1] and net_inside != net_outside and \
532 self.are_apart(inside_net_name, part_inside, outside_net_name, part_outside):
533 parts.append((replace(cap,
534 cap_value=cap.cap_value * (interval[1] - interval[0]),
535 inside_edge=edge_part(cap.inside_edge, interval),
536 outside_edge=edge_part(cap.outside_edge, interval)),
537 net_inside, net_outside))
538 cap.cap_value = sum(part[0].cap_value for part in parts) # NOTE: also for the report, which has the capacitance
539 return parts
541 def add_sidewall_cap(self, cap: SidewallCap):
542 for part, net1, net2 in self.kept_edge_caps(cap, cap.key.net1, cap.key.layer, cap.key.net2, cap.key.layer,
543 cap.outside_edge):
544 if self.exclusions is not None:
545 part.key = replace(part.key, net1=net1, net2=net2)
546 if not self.keep_capacitance_geometry:
547 part.inside_edge = part.outside_edge = None
548 self.sidewall_table[part.key].append(part)
550 def add_sideoverlap_cap(self, cap: SideOverlapCap):
551 for part, net_inside, net_outside in self.kept_edge_caps(cap,
552 cap.key.net_inside, cap.key.layer_inside,
553 cap.key.net_outside, cap.key.layer_outside,
554 cap.near_edge):
555 if self.exclusions is not None:
556 part.key = replace(part.key, net_inside=net_inside, net_outside=net_outside)
557 part.near_edge = None
558 if not self.keep_capacitance_geometry:
559 part.inside_edge = part.outside_edge = None
560 self.sideoverlap_table[part.key].append(part)
562 def label_port_names(self) -> Dict[NetName, Dict[str, NetName]]:
563 """
564 The port names of the nets of several labels, by label
566 NOTE: KLayout names a net of several labels after all of them (e.g. "A,B"),
567 but each label is a port of its own (#211 §11), named after the label,
568 unless a net or another port is, regardless of case (e.g. A$1, like KLayout's SPICE writer)
569 """
570 K = r_network_pb2.RNode.Kind
571 networks = sorted(self.r_extraction_result.networks, key=lambda n: n.net_name)
572 taken_names: Set[NetName] = {network.net_name.casefold() for network in networks} # NOTE: case-folded
573 port_names_by_net: Dict[NetName, Dict[str, NetName]] = {}
574 for network in networks:
575 labels = {n.node_name for n in network.nodes if n.node_kind == K.KIND_PIN}
576 exclusion_ports = {n.node_name for n in network.nodes if n.node_kind == K.KIND_EXCLUSION_PORT}
577 if len(labels) < 2 and not exclusion_ports:
578 continue
579 port_names: Dict[str, NetName] = {}
580 for label in sorted(labels | exclusion_ports):
581 # NOTE: the one label of a net with ports at exclusion markers names the net too
582 if label == network.net_name:
583 port_names[label] = label
584 continue
585 port_names[label] = unique_name(label, taken_names)
586 taken_names.add(port_names[label].casefold())
587 port_names_by_net[network.net_name] = port_names
588 return port_names_by_net
590 def nets_joined_by_substrate(self) -> List[NetName]:
591 """
592 :return: the nets with pieces of their resistor networks that only the substrate, a well or a tap joins
593 """
594 port_names_by_net = self.label_port_names()
595 return sorted(network.net_name
596 for network in self.r_extraction_result.networks
597 if network.substrate_joined_node_ids
598 and NetworkNodeNames.from_network(network,
599 port_names_by_net.get(network.net_name, {})).joined_by_substrate)
601 def summarize(self) -> ExtractionSummary:
602 port_names_by_net = self.label_port_names()
603 node_names_by_net: Dict[NetName, NetworkNodeNames] = {
604 network.net_name: NetworkNodeNames.from_network(network, port_names_by_net.get(network.net_name, {}))
605 for network in self.r_extraction_result.networks
606 }
608 # NOTE: the capacitances of a net with a resistor network go to its nodes nearest to them,
609 # rather than to the net (#211 §8)
610 distributor = CapacitanceDistributor.from_networks(
611 ((network, node_names_by_net[network.net_name].by_node_id)
612 for network in self.r_extraction_result.networks),
613 substrate_layer_name=self.substrate_layer_name,
614 substrate_well_layer_names=self.substrate_well_layer_names
615 )
617 normalized_overlap_table: Dict[NetCoupleKey, float] = defaultdict(float)
618 for key, entries in self.overlap_table.items():
619 if distributor.has_nodes(key.net_bot) or distributor.has_nodes(key.net_top):
620 for e in entries:
621 add_distributed_capacitance(normalized_overlap_table, e.cap_value,
622 distributor.area_fractions(key.net_bot, key.layer_bot,
623 key.net_top, key.layer_top,
624 e.area))
625 continue
626 normalized_key = NetCoupleKey(key.net_bot, key.net_top).normed()
627 normalized_overlap_table[normalized_key] += sum((e.cap_value for e in entries))
628 overlap_summary = ExtractionSummary(capacitances=normalized_overlap_table,
629 resistances={})
631 normalized_sidewall_table: Dict[NetCoupleKey, float] = defaultdict(float)
632 for key, entries in self.sidewall_table.items():
633 if distributor.has_nodes(key.net1) or distributor.has_nodes(key.net2):
634 for e in entries:
635 add_distributed_capacitance(normalized_sidewall_table, e.cap_value,
636 distributor.edge_fractions(key.net1, key.layer, e.inside_edge,
637 key.net2, key.layer, e.outside_edge))
638 continue
639 normalized_key = NetCoupleKey(key.net1, key.net2).normed()
640 normalized_sidewall_table[normalized_key] += sum((e.cap_value for e in entries))
641 sidewall_summary = ExtractionSummary(capacitances=normalized_sidewall_table,
642 resistances={})
644 normalized_sideoverlap_table: Dict[NetCoupleKey, float] = defaultdict(float)
645 for key, entries in self.sideoverlap_table.items():
646 if distributor.has_nodes(key.net_inside) or distributor.has_nodes(key.net_outside):
647 for e in entries:
648 add_distributed_capacitance(normalized_sideoverlap_table, e.cap_value,
649 distributor.edge_fractions(key.net_inside, key.layer_inside,
650 e.inside_edge,
651 key.net_outside, key.layer_outside,
652 e.outside_edge))
653 continue
654 normalized_key = NetCoupleKey(key.net_inside, key.net_outside).normed()
655 normalized_sideoverlap_table[normalized_key] += sum((e.cap_value for e in entries))
656 sideoverlap_summary = ExtractionSummary(capacitances=normalized_sideoverlap_table,
657 resistances={})
659 normalized_resistance_table: Dict[NetCoupleKey, float] = {}
660 device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = {}
661 exclusion_ports: Dict[NetName, List[NetName]] = {}
663 for network in self.r_extraction_result.networks:
664 port_names = port_names_by_net.get(network.net_name, {})
665 node_names = node_names_by_net[network.net_name]
666 for terminal_key, node_ids in node_names.terminal_node_ids.items():
667 device_terminal_nodes[terminal_key] = node_names.by_node_id[node_ids[0]]
669 K = r_network_pb2.RNode.Kind
670 net_exclusion_ports = sorted({port_names[n.node_name] for n in network.nodes
671 if n.node_kind == K.KIND_EXCLUSION_PORT})
672 if net_exclusion_ports:
673 exclusion_ports[network.net_name] = net_exclusion_ports
675 # NOTE: the port of a label on the node of another label (e.g. both on one via) is tied to it
676 ties = {NetCoupleKey(port_names[n.node_name], node_names.by_node_id[n.node_id]).normed()
677 for n in network.nodes
678 if n.node_kind in (K.KIND_PIN, K.KIND_EXCLUSION_PORT)
679 and n.node_name in port_names
680 and port_names[n.node_name] != node_names.by_node_id[n.node_id]}
681 ties.update(NetCoupleKey(network.net_name, node_names.by_node_id[node_id]).normed()
682 for node_id in node_names.net_tie_node_ids)
683 for key in sorted(ties):
684 add_resistance(normalized_resistance_table, key, LABEL_TIE_RESISTANCE)
686 for element in network.elements:
687 resistance = element.resistance
688 normalized_key = NetCoupleKey(node_names.by_node_id[element.node_a.node_id],
689 node_names.by_node_id[element.node_b.node_id]).normed()
690 # NOTE: different nodes can have the same name, e.g. pins with the same label,
691 # which are one node in the netlist, so their elements are in parallel,
692 # and an element between two of them is shorted
693 if normalized_key.net1 == normalized_key.net2:
694 debug(f"Skipping resistor between nodes of the same name {normalized_key.net1} "
695 f"with value {'%.12g' % resistance}")
696 continue
697 add_resistance(normalized_resistance_table, normalized_key, resistance)
699 resistance_summary = ExtractionSummary(capacitances={},
700 resistances=normalized_resistance_table,
701 device_terminal_nodes=device_terminal_nodes,
702 label_ports={net_name: [port_names[label] for label in sorted(port_names)]
703 for net_name, port_names in port_names_by_net.items()},
704 exclusion_ports=exclusion_ports)
706 summary = ExtractionSummary.merged([
707 overlap_summary, sidewall_summary, sideoverlap_summary,
708 resistance_summary
709 ])
711 if node_names_by_net:
712 net_by_node = {node_name: net_name
713 for net_name, node_names in node_names_by_net.items()
714 for node_name in node_names.by_node_id.values()}
715 summary.capacitances = merged_small_capacitances(summary.capacitances, net_by_node, MIN_NODE_CAPACITANCE)
717 return summary
720@dataclass
721class ExtractionResults:
722 cell_extraction_results: Dict[CellName, CellExtractionResults] = field(default_factory=dict)
724 def summarize(self) -> ExtractionSummary:
725 subsummaries = [s.summarize() for s in self.cell_extraction_results.values()]
726 return ExtractionSummary.merged(subsummaries)