Coverage for klayout_pex/rcx25/r/r_extractor.py: 94%

355 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 collections import Counter, defaultdict 

26from typing import * 

27 

28from klayout_pex.log import ( 

29 subproc, 

30 warning, 

31) 

32 

33from ..types import LayerName, NetName 

34 

35from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter 

36from klayout_pex.klayout.lvsdb_extractor import GDSPair, KLayoutExtractionContext 

37from klayout_pex.klayout.rex_core import klayout_r_extractor_tech 

38 

39import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2 

40import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2 

41from klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 import RExtractorTech as pb_RExtractorTech 

42import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

43import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2 

44import klayout_pex_protobuf.kpex.request.pex_request_pb2 as pex_request_pb2 

45import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2 

46 

47import klayout.db as kdb 

48import klayout.pex as klp 

49 

50 

51class RExtractionTechError(Exception): 

52 """ 

53 The tech info can't model layers of the layout for the resistance extraction 

54 """ 

55 pass 

56 

57 

58class RExtractor: 

59 def __init__(self, 

60 pex_context: KLayoutExtractionContext, 

61 substrate_algorithm: pb_RExtractorTech.Algorithm, 

62 wire_algorithm: pb_RExtractorTech.Algorithm, 

63 delaunay_b: float, 

64 delaunay_amax: float, 

65 via_merge_distance: float, 

66 skip_simplify: bool): 

67 """ 

68 :param pex_context: KLayout PEX extraction context 

69 :param substrate_algorithm: The KLayout PEXCore Algorithm for decomposing polygons. 

70 Either SquareCounting or Tesselation (recommended) 

71 :param wire_algorithm: The KLayout PEXCore Algorithm for decomposing polygons. 

72 Either SquareCounting (recommended) or Tesselation 

73 :param delaunay_b: The "b" parameter for the Delaunay triangulation, 

74 a ratio of shortest triangle edge to circle radius 

75 :param delaunay_amax: The "max_area" specifies the maximum area of the triangles 

76 produced in square micrometers. 

77 :param via_merge_distance: Maximum distance where close vias are merged together 

78 :param skip_simplify: skip simplification of resistor network 

79 """ 

80 self.pex_context = pex_context 

81 self.substrate_algorithm = substrate_algorithm 

82 self.wire_algorithm = wire_algorithm 

83 self.delaunay_b = delaunay_b 

84 self.delaunay_amax = delaunay_amax 

85 self.via_merge_distance = via_merge_distance 

86 self.skip_simplify = skip_simplify 

87 

88 self.shapes_converter = ShapesConverter(dbu=self.pex_context.dbu) 

89 

90 def prepare_r_extractor_tech_pb(self, 

91 rex_tech: pb_RExtractorTech): 

92 """ 

93 Prepare KLayout PEXCore Technology Description based on the KPEX Tech Info data 

94 :param rex_tech: RExtractorTech protobuffer message 

95 """ 

96 

97 rex_tech.skip_simplify = self.skip_simplify 

98 

99 tech = self.pex_context.tech 

100 

101 # NOTE: a layer that can't be modeled is not part of the resistance network, 

102 # e.g. a via that joins nothing, which splits the networks of its nets. 

103 # This is never intended, so it's an error rather than a warning (#217). 

104 # It includes the LVS layers the tech info has no layer for (e.g. a via with another name) 

105 unmodeled_layers: List[str] = list(self.pex_context.unmodeled_layers) 

106 

107 # NOTE: the resistance extraction works on the layer of a GDS pair, with the shapes of all its LVS layers, 

108 # so one model covers them all (e.g. sky130A mcon_con and mcon_vpp, the vias within MOM caps) 

109 unmodeled_layers_by_gds_pair: Dict[GDSPair, List[str]] = defaultdict(list) 

110 modeled_gds_pairs: Set[GDSPair] = set() 

111 

112 substrate_gds_pairs = {tech.gds_pair_for_computed_layer_name[name] 

113 for name in tech.tech.substrate.lvs_layer_names 

114 if name in tech.gds_pair_for_computed_layer_name} 

115 wells_without_resistance: Set[str] = set() 

116 

117 for gds_pair, li in self.pex_context.extracted_layers.items(): 

118 for source_layer in li.source_layers: 

119 computed_layer_info = tech.computed_layer_info_by_name.get(source_layer.lvs_layer_name, None) 

120 if computed_layer_info is None: 

121 unmodeled_layers_by_gds_pair[gds_pair].append(f"LVS {source_layer.lvs_layer_name} {gds_pair}: " 

122 f"no computed layer info") 

123 continue 

124 

125 canonical_layer_name = tech.canonical_layer_name_by_gds_pair[gds_pair] 

126 layer_description = f"{canonical_layer_name} (LVS {source_layer.lvs_layer_name})" 

127 

128 LP = tech_pb2.LayerInfo.Purpose 

129 

130 match computed_layer_info.kind: 

131 case tech_pb2.ComputedLayerInfo.Kind.KIND_PIN: 

132 continue 

133 

134 case tech_pb2.ComputedLayerInfo.Kind.KIND_LABEL: 

135 continue 

136 

137 case _: 

138 pass 

139 

140 match computed_layer_info.layer_info.purpose: 

141 case LP.PURPOSE_NWELL | LP.PURPOSE_PWELL: 

142 # NOTE: a well is a conductor between the contacts over its taps, which land on the well 

143 # (e.g. sky130A licon_ntap_con), and the device terminals on it (e.g. the PMOS bulks), 

144 # but the substrate (e.g. IHP pwell_sub, on the GDS pair of pwell), which is ideal, 

145 # as it is 3D bulk rather than a sheet, so the device terminals on it stay on 

146 # the node of their net 

147 if gds_pair in substrate_gds_pairs: 

148 continue 

149 

150 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None) 

151 if layer_resistance is None: 

152 wells_without_resistance.add(layer_description) 

153 continue 

154 

155 cond = rex_tech.conductors.add() 

156 

157 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

158 cond.layer.canonical_layer_name = canonical_layer_name 

159 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

160 

161 cond.triangulation_min_b = self.delaunay_b 

162 cond.triangulation_max_area = self.delaunay_amax 

163 

164 cond.algorithm = self.substrate_algorithm 

165 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance) 

166 modeled_gds_pairs.add(gds_pair) 

167 

168 case LP.PURPOSE_NTAP | LP.PURPOSE_PTAP: 

169 # NOTE: no conductor, the contacts over a tap land on the well below it 

170 # (e.g. sky130A licon_ntap_con on the nwell), neglecting the resistance 

171 # of the tap itself, like the one of the diffusion (e.g. SkyWater rule SR.diff.1) 

172 continue 

173 

174 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT: 

175 # device terminals 

176 # - source/drain (e.g. sky130A: nsdm, psdm) 

177 # - bulk (e.g. nwell) 

178 # 

179 # we will consider this only as a pin end-point, there are no wires at all on this layer, 

180 # so the resistance does not matter for PEX 

181 cond = rex_tech.conductors.add() 

182 

183 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

184 cond.layer.canonical_layer_name = canonical_layer_name 

185 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

186 

187 cond.triangulation_min_b = self.delaunay_b 

188 cond.triangulation_max_area = self.delaunay_amax 

189 

190 cond.algorithm = self.substrate_algorithm 

191 cond.resistance = 0 # see comment above 

192 modeled_gds_pairs.add(gds_pair) 

193 

194 case LP.PURPOSE_METAL | LP.PURPOSE_MIM_CAP: 

195 # NOTE: the top plate of a MIM cap is a thin metal, which the white-box extraction keeps 

196 # (the MIM cap layers are no part of the extraction in the black-box mode) 

197 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None) 

198 if layer_resistance is None: 

199 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no sheet resistance") 

200 continue 

201 

202 cond = rex_tech.conductors.add() 

203 

204 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

205 cond.layer.canonical_layer_name = canonical_layer_name 

206 cond.layer.lvs_layer_name = source_layer.lvs_layer_name 

207 

208 cond.triangulation_min_b = self.delaunay_b 

209 cond.triangulation_max_area = self.delaunay_amax 

210 

211 if canonical_layer_name == tech.internal_substrate_layer_name: 

212 cond.algorithm = self.substrate_algorithm 

213 else: 

214 cond.algorithm = self.wire_algorithm 

215 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance) 

216 modeled_gds_pairs.add(gds_pair) 

217 

218 case LP.PURPOSE_CONTACT: 

219 contact = tech.contact_by_contact_lvs_layer_name.get(source_layer.lvs_layer_name, None) 

220 if contact is None: 

221 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no contact") 

222 continue 

223 

224 contact_resistance = tech.contact_resistance_by_device_layer_name.get(contact.layer_below, 

225 None) 

226 if contact_resistance is None: 

227 unmodeled_layers_by_gds_pair[gds_pair].append( 

228 f"{layer_description}: no contact resistance for {contact.layer_below}") 

229 continue 

230 

231 via = rex_tech.vias.add() 

232 

233 bot_gds_pair = tech.gds_pair(contact.layer_below) 

234 top_gds_pair = tech.gds_pair(contact.metal_above) 

235 

236 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

237 via.layer.canonical_layer_name = canonical_layer_name 

238 via.layer.lvs_layer_name = source_layer.lvs_layer_name 

239 

240 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair) 

241 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair) 

242 

243 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_contact( 

244 contact=contact, 

245 contact_resistance=contact_resistance 

246 ) 

247 via.merge_distance = self.via_merge_distance 

248 modeled_gds_pairs.add(gds_pair) 

249 

250 case LP.PURPOSE_VIA: 

251 via_resistance = tech.via_resistance_by_layer_name.get(canonical_layer_name, None) 

252 if via_resistance is None: 

253 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no via resistance") 

254 continue 

255 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get( 

256 source_layer.lvs_layer_name, None) 

257 if bot_top is None: 

258 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no bottom/top layers") 

259 continue 

260 via = rex_tech.vias.add() 

261 

262 (bot, top) = bot_top 

263 bot_gds_pair = tech.gds_pair(bot) 

264 top_gds_pair = tech.gds_pair(top) 

265 

266 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair) 

267 via.layer.canonical_layer_name = canonical_layer_name 

268 via.layer.lvs_layer_name = source_layer.lvs_layer_name 

269 

270 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair) 

271 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair) 

272 

273 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[ 

274 source_layer.lvs_layer_name] 

275 

276 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_via( 

277 contact=contact, 

278 via_resistance=via_resistance 

279 ) 

280 

281 via.merge_distance = self.via_merge_distance 

282 modeled_gds_pairs.add(gds_pair) 

283 

284 case _: 

285 unmodeled_layers_by_gds_pair[gds_pair].append( 

286 f"{layer_description}: unhandled layer purpose {LP.Name(computed_layer_info.layer_info.purpose)}") 

287 

288 for gds_pair, descriptions in unmodeled_layers_by_gds_pair.items(): 

289 if gds_pair not in modeled_gds_pairs: 

290 unmodeled_layers.extend(descriptions) 

291 

292 if wells_without_resistance: 

293 warning("The resistance network leaves out these wells, as the tech info has no sheet resistance " 

294 "for them, so the device terminals on them (e.g. the PMOS bulks) are ideally on their nets:\n" + 

295 '\n'.join(f" - {well}" for well in sorted(wells_without_resistance))) 

296 

297 if unmodeled_layers: 

298 raise RExtractionTechError( 

299 "The tech info can't model these layers of the layout for the resistance extraction, " 

300 "so their connections would be missing from the resistance network:\n" + 

301 '\n'.join(f" - {layer}" for layer in unmodeled_layers) 

302 ) 

303 

304 # NOTE: a via joins the conductors below and above it, but the layout may have none on one side, 

305 # e.g. a via without metal below, or a tech info that puts the layer below on another GDS pair 

306 # (gf180mcuD before #229), so the resistance network would miss the connection there 

307 conductor_layer_ids = {c.layer.id for c in rex_tech.conductors} 

308 vias_without_conductor: Set[str] = set() 

309 for via in rex_tech.vias: 

310 for side, conductor in (('below', via.bottom_conductor), ('above', via.top_conductor)): 

311 if conductor.id in conductor_layer_ids: 

312 continue 

313 layer_info = self.pex_context.annotated_layout.get_info(conductor.id) 

314 gds_pair = (layer_info.layer, layer_info.datatype) 

315 layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair.get(gds_pair, '?') 

316 problem = 'has no shapes' if gds_pair not in self.pex_context.extracted_layers else 'is no conductor' 

317 vias_without_conductor.add(f"{via.layer.canonical_layer_name} (LVS {via.layer.lvs_layer_name}): " 

318 f"the layer {side} it, {layer_name} {gds_pair}, {problem}") 

319 

320 if vias_without_conductor: 

321 raise RExtractionTechError( 

322 "These vias have no conductor to join on one side, " 

323 "so their connections would be missing from the resistance network:\n" + 

324 '\n'.join(f" - {via}" for via in sorted(vias_without_conductor)) 

325 ) 

326 

327 return rex_tech 

328 

329 def prepare_request(self) -> pex_request_pb2.RExtractionRequest: 

330 rex_request = pex_request_pb2.RExtractionRequest() 

331 

332 # prepare tech info 

333 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech) 

334 

335 # prepare devices 

336 devices_by_name = self.pex_context.devices_by_name 

337 rex_request.devices.MergeFrom(devices_by_name.values()) 

338 

339 # prepare pins 

340 for pin_list in self.pex_context.pins_pb2_by_layer.values(): 

341 rex_request.pins.MergeFrom(pin_list) 

342 

343 net_request_by_name: Dict[NetName, pex_request_pb2.RNetExtractionRequest] = {} 

344 def get_or_create_net_request(net_name: str): 

345 v = net_request_by_name.get(net_name, None) 

346 if not v: 

347 v = rex_request.net_extraction_requests.add() 

348 v.net_name = net_name 

349 net_request_by_name[net_name] = v 

350 return v 

351 

352 for pin in rex_request.pins: 

353 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin) 

354 

355 for device in rex_request.devices: 

356 for terminal in device.terminals: 

357 get_or_create_net_request(terminal.net_name).device_terminals.add().CopyFrom(terminal) 

358 

359 netlist = self.pex_context.lvsdb.netlist() 

360 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name) 

361 # https://www.klayout.de/doc-qt5/code/class_Circuit.html 

362 if not circuit: 

363 circuits = [c.name for c in netlist.each_circuit()] 

364 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, " 

365 f"only available circuits are: {circuits}") 

366 LK = tech_pb2.ComputedLayerInfo.Kind 

367 for net in circuit.each_net(): 

368 net_name = net.name or f"${net.cluster_id}" 

369 for lvs_gds_pair, lyr_info in self.pex_context.extracted_layers.items(): 

370 for lyr in lyr_info.source_layers: 

371 li = self.pex_context.tech.computed_layer_info_by_gds_pair[lyr.gds_pair] 

372 match li.kind: 

373 case LK.KIND_PIN: 

374 continue # skip 

375 case LK.KIND_REGULAR | LK.KIND_DEVICE_CAPACITOR | LK.KIND_DEVICE_CAPACITOR_PLATE | \ 

376 LK.KIND_DEVICE_RESISTOR: 

377 r = self.pex_context.shapes_of_net(lyr.gds_pair, net) 

378 if not r: 

379 continue 

380 l2r = get_or_create_net_request(net_name).region_by_layer.add() 

381 l2r.layer.id = self.pex_context.annotated_layout.layer(*lvs_gds_pair) 

382 l2r.layer.canonical_layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair] 

383 l2r.layer.lvs_layer_name = lyr.lvs_layer_name 

384 self.shapes_converter.klayout_region_to_pb(r, l2r.region) 

385 case _: 

386 raise NotImplementedError() 

387 

388 return rex_request 

389 

390 @staticmethod 

391 def node_name(rn: klp.RNode, layer_name: LayerName) -> str: 

392 """ 

393 The name of a node of the resistor network, unique within the network (but pins), 

394 e.g. $1.li1 (internal node), P0.poly (polygon port), V0.met1 (vertex port) 

395 

396 NOTE: KLayout names a node after its layer index (e.g. $1.17), but the layer indexes depend on 

397 the order KLayout creates the LVS layers in, which differs from run to run 

398 """ 

399 match rn.type(): 

400 case klp.RNodeType.Internal: 

401 prefix = '$' 

402 case klp.RNodeType.VertexPort: 

403 prefix = 'V' 

404 case klp.RNodeType.PolygonPort: 

405 prefix = 'P' 

406 case _: 

407 raise NotImplementedError() 

408 return f"{prefix}{rn.port_index()}.{layer_name}" 

409 

410 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult: 

411 rex_result = pex_result_pb2.RExtractionResult() 

412 

413 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech) 

414 

415 Label = str 

416 LayerName = str 

417 NetName = str 

418 DeviceID = int 

419 TerminalID = int 

420 

421 # dicts keyed by id / klayout_index 

422 layer_names: Dict[int, LayerName] = {} 

423 

424 wire_layer_ids: Set[int] = set() 

425 via_layer_ids: Set[int] = set() 

426 

427 for c in rex_request.tech.conductors: 

428 layer_names[c.layer.id] = c.layer.canonical_layer_name 

429 wire_layer_ids.add(c.layer.id) 

430 

431 for v in rex_request.tech.vias: 

432 layer_names[v.layer.id] = v.layer.canonical_layer_name 

433 via_layer_ids.add(c.layer.id) 

434 

435 LP = tech_pb2.LayerInfo.Purpose 

436 well_layer_ids: Set[int] = set() 

437 for c in rex_request.tech.conductors: 

438 computed_layer_info = self.pex_context.tech.computed_layer_info_by_name.get(c.layer.lvs_layer_name, None) 

439 if computed_layer_info and computed_layer_info.layer_info.purpose in (LP.PURPOSE_NWELL, LP.PURPOSE_PWELL): 

440 well_layer_ids.add(c.layer.id) 

441 

442 nets_with_isolated_wells: List[NetName] = [] 

443 

444 for net_extraction_request in rex_request.net_extraction_requests: 

445 vertex_ports: Dict[int, List[kdb.Point]] = defaultdict(list) 

446 polygon_ports: Dict[int, List[kdb.Polygon]] = defaultdict(list) 

447 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]] = defaultdict(list) 

448 polygon_port_device_terminals: Dict[int, List[device_pb2.Device.Terminal]] = defaultdict(list) 

449 regions: Dict[int, kdb.Region] = defaultdict(kdb.Region) 

450 

451 for t in net_extraction_request.device_terminals: 

452 for l2r in t.region_by_layer: 

453 for sh in l2r.region.shapes: 

454 sh_kly = self.shapes_converter.klayout_shape(sh) 

455 polygon_ports[l2r.layer.id].append(sh_kly) 

456 polygon_port_device_terminals[l2r.layer.id].append(t) 

457 

458 for pin in net_extraction_request.pins: 

459 p = self.shapes_converter.klayout_point(pin.label_point) 

460 vertex_ports[pin.layer.id].append(p) 

461 vertex_port_pins[pin.layer.id].append((pin.label, pin.net_name)) 

462 

463 for l2r in net_extraction_request.region_by_layer: 

464 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region) 

465 

466 rex = klp.RNetExtractor(self.pex_context.dbu) 

467 resistor_network = rex.extract(rex_tech_kly, 

468 regions, 

469 vertex_ports, 

470 polygon_ports) 

471 

472 result_network = rex_result.networks.add() 

473 result_network.net_name = net_extraction_request.net_name 

474 

475 # NOTE: LVS may join a well to its net other than by the contacts over its taps, 

476 # e.g. by a label of the net on the well, so it's a piece of the network that joins nothing else. 

477 # It's left out, so that the device terminals on it stay on the node of their net, ideally 

478 isolated_well_node_ids = self.isolated_well_node_ids(resistor_network, well_layer_ids) 

479 if isolated_well_node_ids: 

480 nets_with_isolated_wells.append(result_network.net_name) 

481 

482 for rn in resistor_network.each_node(): 

483 if rn.object_id() in isolated_well_node_ids: 

484 continue 

485 

486 node_by_node_id: Dict[int, r_network_pb2.RNode] = {} 

487 

488 loc = rn.location() 

489 layer_id = rn.layer() 

490 canonical_layer_name = layer_names[layer_id] 

491 

492 r_node = result_network.nodes.add() 

493 r_node.node_id = rn.object_id() 

494 r_node.node_name = self.node_name(rn, canonical_layer_name) 

495 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO! 

496 r_node.layer_name = canonical_layer_name 

497 

498 match rn.type(): 

499 case klp.RNodeType.VertexPort: # pins! 

500 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_POINT 

501 p = loc.center().to_itype(self.pex_context.dbu) 

502 r_node.location.point.x = p.x 

503 r_node.location.point.y = p.y 

504 case klp.RNodeType.PolygonPort | klp.RNodeType.Internal: 

505 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX 

506 p1 = loc.p1.to_itype(self.pex_context.dbu) 

507 p2 = loc.p2.to_itype(self.pex_context.dbu) 

508 r_node.location.box.lower_left.x = p1.x 

509 r_node.location.box.lower_left.y = p1.y 

510 r_node.location.box.upper_right.x = p2.x 

511 r_node.location.box.upper_right.y = p2.y 

512 case _: 

513 raise NotImplementedError() 

514 

515 match rn.type(): 

516 case klp.RNodeType.VertexPort: 

517 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_PIN 

518 port_idx = rn.port_index() 

519 r_node.node_name, r_node.net_name = vertex_port_pins[rn.layer()][port_idx][0:2] 

520 r_node.location.point.net = r_node.net_name 

521 

522 case klp.RNodeType.PolygonPort: 

523 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL 

524 port_idx = rn.port_index() 

525 # NOTE: the device terminal behind the port, to connect the device to this node 

526 r_node.device_terminal.CopyFrom(polygon_port_device_terminals[rn.layer()][port_idx]) 

527 r_node.device_terminal.ClearField('region_by_layer') 

528 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}" 

529 r_node.location.box.net = r_node.net_name 

530 case klp.RNodeType.Internal: 

531 if rn.layer() in via_layer_ids: 

532 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_VIA_JUNCTION 

533 elif rn.layer() in wire_layer_ids: 

534 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION 

535 else: 

536 raise NotImplementedError() 

537 

538 # NOTE: network prefix, as node name is only unique per network 

539 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}" 

540 r_node.location.box.net = r_node.net_name 

541 case _: 

542 raise NotImplementedError() 

543 

544 node_by_node_id[r_node.node_id] = r_node 

545 

546 # NOTE: nodes of the same name are one node in the netlist, like the pins of the same label, 

547 # so the names of the other nodes must be unique 

548 # (the internal nodes are numbered per network, the ports per layer, so no 2 layers 

549 # of the same name must have ports, e.g. a conductor split into several GDS pairs) 

550 name_counts = Counter(n.node_name for n in result_network.nodes 

551 if n.node_kind != r_network_pb2.RNode.Kind.KIND_PIN) 

552 duplicate_names = sorted(name for name, count in name_counts.items() if count > 1) 

553 if duplicate_names: 

554 raise RExtractionTechError(f"Nodes of net {result_network.net_name} have the same name, " 

555 f"as layers of the same name have ports: {', '.join(duplicate_names)}") 

556 

557 for el in resistor_network.each_element(): 

558 if el.a().object_id() in isolated_well_node_ids: 

559 continue # NOTE: and so is b, of the same piece 

560 r_element = result_network.elements.add() 

561 r_element.element_id = el.object_id() 

562 r_element.node_a.node_id = el.a().object_id() 

563 r_element.node_b.node_id = el.b().object_id() 

564 r_element.resistance = el.resistance() 

565 

566 if nets_with_isolated_wells: 

567 warning("The resistance network leaves out the wells of these nets, as no contact joins them " 

568 "to the rest of their net (e.g. a label of the net on the well does), " 

569 "so the device terminals on them (e.g. the PMOS bulks) are ideally on their nets: " + 

570 ', '.join(sorted(nets_with_isolated_wells))) 

571 

572 return rex_result 

573 

574 @staticmethod 

575 def isolated_well_node_ids(resistor_network: klp.RNetwork, well_layer_ids: Set[int]) -> Set[int]: 

576 """ 

577 :return: the nodes of the pieces of the network that are wells only, without a pin, 

578 i.e. the wells no contact joins to the rest of their net 

579 (none if the network is one piece, e.g. a well with the bulks of transistors only) 

580 """ 

581 nodes = list(resistor_network.each_node()) 

582 if not any(n.layer() in well_layer_ids for n in nodes): 

583 return set() 

584 

585 # the pieces of the network (union-find by node ID) 

586 parent: Dict[int, int] = {n.object_id(): n.object_id() for n in nodes} 

587 

588 def find(node_id: int) -> int: 

589 while parent[node_id] != node_id: 

590 parent[node_id] = parent[parent[node_id]] 

591 node_id = parent[node_id] 

592 return node_id 

593 

594 for el in resistor_network.each_element(): 

595 parent[find(el.a().object_id())] = find(el.b().object_id()) 

596 

597 pieces: Dict[int, List[klp.RNode]] = defaultdict(list) 

598 for n in nodes: 

599 pieces[find(n.object_id())].append(n) 

600 if len(pieces) < 2: 

601 return set() 

602 

603 return {n.object_id() 

604 for piece in pieces.values() 

605 if all(n.layer() in well_layer_ids and n.type() != klp.RNodeType.VertexPort for n in piece) 

606 for n in piece}