Coverage for klayout_pex/rcx25/r/r_extractor.py: 98%
897 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25from collections import Counter, defaultdict
26from dataclasses import dataclass, field
27import itertools
28import math
29import re
30from typing import *
32from klayout_pex.log import (
33 info,
34 subproc,
35 warning,
36)
38from klayout_pex.types import DeviceID, GDSPair, Label, LayerName, NetName, TerminalID
40from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter
41from klayout_pex.klayout.lvsdb_extractor import KLayoutExtractionContext
42from klayout_pex.klayout.rex_core import klayout_r_extractor_tech
43from klayout_pex.rcx25.exclusion import (
44 BlockKind,
45 BlockLabel,
46 Exclusions,
47 NetPiece,
48 RegionIndex,
49 listed_names,
50 polygons_by_center_inside,
51)
52from klayout_pex.util.disjoint_sets import DisjointSets
54import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2
55import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2
56from klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 import RExtractorTech as pb_RExtractorTech
57import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
58import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2
59import klayout_pex_protobuf.kpex.request.pex_request_pb2 as pex_request_pb2
60import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2
62import klayout.db as kdb
63import klayout.pex as klp
66class RExtractionTechError(Exception):
67 """
68 The tech info can't model layers of the layout for the resistance extraction
69 """
70 pass
73# NOTE: the layer of what is within an exclusion marker is the one of the layout plus this (see ideal_layer_id),
74# only known to the resistance extraction, which identifies its layers by these IDs
75IDEAL_LAYER_ID_OFFSET = 1 << 24
78def ideal_layer_id(layer_id: int) -> int:
79 return layer_id + IDEAL_LAYER_ID_OFFSET
82# the widths of the polygon ports at a cut, in DBU
83CUT_STRIP_WIDTH = 1
84ANY_ANGLE_CUT_STRIP_WIDTH = 5
85# the length of the polygon port at a cut along a conductor, in µm, see port_of_cut
86PORT_LENGTH_ALONG_CONDUCTOR = 1.0
89PolygonPortKey = Tuple[int, int] # (layer ID, port index)
91# what the polygon ports of each layer ID are of, None for the ones at a cut (see split_at_exclusions)
92PolygonPortTerminals = Dict[int, List[Optional[device_pb2.Device.Terminal]]]
95@dataclass
96class PinWithinMarker:
97 position: kdb.Point
98 layer_id: int
99 label: Label
100 net_name: NetName
103@dataclass
104class TerminalWithinMarker:
105 terminal: device_pb2.Device.Terminal
106 layer_id: int
107 box: kdb.Box
110class ConnectedShapesWithin:
111 """
112 Which shapes of a net within the blocks are joined there, by metal or vias, regardless of the rest of the net,
113 so that a label on them can name the ports of a cut of only these shapes
114 """
115 def __init__(self,
116 regions_within: Mapping[int, Mapping[LayerName, kdb.Region]],
117 vias_within: Iterable[Tuple[LayerName, LayerName, kdb.Region]],
118 kept_for_vias: Mapping[LayerName, kdb.Region]):
119 self.kept_for_vias = kept_for_vias
120 self.shapes_by_layer: Dict[LayerName, kdb.Shapes] = defaultdict(kdb.Shapes)
121 polygon_count = 0
122 for region_by_layer in regions_within.values():
123 for layer_name, region in region_by_layer.items():
124 for polygon in region.merged().each():
125 self.shapes_by_layer[layer_name].insert(kdb.PolygonWithProperties(polygon.downcast(),
126 {'id': polygon_count}))
127 polygon_count += 1
129 self.polygon_sets = DisjointSets(range(polygon_count))
130 for bottom_layer_name, top_layer_name, vias in vias_within:
131 for via in vias.each():
132 box = via.bbox()
133 bottom, top = self.polygon_at(bottom_layer_name, box), self.polygon_at(top_layer_name, box)
134 if bottom is not None and top is not None:
135 self.polygon_sets.union(bottom, top)
137 def polygon_at(self, layer_name: LayerName, box: kdb.Box) -> Optional[int]:
138 """
139 :return: the ID of the polygon on the layer with the largest overlap with the box
140 """
141 area_by_id: Dict[int, int] = {}
142 box_region = kdb.Region(box)
143 for shape in self.shapes_by_layer[layer_name].each_touching(kdb.Shapes.SRegions, box):
144 area = (kdb.Region(shape.polygon) & box_region).area()
145 if area > 0:
146 area_by_id[shape.property('id')] = area
147 return max(sorted(area_by_id), key=area_by_id.get, default=None)
149 def set_at(self, layer_name: LayerName, box: kdb.Box) -> Optional[int]:
150 """
151 :return: the ID of what is joined to the shapes at the box on the layer, None if there are no shapes
152 """
153 polygon = self.polygon_at(layer_name, box)
154 return None if polygon is None else self.polygon_sets.find(polygon)
156 def set_at_position(self, layer_name: LayerName, position: kdb.Point) -> Optional[int]:
157 """
158 :return: the ID of what is joined to the shapes at the position (e.g. of a pin) on the layer,
159 None if there are no shapes
160 """
161 box = kdb.Box(position, position).enlarged(1, 1)
162 # NOTE: where the metal stays for a via from above, what is around it has the position
163 kept = self.kept_for_vias.get(layer_name, None)
164 if kept is not None:
165 for via in kept.interacting(kdb.Region(box)).each():
166 box = via.bbox().enlarged(1, 1)
167 return self.set_at(layer_name, box)
170@dataclass
171class NetSplit:
172 """
173 What split_at_exclusions tells about the network of a net
174 """
175 # the polygon ports to join without resistance, of the conductors on both sides of a cut
176 polygon_port_ties: List[Tuple[PolygonPortKey, PolygonPortKey]] = field(default_factory=list)
177 # with ports at the cuts: their polygon ports (by the box of the strip), and what is within the markers
178 cut_port_boxes: Dict[PolygonPortKey, kdb.Box] = field(default_factory=dict)
179 # the index of the block of each of them
180 cut_port_blocks: Dict[PolygonPortKey, int] = field(default_factory=dict)
181 # the terminal of a removed device that each of them stands for, if any,
182 # and that a cut is on, which it is one node with then, unless a label names the cut
183 cut_port_terminals: Dict[PolygonPortKey, device_pb2.Device.Terminal] = field(default_factory=dict)
184 terminals_of_cuts: Dict[PolygonPortKey, device_pb2.Device.Terminal] = field(default_factory=dict)
185 pins_within: List[PinWithinMarker] = field(default_factory=list)
186 terminals_within: List[TerminalWithinMarker] = field(default_factory=list)
187 # the shapes within each marker, by its index, by layer
188 regions_within: Dict[int, Dict[LayerName, kdb.Region]] = field(default_factory=lambda: defaultdict(dict))
189 # the vias between them: the layers below and above them, and the vias
190 vias_within: List[Tuple[LayerName, LayerName, kdb.Region]] = field(default_factory=list)
191 # the index of the block of each node of the network at a cut, by the ID of the node
192 block_of_node_id: Dict[int, int] = field(default_factory=dict)
193 # the blocks with ports that a conductor goes on from one to another, without a cut between them
194 abutting_block_indexes: Set[int] = field(default_factory=set)
195 # the vias across a cut by the layer of the conductor below or above them, whose metal stays outside the block
196 kept_for_vias: Dict[LayerName, kdb.Region] = field(default_factory=dict)
197 # whether a conductor goes on beside a block along a cut, see port_of_cut
198 has_cut_along_conductor: bool = False
201def label_names_net(label: Label, net_name: NetName) -> bool:
202 """
203 :return: whether KLayout named the net after the label, regardless of case, as SPICE reads names:
204 A of A,B (a net of several labels), vss of vss$1 (islands of one name),
205 VDD of $1.VDD (a net within a subcircuit)
206 """
207 label = label.casefold()
208 for name in net_name.casefold().split(','):
209 for candidate in (name, re.sub(r'\$\d+$', '', name)):
210 if candidate == label or candidate.endswith(f".{label}"):
211 return True
212 return False
215def keep_pins_of_top_circuit(net_name: NetName,
216 vertex_ports: Dict[int, List[kdb.Point]],
217 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]]):
218 """
219 Keeps the labels of the hierarchy that are pins of the top circuit, like the ports of its LVS netlist:
220 a label in a subcell is none on a net that KLayout named after the labels of its own circuit (#403),
221 and the first one of a net that no label names (e.g. with labels in subcells only) is the node of its port
223 NOTE: after the split at the exclusions, as a label within a marker names the port of the model there,
224 also in a subcell (e.g. of an inductor)
225 """
226 labels = {pin[0] for pins in vertex_port_pins.values() for pin in pins}
227 if not labels:
228 return
229 pin_labels = {label for label in labels if label_names_net(label, net_name)} or {min(labels)}
230 for layer_id in list(vertex_port_pins):
231 kept = [(point, pin)
232 for point, pin in zip(vertex_ports[layer_id], vertex_port_pins[layer_id])
233 if pin[0] in pin_labels]
234 if kept:
235 vertex_ports[layer_id] = [point for point, _ in kept]
236 vertex_port_pins[layer_id] = [pin for _, pin in kept]
237 else:
238 del vertex_ports[layer_id]
239 del vertex_port_pins[layer_id]
242def port_of_cut(strip: kdb.PolygonWithProperties,
243 conductor_near: Callable[[kdb.Box], kdb.Region],
244 strip_width: int,
245 port_length: int) -> kdb.PolygonWithProperties:
246 """
247 A polygon port is one node. So it's the whole strip where a conductor enters a block, across the current,
248 also of a wide wire. But where the conductor goes on beyond the ends of the strip, the strip is along the current,
249 beside metal that it would short (a port can't stand for a model in parallel to that metal),
250 so then it's a short piece in the middle of the longest edge of the strip
252 :param strip: of a conductor along the cut
253 :param conductor_near: the polygons of the conductor at a box
254 :param strip_width: of the strip
255 :param port_length: of the short piece, in DBU
256 """
257 box = strip.bbox()
258 if box.width() >= box.height():
259 ends = (kdb.Box(box.left - 1, box.bottom, box.left, box.top),
260 kdb.Box(box.right, box.bottom, box.right + 1, box.top))
261 else:
262 ends = (kdb.Box(box.left, box.bottom - 1, box.right, box.bottom),
263 kdb.Box(box.left, box.top, box.right, box.top + 1))
264 if not strip.is_rectilinear() or all((conductor_near(end) & kdb.Region(end)).is_empty() for end in ends):
265 return strip
267 edge = max(strip.each_edge(), key=lambda e: e.length())
268 if edge.length() <= port_length:
269 return strip
270 middle = kdb.Point((edge.x1 + edge.x2) // 2, (edge.y1 + edge.y2) // 2)
271 half_length = port_length // 2
272 window = kdb.Box(middle.x - half_length, middle.y - strip_width, middle.x + half_length, middle.y + strip_width) \
273 if edge.dy() == 0 else \
274 kdb.Box(middle.x - strip_width, middle.y - half_length, middle.x + strip_width, middle.y + half_length)
275 return next((kdb.Region(strip.downcast()) & kdb.Region(window)).each())
278class RExtractor:
279 def __init__(self,
280 pex_context: KLayoutExtractionContext,
281 substrate_algorithm: pb_RExtractorTech.Algorithm,
282 wire_algorithm: pb_RExtractorTech.Algorithm,
283 delaunay_b: float,
284 delaunay_amax: float,
285 via_merge_distance: float,
286 skip_simplify: bool,
287 exclusions: Optional[Exclusions] = None):
288 """
289 :param pex_context: KLayout PEX extraction context
290 :param substrate_algorithm: The KLayout PEXCore Algorithm for decomposing polygons.
291 Either SquareCounting or Tesselation (recommended)
292 :param wire_algorithm: The KLayout PEXCore Algorithm for decomposing polygons.
293 Either SquareCounting (recommended) or Tesselation
294 :param delaunay_b: The "b" parameter for the Delaunay triangulation,
295 a ratio of shortest triangle edge to circle radius
296 :param delaunay_amax: The "max_area" specifies the maximum area of the triangles
297 produced in square micrometers.
298 :param via_merge_distance: Maximum distance where close vias are merged together
299 :param skip_simplify: skip simplification of resistor network
300 :param exclusions: within these, the network has no resistances (see split_at_exclusions)
301 """
302 self.pex_context = pex_context
303 self.substrate_algorithm = substrate_algorithm
304 self.wire_algorithm = wire_algorithm
305 self.delaunay_b = delaunay_b
306 self.delaunay_amax = delaunay_amax
307 self.via_merge_distance = via_merge_distance
308 self.skip_simplify = skip_simplify
309 self.exclusions = exclusions
311 self.shapes_converter = ShapesConverter(dbu=self.pex_context.dbu)
313 def prepare_r_extractor_tech_pb(self,
314 rex_tech: pb_RExtractorTech):
315 """
316 Prepare KLayout PEXCore Technology Description based on the KPEX Tech Info data
317 :param rex_tech: RExtractorTech protobuffer message
318 """
320 rex_tech.skip_simplify = self.skip_simplify
322 tech = self.pex_context.tech
324 # NOTE: a layer that can't be modeled is not part of the resistance network,
325 # e.g. a via that joins nothing, which splits the networks of its nets.
326 # This is never intended, so it's an error rather than a warning (#217).
327 # It includes the LVS layers the tech info has no layer for (e.g. a via with another name)
328 unmodeled_layers: List[str] = list(self.pex_context.unmodeled_layers)
330 # NOTE: the resistance extraction works on the layer of a GDS pair, with the shapes of all its LVS layers,
331 # so one model covers them all (e.g. sky130A mcon_con and mcon_vpp, the vias within MOM caps)
332 unmodeled_layers_by_gds_pair: Dict[GDSPair, List[str]] = defaultdict(list)
333 modeled_gds_pairs: Set[GDSPair] = set()
335 substrate_gds_pairs = {tech.gds_pair_for_computed_layer_name[name]
336 for name in tech.tech.substrate.lvs_layer_names
337 if name in tech.gds_pair_for_computed_layer_name}
338 wells_without_resistance: Set[str] = set()
340 for gds_pair, li in self.pex_context.extracted_layers.items():
341 for source_layer in li.source_layers:
342 computed_layer_info = tech.computed_layer_info_by_name.get(source_layer.lvs_layer_name, None)
343 if computed_layer_info is None:
344 unmodeled_layers_by_gds_pair[gds_pair].append(f"LVS {source_layer.lvs_layer_name} {gds_pair}: "
345 f"no computed layer info")
346 continue
348 canonical_layer_name = tech.canonical_layer_name_by_gds_pair[gds_pair]
349 layer_description = f"{canonical_layer_name} (LVS {source_layer.lvs_layer_name})"
351 LP = tech_pb2.LayerInfo.Purpose
353 match computed_layer_info.kind:
354 case tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
355 continue
357 case tech_pb2.ComputedLayerInfo.Kind.KIND_LABEL:
358 continue
360 case _:
361 pass
363 match computed_layer_info.layer_info.purpose:
364 case LP.PURPOSE_NWELL | LP.PURPOSE_PWELL:
365 # NOTE: a well is a conductor between the contacts over its taps, which land on the well
366 # (e.g. sky130A licon_ntap_con), and the device terminals on it (e.g. the PMOS bulks),
367 # but the substrate (e.g. IHP pwell_sub, on the GDS pair of pwell), which is ideal,
368 # as it is 3D bulk rather than a sheet, so the device terminals on it stay on
369 # the node of their net
370 if gds_pair in substrate_gds_pairs:
371 continue
373 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None)
374 if layer_resistance is None:
375 wells_without_resistance.add(layer_description)
376 continue
378 cond = rex_tech.conductors.add()
380 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
381 cond.layer.canonical_layer_name = canonical_layer_name
382 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
384 cond.triangulation_min_b = self.delaunay_b
385 cond.triangulation_max_area = self.delaunay_amax
387 cond.algorithm = self.substrate_algorithm
388 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance)
389 modeled_gds_pairs.add(gds_pair)
391 case LP.PURPOSE_DNWELL:
392 # NOTE: no conductor: no via joins the deep nwell to the nwell over it,
393 # so it would be a piece that joins nothing (see isolated_well_node_ids),
394 # and gf180mcuD has no sheet resistance for it. Like the substrate,
395 # the device terminals on it stay on the node of their net (#292)
396 continue
398 case LP.PURPOSE_NTAP | LP.PURPOSE_PTAP:
399 # NOTE: no conductor, the contacts over a tap land on the well below it
400 # (e.g. sky130A licon_ntap_con on the nwell), neglecting the resistance
401 # of the tap itself, like the one of the diffusion (e.g. SkyWater rule SR.diff.1)
402 continue
404 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT:
405 # device terminals
406 # - source/drain (e.g. sky130A: nsdm, psdm)
407 # - bulk (e.g. nwell)
408 #
409 # we will consider this only as a pin end-point, there are no wires at all on this layer,
410 # so the resistance does not matter for PEX
411 cond = rex_tech.conductors.add()
413 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
414 cond.layer.canonical_layer_name = canonical_layer_name
415 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
417 cond.triangulation_min_b = self.delaunay_b
418 cond.triangulation_max_area = self.delaunay_amax
420 cond.algorithm = self.substrate_algorithm
421 cond.resistance = 0 # see comment above
422 modeled_gds_pairs.add(gds_pair)
424 case LP.PURPOSE_METAL | LP.PURPOSE_MIM_CAP:
425 # NOTE: the top plate of a MIM cap is a thin metal, which the white-box extraction keeps
426 # (the MIM cap layers are no part of the extraction in the black-box mode)
427 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None)
428 if layer_resistance is None:
429 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no sheet resistance")
430 continue
432 cond = rex_tech.conductors.add()
434 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
435 cond.layer.canonical_layer_name = canonical_layer_name
436 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
438 cond.triangulation_min_b = self.delaunay_b
439 cond.triangulation_max_area = self.delaunay_amax
441 if canonical_layer_name == tech.internal_substrate_layer_name:
442 cond.algorithm = self.substrate_algorithm
443 else:
444 cond.algorithm = self.wire_algorithm
445 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance)
446 modeled_gds_pairs.add(gds_pair)
448 case LP.PURPOSE_CONTACT:
449 contact = tech.contact_by_contact_lvs_layer_name.get(source_layer.lvs_layer_name, None)
450 if contact is None:
451 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no contact")
452 continue
454 contact_resistance = tech.contact_resistance_by_device_layer_name.get(contact.layer_below,
455 None)
456 if contact_resistance is None:
457 unmodeled_layers_by_gds_pair[gds_pair].append(
458 f"{layer_description}: no contact resistance for {contact.layer_below}")
459 continue
461 via = rex_tech.vias.add()
463 bot_gds_pair = tech.gds_pair(contact.layer_below)
464 top_gds_pair = tech.gds_pair(contact.metal_above)
466 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
467 via.layer.canonical_layer_name = canonical_layer_name
468 via.layer.lvs_layer_name = source_layer.lvs_layer_name
470 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
471 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
473 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_contact(
474 contact=contact,
475 contact_resistance=contact_resistance
476 )
477 via.merge_distance = self.via_merge_distance
478 modeled_gds_pairs.add(gds_pair)
480 case LP.PURPOSE_VIA:
481 via_resistance = tech.via_resistance_by_layer_name.get(canonical_layer_name, None)
482 if via_resistance is None:
483 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no via resistance")
484 continue
485 # NOTE: the via of the process stack on the GDS pair, if it has none named after the LVS layer
486 # (e.g. the vias of a device, which the LVS deck has apart from the others)
487 via_name = tech.process_layer_name_by_lvs_layer_name.get(source_layer.lvs_layer_name,
488 source_layer.lvs_layer_name)
489 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get(via_name, None)
490 if bot_top is None:
491 unmodeled_layers_by_gds_pair[gds_pair].append(f"{layer_description}: no bottom/top layers")
492 continue
493 via = rex_tech.vias.add()
495 (bot, top) = bot_top
496 bot_gds_pair = tech.gds_pair(bot)
497 top_gds_pair = tech.gds_pair(top)
499 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
500 via.layer.canonical_layer_name = canonical_layer_name
501 via.layer.lvs_layer_name = source_layer.lvs_layer_name
503 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
504 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
506 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[via_name]
508 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_via(
509 contact=contact,
510 via_resistance=via_resistance
511 )
513 via.merge_distance = self.via_merge_distance
514 modeled_gds_pairs.add(gds_pair)
516 case _:
517 unmodeled_layers_by_gds_pair[gds_pair].append(
518 f"{layer_description}: unhandled layer purpose {LP.Name(computed_layer_info.layer_info.purpose)}")
520 for gds_pair, descriptions in unmodeled_layers_by_gds_pair.items():
521 if gds_pair not in modeled_gds_pairs:
522 unmodeled_layers.extend(descriptions)
524 if wells_without_resistance:
525 warning("The resistance network leaves out these wells, as the tech info has no sheet resistance "
526 "for them, so the device terminals on them (e.g. the PMOS bulks) are ideally on their nets:\n" +
527 '\n'.join(f" - {well}" for well in sorted(wells_without_resistance)))
529 if unmodeled_layers:
530 raise RExtractionTechError(
531 "The tech info can't model these layers of the layout for the resistance extraction, "
532 "so their connections would be missing from the resistance network:\n" +
533 '\n'.join(f" - {layer}" for layer in unmodeled_layers)
534 )
536 # NOTE: a via joins the conductors below and above it, but the layout may have none on one side,
537 # e.g. a via without metal below, or a tech info that puts the layer below on another GDS pair
538 # (gf180mcuD before #229), so the resistance network would miss the connection there
539 conductor_layer_ids = {c.layer.id for c in rex_tech.conductors}
540 vias_without_conductor: Set[str] = set()
541 for via in rex_tech.vias:
542 for side, conductor in (('below', via.bottom_conductor), ('above', via.top_conductor)):
543 if conductor.id in conductor_layer_ids:
544 continue
545 layer_info = self.pex_context.annotated_layout.get_info(conductor.id)
546 gds_pair = (layer_info.layer, layer_info.datatype)
547 layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair.get(gds_pair, '?')
548 problem = 'has no shapes' if gds_pair not in self.pex_context.extracted_layers else 'is no conductor'
549 vias_without_conductor.add(f"{via.layer.canonical_layer_name} (LVS {via.layer.lvs_layer_name}): "
550 f"the layer {side} it, {layer_name} {gds_pair}, {problem}")
552 if vias_without_conductor:
553 raise RExtractionTechError(
554 "These vias have no conductor to join on one side, "
555 "so their connections would be missing from the resistance network:\n" +
556 '\n'.join(f" - {via}" for via in sorted(vias_without_conductor))
557 )
559 if self.exclusions is not None:
560 self.add_ideal_layers(rex_tech)
562 return rex_tech
564 @staticmethod
565 def add_ideal_layers(rex_tech: pb_RExtractorTech):
566 """
567 Adds a copy without resistance of each conductor and via, for what is within an exclusion marker
568 (see split_at_exclusions)
569 """
570 for conductor in list(rex_tech.conductors):
571 ideal = rex_tech.conductors.add()
572 ideal.CopyFrom(conductor)
573 ideal.layer.id = ideal_layer_id(conductor.layer.id)
574 ideal.resistance = 0
575 for via in list(rex_tech.vias):
576 ideal = rex_tech.vias.add()
577 ideal.CopyFrom(via)
578 ideal.layer.id = ideal_layer_id(via.layer.id)
579 ideal.bottom_conductor.id = ideal_layer_id(via.bottom_conductor.id)
580 ideal.top_conductor.id = ideal_layer_id(via.top_conductor.id)
581 ideal.resistance = 0
583 def prepare_request(self) -> pex_request_pb2.RExtractionRequest:
584 rex_request = pex_request_pb2.RExtractionRequest()
586 # prepare tech info
587 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech)
589 # prepare devices
590 devices_by_name = self.pex_context.devices_by_name
591 rex_request.devices.MergeFrom(devices_by_name.values())
593 # prepare pins
594 for pin_list in self.pex_context.pins_pb2_by_layer.values():
595 rex_request.pins.MergeFrom(pin_list)
597 net_request_by_name: Dict[NetName, pex_request_pb2.RNetExtractionRequest] = {}
598 def get_or_create_net_request(net_name: str):
599 v = net_request_by_name.get(net_name, None)
600 if not v:
601 v = rex_request.net_extraction_requests.add()
602 v.net_name = net_name
603 net_request_by_name[net_name] = v
604 return v
606 for pin in rex_request.pins:
607 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin)
609 for device in rex_request.devices:
610 for terminal in device.terminals:
611 get_or_create_net_request(terminal.net_name).device_terminals.add().CopyFrom(terminal)
613 netlist = self.pex_context.lvsdb.netlist()
614 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name)
615 # https://www.klayout.de/doc-qt5/code/class_Circuit.html
616 if not circuit:
617 circuits = [c.name for c in netlist.each_circuit()]
618 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, "
619 f"only available circuits are: {circuits}")
620 LK = tech_pb2.ComputedLayerInfo.Kind
621 for net in circuit.each_net():
622 net_name = net.name or f"${net.cluster_id}"
623 for lvs_gds_pair, lyr_info in self.pex_context.extracted_layers.items():
624 for lyr in lyr_info.source_layers:
625 li = self.pex_context.tech.computed_layer_info_by_gds_pair[lyr.gds_pair]
626 match li.kind:
627 case LK.KIND_PIN:
628 continue # skip
629 case LK.KIND_REGULAR | LK.KIND_DEVICE_CAPACITOR | LK.KIND_DEVICE_CAPACITOR_PLATE | \
630 LK.KIND_DEVICE_RESISTOR:
631 r = self.pex_context.shapes_of_net(lyr.gds_pair, net)
632 if not r:
633 continue
634 l2r = get_or_create_net_request(net_name).region_by_layer.add()
635 l2r.layer.id = self.pex_context.annotated_layout.layer(*lvs_gds_pair)
636 l2r.layer.canonical_layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair]
637 l2r.layer.lvs_layer_name = lyr.lvs_layer_name
638 self.shapes_converter.klayout_region_to_pb(r, l2r.region)
639 case _:
640 raise NotImplementedError()
642 return rex_request
644 @staticmethod
645 def node_name(rn: klp.RNode, layer_name: LayerName, index: int) -> str:
646 """
647 The name of a node of the resistor network, unique within the network (but pins),
648 e.g. $1.li1 (internal node), P0.poly (polygon port), V0.met1 (vertex port)
650 NOTE: KLayout names a node after its layer index (e.g. $1.17), but the layer indexes depend on
651 the order KLayout creates the LVS layers in, which differs from run to run
653 :param index: the port index of a port, the index of an internal node (see internal_node_indexes)
654 """
655 match rn.type():
656 case klp.RNodeType.Internal:
657 prefix = '$'
658 case klp.RNodeType.VertexPort:
659 prefix = 'V'
660 case klp.RNodeType.PolygonPort:
661 prefix = 'P'
662 case _:
663 raise NotImplementedError()
664 return f"{prefix}{index}.{layer_name}"
666 def split_at_exclusions(self,
667 tech: pb_RExtractorTech,
668 regions: Dict[int, kdb.Region],
669 vertex_ports: Dict[int, List[kdb.Point]],
670 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]],
671 polygon_ports: Dict[int, List[kdb.Polygon]],
672 polygon_port_device_terminals: PolygonPortTerminals,
673 layer_names: Dict[int, LayerName]) -> NetSplit:
674 """
675 Takes the shapes of a net within the exclusion markers out of its network.
676 A polygon port on the conductor beside each cut is its node, as a whole cut is one node, also of a wide wire.
678 Either these become ports of the netlist, for the model of what the markers stand for,
679 like the device terminals within the markers, which the model connects then.
681 Or the shapes go to layers without resistance (ideal_layer_id), so that the extraction still tells what they
682 connect, with their pins and device terminals, and a polygon port on them joins each cut to them.
684 NOTE: a via is within a block by its center, which is the location of its nodes, on both of its conductors,
685 otherwise it keeps its resistance and the metal it lands on, as a via port needs its center on the metal,
686 and the resistance of a part of it would be made up
688 :param tech: the conductors and vias, with their ideal copies
689 :param regions: the shapes of the net by layer ID
690 :param vertex_ports: the pins by layer ID
691 :param vertex_port_pins: what the pins are of, by layer ID
692 :param polygon_ports: the device terminals by layer ID
693 :param polygon_port_device_terminals: what the polygon ports are of
694 :param layer_names: the canonical names of the conductors and vias, by layer ID
695 """
696 split = NetSplit()
697 exclusions = self.exclusions
699 def is_ideal(layer_id: int) -> bool:
700 return layer_id >= IDEAL_LAYER_ID_OFFSET
702 # NOTE: the device terminals of the removed devices are no ports of devices anymore
703 removed_terminals: List[Tuple[int, kdb.Polygon, device_pb2.Device.Terminal]] = []
704 removed_terminal_shapes: Dict[int, kdb.Shapes] = defaultdict(kdb.Shapes)
705 for layer_id in list(polygon_ports):
706 ports_and_terminals = list(zip(polygon_ports.pop(layer_id), polygon_port_device_terminals.pop(layer_id)))
707 for port, terminal in ports_and_terminals:
708 if terminal.device_id in exclusions.removed_device_blocks:
709 removed_terminal_shapes[layer_id].insert(kdb.PolygonWithProperties(
710 port, {'terminal': len(removed_terminals)}))
711 removed_terminals.append((layer_id, port, terminal))
712 else:
713 polygon_ports[layer_id].append(port)
714 polygon_port_device_terminals[layer_id].append(terminal)
716 kept_for_vias: Dict[int, kdb.Region] = defaultdict(kdb.Region)
717 for via in {v.layer.id: v for v in tech.vias if not is_ideal(v.layer.id)}.values():
718 shapes = regions.get(via.layer.id, None)
719 # NOTE: most nets are far from the blocks
720 if shapes is None or not shapes.bbox().touches(exclusions.bbox):
721 continue
722 bottom_layer_name = layer_names[via.bottom_conductor.id]
723 top_layer_name = layer_names[via.top_conductor.id]
724 # NOTE: within a block on both of its conductors
725 box = shapes.bbox()
726 within_tie = polygons_by_center_inside(shapes, exclusions.mask_near(box, bottom_layer_name, ports=False) &
727 exclusions.mask_near(box, top_layer_name, ports=False))
728 within_ports = polygons_by_center_inside(shapes, exclusions.mask_near(box, bottom_layer_name, ports=True) &
729 exclusions.mask_near(box, top_layer_name, ports=True))
730 if not within_tie.is_empty():
731 regions[ideal_layer_id(via.layer.id)] = within_tie
732 if not within_ports.is_empty():
733 split.vias_within.append((bottom_layer_name, top_layer_name, within_ports))
734 rest = shapes - within_tie - within_ports
735 regions[via.layer.id] = rest
736 across = rest.interacting(exclusions.mask_near(box, bottom_layer_name) +
737 exclusions.mask_near(box, top_layer_name))
738 kept_for_vias[via.bottom_conductor.id] += across
739 kept_for_vias[via.top_conductor.id] += across
741 for layer_id in sorted({c.layer.id for c in tech.conductors if not is_ideal(c.layer.id)}):
742 shapes = regions.get(layer_id, None)
743 if shapes is None or not shapes.bbox().touches(exclusions.bbox):
744 continue
745 layer_name = layer_names[layer_id]
746 box = shapes.bbox()
747 mask_ports = exclusions.mask_near(box, layer_name, ports=True) - kept_for_vias[layer_id]
748 mask_tie = exclusions.mask_near(box, layer_name, ports=False) - kept_for_vias[layer_id] - mask_ports
749 within_ports = shapes & mask_ports
750 within_tie = shapes & mask_tie
751 if within_tie.is_empty() and within_ports.is_empty():
752 continue
753 # NOTE: without the masks rather than what is within them, as any-angle edges round differently,
754 # which leaves slivers along the cuts otherwise, that join what is on both sides of a block
755 outside = shapes - mask_ports - mask_tie
756 ideal_id = ideal_layer_id(layer_id)
757 regions[layer_id] = outside
758 split.kept_for_vias[layer_name] = kept_for_vias[layer_id]
759 if not within_tie.is_empty():
760 regions[ideal_id] = within_tie
761 for polygon in within_ports.merged().each():
762 block_indexes = exclusions.block_indexes_with_ports_within(kdb.Region(polygon.downcast()), layer_name)
763 if len(block_indexes) > 1:
764 split.abutting_block_indexes.update(block_indexes)
765 region_by_layer = split.regions_within[block_indexes[0]]
766 region_by_layer.setdefault(layer_name, kdb.Region()).insert(polygon.downcast())
768 # NOTE: the pins and device terminals go with the conductor they are on, those on both to both of them,
769 # which are one node, as the ports of a device terminal are
770 for point, pin in zip(vertex_ports.pop(layer_id, []), vertex_port_pins.pop(layer_id, [])):
771 label_box = kdb.Region(kdb.Box(point, point).enlarged(1, 1))
772 # NOTE: a pin of a cut cell is none of the top cell, but it names the port of the model,
773 # also where its metal stays for a via from above, which can land on it
774 if exclusions.is_only_label_of_cut_cell(pin[0], point) and \
775 not (label_box & exclusions.mask_near(label_box.bbox(), layer_name, ports=True)).is_empty():
776 continue
777 if not (label_box & outside).is_empty():
778 vertex_ports[layer_id].append(point)
779 vertex_port_pins[layer_id].append(pin)
780 elif not (label_box & within_ports).is_empty():
781 split.pins_within.append(PinWithinMarker(point, layer_id, *pin))
782 else:
783 vertex_ports[ideal_id].append(point)
784 vertex_port_pins[ideal_id].append(pin)
786 def add_port(port_layer_id: int,
787 polygon: kdb.Polygon,
788 device_terminal: Optional[device_pb2.Device.Terminal] = None) -> PolygonPortKey:
789 polygon_ports[port_layer_id].append(polygon.downcast())
790 polygon_port_device_terminals[port_layer_id].append(device_terminal)
791 return port_layer_id, len(polygon_ports[port_layer_id]) - 1
793 for port, terminal in zip(polygon_ports.pop(layer_id, []), polygon_port_device_terminals.pop(layer_id, [])):
794 port_region = kdb.Region(port)
795 part_tie = port_region & within_tie
796 part_ports = port_region & within_ports
797 for polygon in (port_region - part_tie - part_ports).each():
798 add_port(layer_id, polygon, terminal)
799 for polygon in part_tie.each():
800 add_port(ideal_id, polygon, terminal)
801 for polygon in part_ports.each():
802 split.terminals_within.append(TerminalWithinMarker(terminal, layer_id, polygon.bbox()))
804 # NOTE: strips of 1 dbu, like the device terminals that abut a conductor
805 # (see KLayoutExtractionContext.terminal_region_on_net),
806 # wider at any angle, where the extractor can't split such a narrow polygon into triangles
807 strip_width = CUT_STRIP_WIDTH if (outside + within_ports + within_tie).non_rectilinear().is_empty() \
808 else ANY_ANGLE_CUT_STRIP_WIDTH
809 around_ports = within_ports.sized(strip_width)
811 # NOTE: also where what is within a marker without ports goes on into a block with ports
812 conductors_near = RegionIndex()
813 for strip_layer_id, conductor in ((layer_id, outside), (ideal_id, within_tie)):
814 for strip in (conductor & around_ports).merged().each():
815 port = port_of_cut(strip, lambda box: conductors_near.near(conductor, box), strip_width,
816 round(PORT_LENGTH_ALONG_CONDUCTOR / self.pex_context.dbu))
817 split.has_cut_along_conductor |= port.bbox() != strip.bbox()
818 key = add_port(strip_layer_id, port)
819 split.cut_port_boxes[key] = port.bbox()
820 split.cut_port_blocks[key] = exclusions.block_index_with_ports_at(port.bbox(), layer_name)
821 for shape in removed_terminal_shapes[layer_id].each_touching(kdb.Shapes.SRegions, port.bbox()):
822 terminal = removed_terminals[shape.property('terminal')][2]
823 if exclusions.removed_device_blocks[terminal.device_id] == split.cut_port_blocks[key] and \
824 not (kdb.Region(shape.polygon) & kdb.Region(port.downcast())).is_empty():
825 split.terminals_of_cuts[key] = terminal
826 break
827 for outside_strip in (outside & within_tie.sized(strip_width) - around_ports).merged().each():
828 outside_key = add_port(layer_id, outside_strip)
829 for within_strip in (within_tie & kdb.Region(outside_strip).sized(strip_width)).merged().each():
830 split.polygon_port_ties.append((outside_key, add_port(ideal_id, within_strip)))
832 # NOTE: a removed device stands for what its model has, and where it meets the rest of its net is a port,
833 # e.g. the terminal of a coil on its lead outside its marker
834 conductor_layer_ids = {c.layer.id for c in tech.conductors}
835 for layer_id, port, terminal in removed_terminals:
836 shapes = regions.get(layer_id, None)
837 if layer_id not in conductor_layer_ids or shapes is None or shapes.is_empty():
838 continue # NOTE: e.g. the bulk of a transistor on the substrate
839 for polygon in (kdb.Region(port) & shapes).merged().each():
840 polygon_ports[layer_id].append(polygon.downcast())
841 polygon_port_device_terminals[layer_id].append(None)
842 key = (layer_id, len(polygon_ports[layer_id]) - 1)
843 split.cut_port_boxes[key] = polygon.bbox()
844 split.cut_port_blocks[key] = exclusions.removed_device_blocks[terminal.device_id]
845 split.cut_port_terminals[key] = terminal
846 return split
848 @staticmethod
849 def sets_of_cuts(split: NetSplit,
850 connected_shapes: ConnectedShapesWithin,
851 layer_names: Dict[int, LayerName]) -> Dict[PolygonPortKey, int]:
852 """
853 :return: what is joined to the shapes within the blocks at each cut, by its polygon port,
854 a cut without such shapes has none (e.g. of a terminal of a removed device, on a wire to its block)
855 """
856 sets_of_cuts: Dict[PolygonPortKey, int] = {}
857 for key, cut_box in split.cut_port_boxes.items():
858 cut_set = connected_shapes.set_at(layer_names[key[0]], cut_box.enlarged(1, 1))
859 if cut_set is not None:
860 sets_of_cuts[key] = cut_set
861 return sets_of_cuts
863 def exclusion_port_names(self,
864 net_name: NetName,
865 split: NetSplit,
866 connected_shapes: ConnectedShapesWithin,
867 sets_of_cuts: Mapping[PolygonPortKey, int],
868 layer_names: Dict[int, LayerName],
869 labels_outside: Collection[Label]) -> Tuple[Dict[PolygonPortKey, str], Dict[int, str]]:
870 """
871 The names of the ports of a net at the exclusion blocks: a cut has the name of the pin of the net
872 within the block that is nearest to it (e.g. of a lead of a coil), otherwise <net>__<block>_<number>,
873 like the device terminals within the markers.
875 At a cut cell, it can also be a label of the cell on the net, <block>__<label>, also for several cuts,
876 as these are one pin of its model, or the terminal of a device of it that the cut is on,
877 <block>__<terminal>, numbered if devices have several such nodes, otherwise <block>__<net>_<number>
879 A pin or a label only names a cut of the shapes it is joined to within the block, as a port is one node.
880 So does a pin of a label the net also has outside the block only when that isn't the same name,
881 which would be one node with that pin
883 :param connected_shapes: of the shapes within the blocks
884 :param sets_of_cuts: see sets_of_cuts
885 :param labels_outside: the labels of the pins of the net outside the blocks
886 :return: the names of the cuts by their polygon ports,
887 and the names of the device terminals within the markers by their indexes in split.terminals_within
888 """
889 blocks = self.exclusions.blocks
890 safe_net_name = re.sub(r'[^A-Za-z0-9_]', '_', net_name) # NOTE: e.g. A,B of several labels, $2 of none
892 def block_index_of_pin(pin: PinWithinMarker) -> int:
893 return self.exclusions.block_index_with_ports_at(kdb.Box(pin.position, pin.position),
894 layer_names[pin.layer_id])
896 cut_keys = sorted(split.cut_port_boxes,
897 key=lambda key: (split.cut_port_blocks[key],
898 split.cut_port_boxes[key].bottom, split.cut_port_boxes[key].left,
899 layer_names[key[0]]))
901 def distance(position: kdb.Point, box: kdb.Box) -> float:
902 center = box.center()
903 return math.hypot(position.x - center.x, position.y - center.y)
905 # the candidates of the names of each cut: whether on another layer, the distance, the name
906 candidates_by_cut: Dict[PolygonPortKey, List[Tuple[bool, float, str]]] = defaultdict(list)
907 pin_sets = [connected_shapes.set_at_position(layer_names[pin.layer_id], pin.position)
908 for pin in split.pins_within]
909 labels_with_sets = {block_index: [(label, connected_shapes.set_at_position(label.layer_name, label.position))
910 for label in self.labels_at_shapes(block_index, split)]
911 for block_index in set(split.cut_port_blocks.values())}
912 for key in cut_keys:
913 # NOTE: a port at the terminal of a removed device is named after that terminal
914 if key in split.cut_port_terminals:
915 continue
916 block_index = split.cut_port_blocks[key]
917 cut_box = split.cut_port_boxes[key]
918 cut_set = sets_of_cuts.get(key, None)
919 if cut_set is None:
920 continue
921 for pin, pin_set in zip(split.pins_within, pin_sets):
922 if pin_set == cut_set and pin.label not in labels_outside and block_index_of_pin(pin) == block_index:
923 candidates_by_cut[key].append((pin.layer_id != key[0],
924 distance(pin.position, cut_box),
925 pin.label))
926 for label, label_set in labels_with_sets[block_index]:
927 if label_set == cut_set:
928 candidates_by_cut[key].append((label.layer_name != layer_names[key[0]],
929 distance(label.position, cut_box),
930 f"{blocks[block_index].name}__{label.text}"))
932 # NOTE: the nearest pairs first, on the layer of the pin before others, each pin names one cut
933 names_by_cut: Dict[PolygonPortKey, str] = {}
934 names_left = {candidate[2] for candidates in candidates_by_cut.values() for candidate in candidates}
935 for _, _, name, _, key in sorted((*candidate, index, key)
936 for index, key in enumerate(cut_keys)
937 for candidate in candidates_by_cut[key]):
938 if name in names_left and key not in names_by_cut:
939 names_by_cut[key] = name
940 names_left.discard(name)
942 numbers: Counter[int] = Counter()
943 taken_names: Set[str] = {candidate[2] for candidates in candidates_by_cut.values() for candidate in candidates}
944 taken_names.update(labels_outside)
945 taken_names.update(pin.label for pin in split.pins_within)
947 def numbered_name(block_index: int) -> str:
948 while True:
949 number = numbers[block_index]
950 numbers[block_index] += 1
951 name = f"{blocks[block_index].name}__{safe_net_name}_{number}" \
952 if blocks[block_index].kind == BlockKind.CUT else \
953 f"{safe_net_name}__{blocks[block_index].name}_{number}"
954 if name not in taken_names:
955 taken_names.add(name)
956 return name
958 for key in cut_keys:
959 if key in names_by_cut:
960 continue
961 block_index = split.cut_port_blocks[key]
962 cut_candidates = [c for c in candidates_by_cut[key] if c[2].startswith(f"{blocks[block_index].name}__")]
963 if blocks[block_index].kind == BlockKind.CUT and cut_candidates:
964 names_by_cut[key] = min(cut_candidates)[2]
965 elif key not in split.cut_port_terminals and key not in split.terminals_of_cuts:
966 names_by_cut[key] = numbered_name(block_index)
968 names_by_cut.update(self.terminal_port_names(split, [key for key in cut_keys if key not in names_by_cut],
969 taken_names))
971 def name_at_terminal_of_removed_device(within: TerminalWithinMarker) -> Optional[str]:
972 # NOTE: a diffusion on which a terminal of a removed device also is, which needs no second port
973 for key, terminal_box in split.cut_port_boxes.items():
974 if key in split.cut_port_terminals and key[0] == within.layer_id and terminal_box.touches(within.box):
975 return names_by_cut[key]
976 return None
978 # NOTE: the polygons of a device terminal are one port
979 names_by_terminal: Dict[Tuple[int, int], str] = {}
980 names_by_terminal_index: Dict[int, str] = {}
981 for index, within in enumerate(split.terminals_within):
982 key = (within.terminal.device_id, within.terminal.terminal_id)
983 if key not in names_by_terminal:
984 names_by_terminal[key] = name_at_terminal_of_removed_device(within) or \
985 numbered_name(self.exclusions.block_index_with_ports_at(within.box, layer_names[within.layer_id]))
986 names_by_terminal_index[index] = names_by_terminal[key]
987 return names_by_cut, names_by_terminal_index
989 def labels_at_shapes(self, block_index: int, split: NetSplit) -> List[BlockLabel]:
990 """
991 :return: the labels of the block at the shapes of the net within it,
992 also at its metal kept for vias, which can be in a hole of them or beside them
993 """
994 labels: Dict[int, BlockLabel] = {}
995 for layer_name, region in split.regions_within.get(block_index, {}).items():
996 for shapes in (region, split.kept_for_vias.get(layer_name, None)):
997 if shapes is not None:
998 for polygon in shapes.merged().each():
999 labels.update(self.exclusions.labels_at(block_index, layer_name, polygon.bbox()))
1000 return [labels[index] for index in sorted(labels)]
1002 def terminal_port_names(self,
1003 split: NetSplit,
1004 keys: Sequence[PolygonPortKey],
1005 taken_names: Set[str]) -> Dict[PolygonPortKey, str]:
1006 """
1007 A port is one node: of a terminal of a device, also of several polygons,
1008 and of the terminals of that name of the other devices of the block on the same polygon
1010 :param keys: the polygon ports at the terminals of removed devices
1011 :param taken_names: the names of the other ports of the net, to which these go too
1012 :return: their names, <block>__<terminal>, numbered if the block has several such nodes on the net
1013 """
1014 blocks = self.exclusions.blocks
1015 terminals = {**split.terminals_of_cuts, **split.cut_port_terminals}
1016 ports = DisjointSets(keys)
1017 first_key_by_node: Dict[Hashable, PolygonPortKey] = {}
1018 for key in keys:
1019 terminal = terminals[key]
1020 for node in ((terminal.device_id, terminal.terminal_id),
1021 (split.cut_port_blocks[key], terminal.name, key[0], split.cut_port_boxes[key])):
1022 ports.union(key, first_key_by_node.setdefault(node, key))
1024 roots_by_name: Dict[str, List[PolygonPortKey]] = defaultdict(list)
1025 for key in keys:
1026 name = f"{blocks[split.cut_port_blocks[key]].name}__{terminals[key].name}"
1027 root = ports.find(key)
1028 if root not in roots_by_name[name]:
1029 roots_by_name[name].append(root)
1031 name_by_root: Dict[PolygonPortKey, str] = {}
1032 for name, roots in roots_by_name.items():
1033 if len(roots) == 1 and name not in taken_names:
1034 name_by_root[roots[0]] = name
1035 taken_names.add(name)
1036 continue
1037 numbers = (f"{name}_{number}" for number in itertools.count())
1038 for root in roots:
1039 name_by_root[root] = next(number for number in numbers if number not in taken_names)
1040 taken_names.add(name_by_root[root])
1041 return {key: name_by_root[ports.find(key)] for key in keys}
1043 def internal_node_indexes(self,
1044 resistor_network: klp.RNetwork,
1045 layer_names: Dict[int, LayerName],
1046 excluded_node_ids: Set[int]) -> Dict[int, int]:
1047 """
1048 The index of each internal node of a resistor network, by its object ID:
1049 in the order of their layers and locations, nodes of the same location by their elements
1051 NOTE: KLayout numbers the internal nodes in the order it creates them, which can differ from run to run
1052 (e.g. KLayout 0.30.12), and their names decide ties in the distribution of the capacitances (#286)
1053 """
1054 dbu = self.pex_context.dbu
1056 def location(rn: klp.RNode) -> Tuple[LayerName, str, int, int, int, int, int]:
1057 if rn.type() != klp.RNodeType.Internal:
1058 # NOTE: a port by its index, as KLayout's location of a vertex port can differ from run to run
1059 return layer_names[rn.layer()], str(rn.type()), rn.port_index(), 0, 0, 0, 0
1060 box = rn.location()
1061 p1 = box.p1.to_itype(dbu)
1062 p2 = box.p2.to_itype(dbu)
1063 return layer_names[rn.layer()], str(rn.type()), -1, p1.x, p1.y, p2.x, p2.y
1065 neighbors: Dict[int, List[Tuple[float, Tuple]]] = defaultdict(list)
1066 for el in resistor_network.each_element():
1067 neighbors[el.a().object_id()].append((el.resistance(), location(el.b())))
1068 neighbors[el.b().object_id()].append((el.resistance(), location(el.a())))
1070 keys = {rn.object_id(): (location(rn), sorted(neighbors[rn.object_id()]))
1071 for rn in resistor_network.each_node()
1072 if rn.type() == klp.RNodeType.Internal and rn.object_id() not in excluded_node_ids}
1073 return {node_id: index for index, node_id in enumerate(sorted(keys, key=keys.get))}
1075 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult:
1076 rex_result = pex_result_pb2.RExtractionResult()
1078 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech)
1080 # dicts keyed by id / klayout_index
1081 layer_names: Dict[int, LayerName] = {}
1083 wire_layer_ids: Set[int] = set()
1084 via_layer_ids: Set[int] = set()
1086 for c in rex_request.tech.conductors:
1087 layer_names[c.layer.id] = c.layer.canonical_layer_name
1088 wire_layer_ids.add(c.layer.id)
1090 for v in rex_request.tech.vias:
1091 layer_names[v.layer.id] = v.layer.canonical_layer_name
1092 via_layer_ids.add(v.layer.id)
1094 # NOTE: the nodes of a layer are named after it, and those of an ideal layer have to differ from them
1095 node_layer_names: Dict[int, str] = {layer_id: f"{name}.excluded" if layer_id >= IDEAL_LAYER_ID_OFFSET else name
1096 for layer_id, name in layer_names.items()}
1098 LP = tech_pb2.LayerInfo.Purpose
1099 well_layer_ids: Set[int] = set()
1100 for c in rex_request.tech.conductors:
1101 computed_layer_info = self.pex_context.tech.computed_layer_info_by_name.get(c.layer.lvs_layer_name, None)
1102 if computed_layer_info and computed_layer_info.layer_info.purpose in (LP.PURPOSE_NWELL, LP.PURPOSE_PWELL):
1103 well_layer_ids.add(c.layer.id)
1105 nets_with_isolated_wells: List[NetName] = []
1106 nets_with_cuts_along_conductors: List[NetName] = []
1108 for net_extraction_request in rex_request.net_extraction_requests:
1109 if self.exclusions is not None and net_extraction_request.net_name in self.exclusions.excluded_net_names:
1110 continue
1112 vertex_ports: Dict[int, List[kdb.Point]] = defaultdict(list)
1113 polygon_ports: Dict[int, List[kdb.Polygon]] = defaultdict(list)
1114 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]] = defaultdict(list)
1115 polygon_port_device_terminals: PolygonPortTerminals = defaultdict(list)
1116 regions: Dict[int, kdb.Region] = defaultdict(kdb.Region)
1118 for t in net_extraction_request.device_terminals:
1119 for l2r in t.region_by_layer:
1120 for sh in l2r.region.shapes:
1121 sh_kly = self.shapes_converter.klayout_shape(sh)
1122 polygon_ports[l2r.layer.id].append(sh_kly)
1123 polygon_port_device_terminals[l2r.layer.id].append(t)
1125 for pin in net_extraction_request.pins:
1126 p = self.shapes_converter.klayout_point(pin.label_point)
1127 vertex_ports[pin.layer.id].append(p)
1128 vertex_port_pins[pin.layer.id].append((pin.label, pin.net_name))
1130 for l2r in net_extraction_request.region_by_layer:
1131 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region)
1133 split = NetSplit()
1134 if self.exclusions is not None:
1135 split = self.split_at_exclusions(rex_request.tech, regions,
1136 vertex_ports, vertex_port_pins,
1137 polygon_ports, polygon_port_device_terminals,
1138 layer_names)
1139 if split.has_cut_along_conductor:
1140 nets_with_cuts_along_conductors.append(net_extraction_request.net_name)
1141 if split.abutting_block_indexes:
1142 block_names = sorted(self.exclusions.blocks[index].name for index in split.abutting_block_indexes)
1143 warning(f"The net {net_extraction_request.net_name} goes on from one to another of "
1144 f"{listed_names(block_names)}, where they abut, so the netlist has no ports for their "
1145 f"models there")
1147 keep_pins_of_top_circuit(net_extraction_request.net_name, vertex_ports, vertex_port_pins)
1149 rex = klp.RNetExtractor(self.pex_context.dbu)
1150 try:
1151 resistor_network = rex.extract(rex_tech_kly,
1152 regions,
1153 vertex_ports,
1154 polygon_ports)
1155 except RuntimeError as exc:
1156 raise RExtractionTechError(f"KLayout's resistance extraction fails for net "
1157 f"{net_extraction_request.net_name}: {exc}") from exc
1159 result_network = rex_result.networks.add()
1160 result_network.net_name = net_extraction_request.net_name
1162 polygon_port_node_ids = {(rn.layer(), rn.port_index()): rn.object_id()
1163 for rn in resistor_network.each_node()
1164 if rn.type() == klp.RNodeType.PolygonPort}
1165 node_id_ties = [(polygon_port_node_ids[a], polygon_port_node_ids[b]) for a, b in split.polygon_port_ties]
1167 cut_port_names: Dict[PolygonPortKey, str] = {}
1168 terminal_port_names: Dict[int, str] = {}
1169 sets_of_cuts: Dict[PolygonPortKey, int] = {}
1170 connected_shapes: Optional[ConnectedShapesWithin] = None
1171 if split.cut_port_boxes or split.terminals_within or split.pins_within:
1172 connected_shapes = ConnectedShapesWithin(split.regions_within, split.vias_within, split.kept_for_vias)
1173 sets_of_cuts = self.sets_of_cuts(split, connected_shapes, layer_names)
1174 labels_outside = {pin[0] for pins in vertex_port_pins.values() for pin in pins}
1175 cut_port_names, terminal_port_names = self.exclusion_port_names(result_network.net_name, split,
1176 connected_shapes, sets_of_cuts,
1177 layer_names, labels_outside)
1179 pieces = self.network_pieces(resistor_network, node_id_ties)
1181 # NOTE: LVS may join a well to its net other than by the contacts over its taps,
1182 # e.g. by a label of the net on the well, so it's a piece of the network that joins nothing else.
1183 # It's left out, so that the device terminals on it stay on the node of their net, ideally
1184 isolated_well_node_ids = self.isolated_well_node_ids(pieces, well_layer_ids, split.cut_port_boxes)
1185 if isolated_well_node_ids:
1186 nets_with_isolated_wells.append(result_network.net_name)
1188 internal_node_indexes = self.internal_node_indexes(resistor_network, node_layer_names,
1189 isolated_well_node_ids)
1191 for rn in resistor_network.each_node():
1192 if rn.object_id() in isolated_well_node_ids:
1193 continue
1195 node_by_node_id: Dict[int, r_network_pb2.RNode] = {}
1197 loc = rn.location()
1198 layer_id = rn.layer()
1199 canonical_layer_name = layer_names[layer_id]
1201 r_node = result_network.nodes.add()
1202 r_node.node_id = rn.object_id()
1203 r_node.node_name = self.node_name(rn, node_layer_names[layer_id],
1204 internal_node_indexes.get(rn.object_id(), rn.port_index()))
1205 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO!
1206 r_node.layer_name = canonical_layer_name
1208 match rn.type():
1209 case klp.RNodeType.VertexPort: # pins!
1210 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_POINT
1211 # NOTE: at its label, the vertex port, rather than where KLayout locates the node,
1212 # which can differ from run to run (e.g. KLayout 0.30.12), and so would
1213 # the nodes the capacitances near it go to (#286)
1214 p = vertex_ports[rn.layer()][rn.port_index()]
1215 r_node.location.point.x = p.x
1216 r_node.location.point.y = p.y
1217 case klp.RNodeType.PolygonPort | klp.RNodeType.Internal:
1218 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX
1219 p1 = loc.p1.to_itype(self.pex_context.dbu)
1220 p2 = loc.p2.to_itype(self.pex_context.dbu)
1221 r_node.location.box.lower_left.x = p1.x
1222 r_node.location.box.lower_left.y = p1.y
1223 r_node.location.box.upper_right.x = p2.x
1224 r_node.location.box.upper_right.y = p2.y
1225 case _:
1226 raise NotImplementedError()
1228 match rn.type():
1229 case klp.RNodeType.VertexPort:
1230 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_PIN
1231 port_idx = rn.port_index()
1232 r_node.node_name, r_node.net_name = vertex_port_pins[rn.layer()][port_idx][0:2]
1233 r_node.location.point.net = r_node.net_name
1235 case klp.RNodeType.PolygonPort:
1236 port_idx = rn.port_index()
1237 device_terminal = polygon_port_device_terminals[rn.layer()][port_idx]
1238 cut_port_name = cut_port_names.get((rn.layer(), port_idx), None)
1239 if cut_port_name is not None:
1240 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_EXCLUSION_PORT
1241 r_node.node_name = cut_port_name
1242 split.block_of_node_id[r_node.node_id] = split.cut_port_blocks[(rn.layer(), port_idx)]
1243 elif device_terminal is None: # NOTE: at a cut, see split_at_exclusions
1244 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION
1245 else:
1246 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL
1247 # NOTE: the device terminal behind the port, to connect the device to this node
1248 r_node.device_terminal.CopyFrom(device_terminal)
1249 r_node.device_terminal.ClearField('region_by_layer')
1250 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}"
1251 r_node.location.box.net = r_node.net_name
1252 case klp.RNodeType.Internal:
1253 if rn.layer() in via_layer_ids:
1254 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_VIA_JUNCTION
1255 elif rn.layer() in wire_layer_ids:
1256 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION
1257 else:
1258 raise NotImplementedError()
1260 # NOTE: network prefix, as node name is only unique per network
1261 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}"
1262 r_node.location.box.net = r_node.net_name
1263 case _:
1264 raise NotImplementedError()
1266 node_by_node_id[r_node.node_id] = r_node
1268 # NOTE: nodes of the same name are one node in the netlist, like the pins of the same label,
1269 # so the names of the other nodes must be unique
1270 # (the internal nodes are numbered per network, the ports per layer, so no 2 layers
1271 # of the same name must have ports, e.g. a conductor split into several GDS pairs)
1272 # NOTE: and so are the ports of the cuts of one pin of a cut cell
1273 name_counts = Counter(n.node_name for n in result_network.nodes
1274 if n.node_kind not in (r_network_pb2.RNode.Kind.KIND_PIN,
1275 r_network_pb2.RNode.Kind.KIND_EXCLUSION_PORT))
1276 duplicate_names = sorted(name for name, count in name_counts.items() if count > 1)
1277 if duplicate_names:
1278 raise RExtractionTechError(f"Nodes of net {result_network.net_name} have the same name, "
1279 f"as layers of the same name have ports: {', '.join(duplicate_names)}")
1281 for el in resistor_network.each_element():
1282 if el.a().object_id() in isolated_well_node_ids:
1283 continue # NOTE: and so is b, of the same piece
1284 r_element = result_network.elements.add()
1285 r_element.element_id = el.object_id()
1286 r_element.node_a.node_id = el.a().object_id()
1287 r_element.node_b.node_id = el.b().object_id()
1288 r_element.resistance = el.resistance()
1290 for node_id_a, node_id_b in node_id_ties:
1291 if node_id_a in isolated_well_node_ids:
1292 continue # NOTE: and so is b, of the same piece
1293 self.add_tie(result_network, node_id_a, node_id_b)
1295 node_ids_by_set: Dict[int, List[int]] = defaultdict(list)
1296 for key, cut_set in sets_of_cuts.items():
1297 node_ids_by_set[cut_set].append(polygon_port_node_ids[key])
1299 nodes_within = self.add_nodes_within_markers(result_network, split, terminal_port_names, cut_port_names,
1300 layer_names, vertex_port_pins, connected_shapes,
1301 node_ids_by_set)
1302 if self.exclusions is not None:
1303 self.find_net_pieces(result_network, pieces, nodes_within, split, regions, layer_names, wire_layer_ids,
1304 well_layer_ids)
1306 if len(pieces) > 1 + sum(p[0].object_id() in isolated_well_node_ids for p in pieces):
1307 node_ids = self.substrate_joined_node_ids(result_network, regions, layer_names, well_layer_ids)
1308 result_network.substrate_joined_node_ids.extend(sorted(node_ids))
1310 for _, node_ids in sorted(node_ids_by_set.items()):
1311 if len(node_ids) > 1:
1312 result_network.model_joins.add().node_ids.extend(sorted(node_ids))
1314 if nets_with_cuts_along_conductors:
1315 info("The metal of these nets goes on beside exclusion blocks, along their cuts, so it carries the current "
1316 "alone, with a short port for each model, as a port can't connect a model in parallel to it: " +
1317 listed_names(sorted(nets_with_cuts_along_conductors)))
1319 if nets_with_isolated_wells:
1320 warning("The resistance network leaves out the wells of these nets, as no contact joins them "
1321 "to the rest of their net (e.g. a label of the net on the well does), "
1322 "so the device terminals on them (e.g. the PMOS bulks) are ideally on their nets: " +
1323 ', '.join(sorted(nets_with_isolated_wells)))
1325 return rex_result
1327 @staticmethod
1328 def add_tie(network: r_network_pb2.RNetwork, node_id_a: int, node_id_b: int):
1329 """
1330 Adds an element without resistance, which makes the nodes one (see NetworkNodeNames.from_network)
1331 """
1332 element = network.elements.add()
1333 element.element_id = node_id_a
1334 element.node_a.node_id = node_id_a
1335 element.node_b.node_id = node_id_b
1336 element.resistance = 0.0
1338 @classmethod
1339 def add_nodes_within_markers(cls,
1340 network: r_network_pb2.RNetwork,
1341 split: NetSplit,
1342 terminal_port_names: Dict[int, str],
1343 cut_port_names: Dict[PolygonPortKey, str],
1344 layer_names: Dict[int, LayerName],
1345 vertex_port_pins: Mapping[int, List[Tuple[Label, NetName]]],
1346 connected_shapes: Optional[ConnectedShapesWithin],
1347 node_ids_by_set: Dict[int, List[int]]) -> List[r_network_pb2.RNode]:
1348 """
1349 Adds the nodes of what is within the exclusion markers, for ports at the cuts, as no conductors are there:
1350 the device terminals with their ports, and the pins that name no cut, which are ports for the model too,
1351 but those that are pins of the net outside the markers as well
1353 NOTE: the extractor's node IDs are addresses, so these small ones are unique
1355 :param connected_shapes: of the shapes within the blocks, if there are such nodes
1356 :param node_ids_by_set: the ports by what is joined to them within the blocks (see ConnectedShapesWithin),
1357 to which these go too
1358 :return: the nodes
1359 """
1360 K = r_network_pb2.RNode.Kind
1361 next_node_id = 1
1362 nodes: List[r_network_pb2.RNode] = []
1364 def add_node(kind: r_network_pb2.RNode.Kind, name: str, layer_id: int, box: kdb.Box) -> r_network_pb2.RNode:
1365 nonlocal next_node_id
1366 node = network.nodes.add()
1367 nodes.append(node)
1368 node.node_id = next_node_id
1369 next_node_id += 1
1370 node.node_name = name
1371 node.node_kind = kind
1372 node.layer_name = layer_names[layer_id]
1373 node.net_name = f"{network.net_name}.{name}"
1374 node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX
1375 node.location.box.lower_left.x, node.location.box.lower_left.y = box.left, box.bottom
1376 node.location.box.upper_right.x, node.location.box.upper_right.y = box.right, box.top
1377 node.location.box.net = node.net_name
1378 return node
1380 def add_to_set(node_set: Optional[int], node_id: int):
1381 if node_set is not None:
1382 node_ids_by_set[node_set].append(node_id)
1384 port_node_id_by_name: Dict[str, int] = {}
1385 for index, within in enumerate(split.terminals_within):
1386 port_name = terminal_port_names[index]
1387 if port_name not in port_node_id_by_name:
1388 port_node_id_by_name[port_name] = add_node(K.KIND_EXCLUSION_PORT, port_name,
1389 within.layer_id, within.box).node_id
1390 add_to_set(connected_shapes.set_at(layer_names[within.layer_id], within.box),
1391 port_node_id_by_name[port_name])
1392 terminal_node = add_node(K.KIND_DEVICE_TERMINAL, f"T{index}.{layer_names[within.layer_id]}.excluded",
1393 within.layer_id, within.box)
1394 terminal_node.device_terminal.CopyFrom(within.terminal)
1395 terminal_node.device_terminal.ClearField('region_by_layer')
1396 cls.add_tie(network, port_node_id_by_name[port_name], terminal_node.node_id)
1398 taken_names = set(cut_port_names.values()) | {pin[0] for pins in vertex_port_pins.values() for pin in pins}
1399 for within in split.pins_within:
1400 if within.label not in taken_names:
1401 add_to_set(connected_shapes.set_at_position(layer_names[within.layer_id], within.position),
1402 add_node(K.KIND_EXCLUSION_PORT, within.label, within.layer_id,
1403 kdb.Box(within.position, within.position)).node_id)
1404 return nodes
1406 def find_net_pieces(self,
1407 network: r_network_pb2.RNetwork,
1408 pieces: List[List[klp.RNode]],
1409 nodes_within: List[r_network_pb2.RNode],
1410 split: NetSplit,
1411 regions: Dict[int, kdb.Region],
1412 layer_names: Dict[int, LayerName],
1413 wire_layer_ids: Set[int],
1414 well_layer_ids: Set[int]):
1415 """
1416 Finds the pieces of a net with ports at exclusion markers (see NetPiece), by the nodes of its network:
1417 what resistors join outside the markers, and what is within each marker, with the ports at its cuts
1418 """
1419 if not split.regions_within:
1420 return
1422 K = r_network_pb2.RNode.Kind
1423 well_layer_names = {layer_names[layer_id] for layer_id in well_layer_ids}
1425 def with_substrate(region_by_layer: Dict[LayerName, kdb.Region]) -> Dict[LayerName, kdb.Region]:
1426 # NOTE: the capacitances to a well are on the layer of the substrate, which the wells are split from
1427 wells = [region for layer_name, region in region_by_layer.items() if layer_name in well_layer_names]
1428 if not wells:
1429 return region_by_layer
1430 substrate = kdb.Region()
1431 for region in wells:
1432 substrate += region
1433 return {**region_by_layer, self.exclusions.substrate_layer_name: substrate}
1435 node_by_id = {node.node_id: node for node in network.nodes}
1436 names_by_node_id: Dict[int, List[str]] = defaultdict(list)
1437 net_pieces: List[NetPiece] = []
1439 def piece_name(name: str) -> str:
1440 # NOTE: not the name of another net, as a label can have any text
1441 while self.pex_context.top_circuit.net_by_name(name) is not None:
1442 name = f"{name}#"
1443 return name
1445 for index, piece in enumerate(pieces):
1446 piece_nodes = [node_by_id[rn.object_id()] for rn in piece if rn.object_id() in node_by_id]
1447 name = piece_name(f"{network.net_name}#piece{index}")
1448 region_by_layer: Dict[LayerName, kdb.Region] = {}
1449 for layer_id in sorted(wire_layer_ids & set(regions)):
1450 nodes_on_layer = [node for node in piece_nodes if node.layer_name == layer_names[layer_id]]
1451 if nodes_on_layer:
1452 # NOTE: with what is on the ideal copy of the layer, which has the same name
1453 region = regions[layer_id].interacting(self.node_boxes(nodes_on_layer))
1454 layer_name = layer_names[layer_id]
1455 region_by_layer[layer_name] = region_by_layer[layer_name] + region \
1456 if layer_name in region_by_layer else region
1457 if piece_nodes:
1458 net_pieces.append(NetPiece(name, with_substrate(region_by_layer)))
1459 for node in piece_nodes:
1460 names_by_node_id[node.node_id].append(name)
1462 def block_index_of(node: r_network_pb2.RNode) -> int:
1463 # NOTE: a port at a terminal of a removed device can be on a net taken out of the block
1464 block_index = split.block_of_node_id.get(node.node_id, None)
1465 if block_index is None:
1466 block_index = self.exclusions.block_index_with_ports_at(self.node_box(node), node.layer_name)
1467 return block_index
1469 nodes_of_blocks: Dict[int, List[r_network_pb2.RNode]] = defaultdict(list)
1470 for node in network.nodes:
1471 if node.node_kind == K.KIND_EXCLUSION_PORT:
1472 nodes_of_blocks[block_index_of(node)].append(node)
1473 for node in nodes_within:
1474 nodes_of_blocks[block_index_of(node)].append(node)
1475 for block_index, region_by_layer in split.regions_within.items():
1476 name = piece_name(f"{network.net_name}#marker{block_index}")
1477 nodes_of_block = nodes_of_blocks.get(block_index, [])
1478 if nodes_of_block:
1479 net_pieces.append(NetPiece(name, with_substrate(region_by_layer)))
1480 for node in nodes_of_block:
1481 names_by_node_id[node.node_id].append(name)
1483 if len(net_pieces) > 1:
1484 self.exclusions.pieces_by_net[network.net_name] = net_pieces
1485 for node in network.nodes:
1486 node.piece_names.extend(names_by_node_id.get(node.node_id, []))
1488 @staticmethod
1489 def network_pieces(resistor_network: klp.RNetwork,
1490 node_id_ties: Sequence[Tuple[int, int]] = ()) -> List[List[klp.RNode]]:
1491 """
1492 :param node_id_ties: the nodes joined besides by the resistors
1493 :return: the nodes of each piece of the network, which no resistor joins to the others
1494 """
1495 nodes = list(resistor_network.each_node())
1496 node_sets = DisjointSets(n.object_id() for n in nodes)
1497 for el in resistor_network.each_element():
1498 node_sets.union(el.a().object_id(), el.b().object_id())
1499 for node_id_a, node_id_b in node_id_ties:
1500 node_sets.union(node_id_a, node_id_b)
1502 pieces: Dict[int, List[klp.RNode]] = defaultdict(list)
1503 for n in nodes:
1504 pieces[node_sets.find(n.object_id())].append(n)
1505 return list(pieces.values())
1507 @staticmethod
1508 def isolated_well_node_ids(pieces: List[List[klp.RNode]],
1509 well_layer_ids: Set[int],
1510 cut_ports: Collection[PolygonPortKey] = ()) -> Set[int]:
1511 """
1512 :param cut_ports: the polygon ports at the cuts of exclusion blocks, where a model joins a piece to the rest
1513 :return: the nodes of the pieces of the network that are wells only, without a pin or a port at a cut,
1514 i.e. the wells no contact joins to the rest of their net
1515 (none if the network is one piece, e.g. a well with the bulks of transistors only)
1516 """
1517 if len(pieces) < 2:
1518 return set()
1520 def is_port(n: klp.RNode) -> bool:
1521 return n.type() == klp.RNodeType.VertexPort or \
1522 (n.type() == klp.RNodeType.PolygonPort and (n.layer(), n.port_index()) in cut_ports)
1524 return {n.object_id()
1525 for piece in pieces
1526 if all(n.layer() in well_layer_ids and not is_port(n) for n in piece)
1527 for n in piece}
1529 def substrate_joined_node_ids(self,
1530 network: r_network_pb2.RNetwork,
1531 regions: Dict[int, kdb.Region],
1532 layer_names: Dict[int, LayerName],
1533 well_layer_ids: Set[int]) -> Set[int]:
1534 """
1535 :param regions: the shapes of the net, by layer ID
1536 :param layer_names: the canonical names of the conductors and vias, by layer ID
1537 :return: the nodes that the substrate or a well joins to the net of the network, as LVS does:
1538 the nodes on a well, and on the conductors joined to a tap (see SubstrateInfo.tap_connections)
1539 """
1540 well_layer_names = {layer_names[layer_id] for layer_id in well_layer_ids}
1541 node_ids = {n.node_id for n in network.nodes if n.layer_name in well_layer_names}
1543 tech = self.pex_context.tech
1544 layout = self.pex_context.annotated_layout
1545 for connection in tech.tech.substrate.tap_connections:
1546 conductor_layer_id = layout.layer(*tech.gds_pair(connection.conductor_layer_name))
1547 tap_layer_id = layout.layer(*tech.gds_pair(connection.tap_lvs_layer_name))
1548 if conductor_layer_id not in layer_names or conductor_layer_id not in regions or tap_layer_id not in regions:
1549 continue
1550 conductor = regions[conductor_layer_id]
1551 if ideal_layer_id(conductor_layer_id) in regions:
1552 conductor = conductor + regions[ideal_layer_id(conductor_layer_id)]
1553 node_ids |= self.node_ids_at(nodes=[n for n in network.nodes if n.layer_name == layer_names[conductor_layer_id]],
1554 polygons=conductor.interacting(regions[tap_layer_id]))
1555 return node_ids
1557 @staticmethod
1558 def node_box(node: r_network_pb2.RNode) -> kdb.Box:
1559 """
1560 :return: the location of the node, enlarged, as a region drops a box without area
1561 (the location of a pin, or a junction across a wire)
1562 """
1563 match node.location.kind:
1564 case location_pb2.Location.Kind.LOCATION_KIND_POINT:
1565 point = kdb.Point(node.location.point.x, node.location.point.y)
1566 box = kdb.Box(point, point)
1567 case _:
1568 box = kdb.Box(node.location.box.lower_left.x, node.location.box.lower_left.y,
1569 node.location.box.upper_right.x, node.location.box.upper_right.y)
1570 return box.enlarged(1)
1572 @classmethod
1573 def node_boxes(cls, nodes: List[r_network_pb2.RNode]) -> kdb.Region:
1574 boxes = kdb.Region()
1575 boxes.merged_semantics = False # NOTE: each box on its own, rather than the polygon of those overlapping
1576 for node in nodes:
1577 boxes.insert(cls.node_box(node))
1578 return boxes
1580 @classmethod
1581 def node_ids_at(cls, nodes: List[r_network_pb2.RNode], polygons: kdb.Region) -> Set[int]:
1582 """
1583 :return: the nodes at the polygons, by their location
1584 """
1585 node_ids_by_box: Dict[kdb.Box, List[int]] = defaultdict(list)
1586 for node in nodes:
1587 node_ids_by_box[cls.node_box(node)].append(node.node_id)
1589 boxes = kdb.Region()
1590 boxes.merged_semantics = False # NOTE: each box on its own, rather than the polygon of those overlapping
1591 for box in node_ids_by_box:
1592 boxes.insert(box)
1593 return {node_id for polygon in boxes.interacting(polygons).each() for node_id in node_ids_by_box[polygon.bbox()]}