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

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 

28from typing import * 

29 

30from .types import NetName, LayerName, CellName 

31from ..klayout.lvsdb_extractor import unique_name 

32from ..log import debug, error 

33 

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 

37 

38 

39@dataclass 

40class NodeRegion: 

41 layer_name: LayerName 

42 net_name: NetName 

43 cap_to_gnd: float 

44 perimeter: float 

45 area: float 

46 

47 

48@dataclass(frozen=True) 

49class SidewallKey: 

50 layer: LayerName 

51 net1: NetName 

52 net2: NetName 

53 

54 

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 

62 

63 

64@dataclass(frozen=True) 

65class OverlapKey: 

66 layer_top: LayerName 

67 net_top: NetName 

68 layer_bot: LayerName 

69 net_bot: NetName 

70 

71 

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 

79 

80 

81@dataclass(frozen=True) 

82class SideOverlapKey: 

83 layer_inside: LayerName 

84 net_inside: NetName 

85 layer_outside: LayerName 

86 net_outside: NetName 

87 

88 def __repr__(self) -> str: 

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

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

91 

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

101 

102 

103@dataclass 

104class SideOverlapCap: 

105 key: SideOverlapKey 

106 cap_value: float # femto farad 

107 

108 def __str__(self) -> str: 

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

110 

111 

112@dataclass(frozen=True) 

113class NetCoupleKey: 

114 net1: NetName 

115 net2: NetName 

116 

117 def __repr__(self) -> str: 

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

119 

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

124 

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

130 

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) 

137 

138 

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) 

143 

144 

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 

154 

155 

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

157class DeviceTerminalKey: 

158 device_id: int 

159 terminal_id: int 

160 

161 

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

165 

166 

167@dataclass 

168class ExtractionSummary: 

169 capacitances: Dict[NetCoupleKey, float] 

170 resistances: Dict[NetCoupleKey, float] 

171 

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

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

174 

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) 

177 

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) 

195 

196 

197@dataclass 

198class NetworkNodeNames: 

199 """ 

200 The names of the netlist nodes of a resistor network 

201 """ 

202 by_node_id: Dict[int, NetName] 

203 

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

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

206 

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

214 

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

216 """ 

217 K = r_network_pb2.RNode.Kind 

218 

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

220 

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 

236 

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) 

239 

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) 

245 

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} 

248 

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 

254 

255 def union(node_id_a: int, node_id_b: int): 

256 parent[find(node_id_a)] = find(node_id_b) 

257 

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) 

264 

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) 

268 

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

270 return any(n.node_kind == K.KIND_PIN for n in groups[root]) 

271 

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

281 

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

283 

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 

293 

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) 

296 

297 

298@dataclass 

299class CellExtractionResults: 

300 cell_name: CellName 

301 

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

305 

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

307 

308 def add_overlap_cap(self, cap: OverlapCap): 

309 self.overlap_table[cap.key].append(cap) 

310 

311 def add_sidewall_cap(self, cap: SidewallCap): 

312 self.sidewall_table[cap.key].append(cap) 

313 

314 def add_sideoverlap_cap(self, cap: SideOverlapCap): 

315 self.sideoverlap_table[cap.key].append(cap) 

316 

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

318 """ 

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

320 

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 

339 

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

347 

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

354 

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

361 

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

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

364 

365 port_names_by_net = self.label_port_names() 

366 

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

372 

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) 

381 

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) 

394 

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

400 

401 return ExtractionSummary.merged([ 

402 overlap_summary, sidewall_summary, sideoverlap_summary, 

403 resistance_summary 

404 ]) 

405 

406 

407@dataclass 

408class ExtractionResults: 

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

410 

411 def summarize(self) -> ExtractionSummary: 

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

413 return ExtractionSummary.merged(subsummaries)