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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25from collections import Counter, defaultdict
26from typing import *
28from klayout_pex.log import (
29 subproc,
30 warning,
31)
33from ..types import LayerName, NetName
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
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
47import klayout.db as kdb
48import klayout.pex as klp
51class RExtractionTechError(Exception):
52 """
53 The tech info can't model layers of the layout for the resistance extraction
54 """
55 pass
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
88 self.shapes_converter = ShapesConverter(dbu=self.pex_context.dbu)
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 """
97 rex_tech.skip_simplify = self.skip_simplify
99 tech = self.pex_context.tech
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)
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()
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()
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
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})"
128 LP = tech_pb2.LayerInfo.Purpose
130 match computed_layer_info.kind:
131 case tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
132 continue
134 case tech_pb2.ComputedLayerInfo.Kind.KIND_LABEL:
135 continue
137 case _:
138 pass
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
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
155 cond = rex_tech.conductors.add()
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
161 cond.triangulation_min_b = self.delaunay_b
162 cond.triangulation_max_area = self.delaunay_amax
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)
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
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()
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
187 cond.triangulation_min_b = self.delaunay_b
188 cond.triangulation_max_area = self.delaunay_amax
190 cond.algorithm = self.substrate_algorithm
191 cond.resistance = 0 # see comment above
192 modeled_gds_pairs.add(gds_pair)
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
202 cond = rex_tech.conductors.add()
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
208 cond.triangulation_min_b = self.delaunay_b
209 cond.triangulation_max_area = self.delaunay_amax
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)
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
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
231 via = rex_tech.vias.add()
233 bot_gds_pair = tech.gds_pair(contact.layer_below)
234 top_gds_pair = tech.gds_pair(contact.metal_above)
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
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)
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)
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()
262 (bot, top) = bot_top
263 bot_gds_pair = tech.gds_pair(bot)
264 top_gds_pair = tech.gds_pair(top)
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
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)
273 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[
274 source_layer.lvs_layer_name]
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 )
281 via.merge_distance = self.via_merge_distance
282 modeled_gds_pairs.add(gds_pair)
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)}")
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)
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)))
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 )
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}")
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 )
327 return rex_tech
329 def prepare_request(self) -> pex_request_pb2.RExtractionRequest:
330 rex_request = pex_request_pb2.RExtractionRequest()
332 # prepare tech info
333 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech)
335 # prepare devices
336 devices_by_name = self.pex_context.devices_by_name
337 rex_request.devices.MergeFrom(devices_by_name.values())
339 # prepare pins
340 for pin_list in self.pex_context.pins_pb2_by_layer.values():
341 rex_request.pins.MergeFrom(pin_list)
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
352 for pin in rex_request.pins:
353 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin)
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)
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()
388 return rex_request
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)
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}"
410 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult:
411 rex_result = pex_result_pb2.RExtractionResult()
413 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech)
415 Label = str
416 LayerName = str
417 NetName = str
418 DeviceID = int
419 TerminalID = int
421 # dicts keyed by id / klayout_index
422 layer_names: Dict[int, LayerName] = {}
424 wire_layer_ids: Set[int] = set()
425 via_layer_ids: Set[int] = set()
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)
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)
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)
442 nets_with_isolated_wells: List[NetName] = []
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)
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)
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))
463 for l2r in net_extraction_request.region_by_layer:
464 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region)
466 rex = klp.RNetExtractor(self.pex_context.dbu)
467 resistor_network = rex.extract(rex_tech_kly,
468 regions,
469 vertex_ports,
470 polygon_ports)
472 result_network = rex_result.networks.add()
473 result_network.net_name = net_extraction_request.net_name
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)
482 for rn in resistor_network.each_node():
483 if rn.object_id() in isolated_well_node_ids:
484 continue
486 node_by_node_id: Dict[int, r_network_pb2.RNode] = {}
488 loc = rn.location()
489 layer_id = rn.layer()
490 canonical_layer_name = layer_names[layer_id]
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
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()
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
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()
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()
544 node_by_node_id[r_node.node_id] = r_node
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)}")
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()
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)))
572 return rex_result
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()
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}
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
594 for el in resistor_network.each_element():
595 parent[find(el.a().object_id())] = find(el.b().object_id())
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()
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}