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

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# 

24 

25from __future__ import annotations 

26from collections import defaultdict 

27from dataclasses import dataclass, field, replace 

28from typing import * 

29 

30import klayout.db as kdb 

31 

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 

38 

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 

42 

43 

44@dataclass 

45class NodeRegion: 

46 layer_name: LayerName 

47 net_name: NetName 

48 cap_to_gnd: float 

49 perimeter: float 

50 area: float 

51 

52 

53@dataclass(frozen=True) 

54class SidewallKey: 

55 layer: LayerName 

56 net1: NetName 

57 net2: NetName 

58 

59 

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 

67 

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 

71 

72 

73@dataclass(frozen=True) 

74class OverlapKey: 

75 layer_top: LayerName 

76 net_top: NetName 

77 layer_bot: LayerName 

78 net_bot: NetName 

79 

80 

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 

88 

89 # the overlap of both nets, to distribute the capacitance (in DBU) 

90 area: Optional[kdb.Region] = None 

91 

92 

93@dataclass(frozen=True) 

94class SideOverlapKey: 

95 layer_inside: LayerName 

96 net_inside: NetName 

97 layer_outside: LayerName 

98 net_outside: NetName 

99 

100 def __repr__(self) -> str: 

101 return f"{self.layer_inside}({self.net_inside})-"\ 

102 f"{self.layer_outside}({self.net_outside})" 

103 

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

113 

114 

115@dataclass 

116class SideOverlapCap: 

117 key: SideOverlapKey 

118 cap_value: float # femto farad 

119 

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 

124 

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 

128 

129 def __str__(self) -> str: 

130 return f"(Side Overlap): {self.key} = {round(self.cap_value, 6)}fF" 

131 

132 

133@dataclass(frozen=True) 

134class NetCoupleKey: 

135 net1: NetName 

136 net2: NetName 

137 

138 def __repr__(self) -> str: 

139 return f"{self.net1}-{self.net2}" 

140 

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()) 

145 

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

151 

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) 

158 

159 

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) 

164 

165 

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 

175 

176 

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 

185 

186 

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 

190 

191 

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 

198 

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) 

205 

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 

218 

219 

220@dataclass(frozen=True, order=True) 

221class DeviceTerminalKey: 

222 device_id: int 

223 terminal_id: int 

224 

225 

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 # Ω 

230 

231 

232@dataclass 

233class ExtractionSummary: 

234 capacitances: Dict[NetCoupleKey, float] 

235 resistances: Dict[NetCoupleKey, float] 

236 

237 # node of the resistor network each device terminal connects to 

238 device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = field(default_factory=dict) 

239 

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) 

243 

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) 

246 

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) 

267 

268 

269@dataclass 

270class NetworkNodeNames: 

271 """ 

272 The names of the netlist nodes of a resistor network 

273 """ 

274 by_node_id: Dict[int, NetName] 

275 

276 # the IDs of the nodes of each device terminal (its ports) 

277 terminal_node_ids: Dict[DeviceTerminalKey, List[int]] 

278 

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) 

282 

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 

285 

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). 

293 

294 :param port_names: the port name of each label, for a net of several labels 

295 """ 

296 K = r_network_pb2.RNode.Kind 

297 

298 pin_labels = {n.node_name for n in network.nodes if n.node_kind == K.KIND_PIN} 

299 

300 def is_port(node: r_network_pb2.RNode) -> bool: 

301 return node.node_kind in (K.KIND_PIN, K.KIND_EXCLUSION_PORT) 

302 

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 

321 

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) 

324 

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) 

330 

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 

334 

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) 

341 

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) 

345 

346 def has_pin(root: int) -> bool: 

347 return any(is_port(n) for n in groups[root]) 

348 

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]) 

358 

359 name_by_root: Dict[int, NetName] = {root: group_name(root) for root in groups} 

360 

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] = [] 

367 

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])) 

377 

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] 

380 

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) 

401 

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 

405 

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) 

414 

415 if network.net_name not in name_by_root.values() and not net_tie_node_ids: 

416 put_on_net(candidate_roots) 

417 

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) 

422 

423 

424@dataclass 

425class CellExtractionResults: 

426 cell_name: CellName 

427 

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 

431 

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) 

436 

437 # NOTE: the capacitances that a model of what they mark has are left out (see exclusion.py) 

438 exclusions: Optional[Exclusions] = None 

439 

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)) 

443 

444 r_extraction_result: pex_result_pb2.RExtractionResult = field(default_factory=lambda: pex_result_pb2.RExtractionResult()) 

445 

446 def net_of_capacitance(self, net_name: NetName) -> NetName: 

447 return self.exclusions.net_of_capacitance(net_name, self.substrate_layer_name) 

448 

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) 

454 

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) 

465 

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] 

473 

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 [] 

478 

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) 

481 

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 

494 

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) 

500 

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)] 

516 

517 if exclusions.leaves_out_capacitance_of_net(inside_net_name, outside_net_name): 

518 cap.cap_value = 0.0 

519 return [] 

520 

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) 

523 

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 

540 

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) 

549 

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) 

561 

562 def label_port_names(self) -> Dict[NetName, Dict[str, NetName]]: 

563 """ 

564 The port names of the nets of several labels, by label 

565 

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 

589 

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) 

600 

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 } 

607 

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 ) 

616 

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={}) 

630 

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={}) 

643 

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={}) 

658 

659 normalized_resistance_table: Dict[NetCoupleKey, float] = {} 

660 device_terminal_nodes: Dict[DeviceTerminalKey, NetName] = {} 

661 exclusion_ports: Dict[NetName, List[NetName]] = {} 

662 

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]] 

668 

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 

674 

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) 

685 

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) 

698 

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) 

705 

706 summary = ExtractionSummary.merged([ 

707 overlap_summary, sidewall_summary, sideoverlap_summary, 

708 resistance_summary 

709 ]) 

710 

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) 

716 

717 return summary 

718 

719 

720@dataclass 

721class ExtractionResults: 

722 cell_extraction_results: Dict[CellName, CellExtractionResults] = field(default_factory=dict) 

723 

724 def summarize(self) -> ExtractionSummary: 

725 subsummaries = [s.summarize() for s in self.cell_extraction_results.values()] 

726 return ExtractionSummary.merged(subsummaries)