Coverage for klayout_pex / rcx25 / r / r_extractor.py: 90%
254 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:21 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:21 +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 defaultdict
26from typing import *
28from klayout_pex.log import (
29 warning,
30 subproc,
31)
33from ..types import NetName
35from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter
36from klayout_pex.klayout.lvsdb_extractor import 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 RExtractor:
52 def __init__(self,
53 pex_context: KLayoutExtractionContext,
54 substrate_algorithm: pb_RExtractorTech.Algorithm,
55 wire_algorithm: pb_RExtractorTech.Algorithm,
56 delaunay_b: float,
57 delaunay_amax: float,
58 via_merge_distance: float,
59 skip_simplify: bool):
60 """
61 :param pex_context: KLayout PEX extraction context
62 :param substrate_algorithm: The KLayout PEXCore Algorithm for decomposing polygons.
63 Either SquareCounting or Tesselation (recommended)
64 :param wire_algorithm: The KLayout PEXCore Algorithm for decomposing polygons.
65 Either SquareCounting (recommended) or Tesselation
66 :param delaunay_b: The "b" parameter for the Delaunay triangulation,
67 a ratio of shortest triangle edge to circle radius
68 :param delaunay_amax: The "max_area" specifies the maximum area of the triangles
69 produced in square micrometers.
70 :param via_merge_distance: Maximum distance where close vias are merged together
71 :param skip_simplify: skip simplification of resistor network
72 """
73 self.pex_context = pex_context
74 self.substrate_algorithm = substrate_algorithm
75 self.wire_algorithm = wire_algorithm
76 self.delaunay_b = delaunay_b
77 self.delaunay_amax = delaunay_amax
78 self.via_merge_distance = via_merge_distance
79 self.skip_simplify = skip_simplify
81 self.shapes_converter = ShapesConverter(dbu=self.pex_context.dbu)
83 def prepare_r_extractor_tech_pb(self,
84 rex_tech: pb_RExtractorTech):
85 """
86 Prepare KLayout PEXCore Technology Description based on the KPEX Tech Info data
87 :param rex_tech: RExtractorTech protobuffer message
88 """
90 rex_tech.skip_simplify = self.skip_simplify
92 tech = self.pex_context.tech
94 for gds_pair, li in self.pex_context.extracted_layers.items():
95 for source_layer in li.source_layers:
96 computed_layer_info = tech.computed_layer_info_by_name.get(source_layer.lvs_layer_name, None)
97 if computed_layer_info is None:
98 warning(f"ignoring layer {gds_pair}, no computed layer info found in tech info")
99 continue
101 canonical_layer_name = tech.canonical_layer_name_by_gds_pair[gds_pair]
103 LP = tech_pb2.LayerInfo.Purpose
105 match computed_layer_info.kind:
106 case tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
107 continue
109 case tech_pb2.ComputedLayerInfo.Kind.KIND_LABEL:
110 continue
112 case _:
113 pass
115 match computed_layer_info.layer_info.purpose:
116 case LP.PURPOSE_NWELL | LP.PURPOSE_PWELL:
117 pass # TODO!?
119 case LP.PURPOSE_NTAP | LP.PURPOSE_PTAP:
120 pass # TODO!?
122 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT:
123 # device terminals
124 # - source/drain (e.g. sky130A: nsdm, psdm)
125 # - bulk (e.g. nwell)
126 #
127 # we will consider this only as a pin end-point, there are no wires at all on this layer,
128 # so the resistance does not matter for PEX
129 cond = rex_tech.conductors.add()
131 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
132 cond.layer.canonical_layer_name = canonical_layer_name
133 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
135 cond.triangulation_min_b = self.delaunay_b
136 cond.triangulation_max_area = self.delaunay_amax
138 cond.algorithm = self.substrate_algorithm
139 cond.resistance = 0 # see comment above
141 case LP.PURPOSE_METAL:
142 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None)
144 cond = rex_tech.conductors.add()
146 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
147 cond.layer.canonical_layer_name = canonical_layer_name
148 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
150 cond.triangulation_min_b = self.delaunay_b
151 cond.triangulation_max_area = self.delaunay_amax
153 if canonical_layer_name == tech.internal_substrate_layer_name:
154 cond.algorithm = self.substrate_algorithm
155 else:
156 cond.algorithm = self.wire_algorithm
157 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance)
159 case LP.PURPOSE_CONTACT:
160 contact = tech.contact_by_contact_lvs_layer_name.get(source_layer.lvs_layer_name, None)
161 if contact is None:
162 warning(
163 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), "
164 f"no contact found in tech info")
165 continue
167 contact_resistance = tech.contact_resistance_by_device_layer_name.get(contact.layer_below,
168 None)
169 if contact_resistance is None:
170 warning(
171 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), "
172 f"no contact resistance found in tech info")
173 continue
175 via = rex_tech.vias.add()
177 bot_gds_pair = tech.gds_pair(contact.layer_below)
178 top_gds_pair = tech.gds_pair(contact.metal_above)
180 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
181 via.layer.canonical_layer_name = canonical_layer_name
182 via.layer.lvs_layer_name = source_layer.lvs_layer_name
184 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
185 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
187 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_contact(
188 contact=contact,
189 contact_resistance=contact_resistance
190 )
191 via.merge_distance = self.via_merge_distance
193 case LP.PURPOSE_VIA:
194 via_resistance = tech.via_resistance_by_layer_name.get(canonical_layer_name, None)
195 if via_resistance is None:
196 warning(f"ignoring layer {canonical_layer_name}, no via resistance found in tech info")
197 continue
198 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get(
199 source_layer.lvs_layer_name, None)
200 if bot_top is None:
201 warning(f"ignoring layer {canonical_layer_name} (LVS {source_layer.lvs_layer_name}), no bottom/top layers found in tech info")
202 continue
203 via = rex_tech.vias.add()
205 (bot, top) = bot_top
206 bot_gds_pair = tech.gds_pair(bot)
207 top_gds_pair = tech.gds_pair(top)
209 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
210 via.layer.canonical_layer_name = canonical_layer_name
211 via.layer.lvs_layer_name = source_layer.lvs_layer_name
213 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
214 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
216 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[
217 source_layer.lvs_layer_name]
219 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_via(
220 contact=contact,
221 via_resistance=via_resistance
222 )
224 via.merge_distance = self.via_merge_distance
226 case _:
227 warning(f"prepare_r_extractor_tech_pb: Unhandled layer purpose "
228 f"{LP.Name(computed_layer_info.layer_info.purpose)}"
229 f"({computed_layer_info.layer_info.purpose}), "
230 f"LVS computed layer is {source_layer.lvs_layer_name} ({source_layer.gds_pair}), "
231 f"original layer is {canonical_layer_name}")
233 return rex_tech
235 def prepare_request(self) -> pex_request_pb2.RExtractionRequest:
236 rex_request = pex_request_pb2.RExtractionRequest()
238 # prepare tech info
239 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech)
241 # prepare devices
242 devices_by_name = self.pex_context.devices_by_name
243 rex_request.devices.MergeFrom(devices_by_name.values())
245 # prepare pins
246 for pin_list in self.pex_context.pins_pb2_by_layer.values():
247 rex_request.pins.MergeFrom(pin_list)
249 net_request_by_name: Dict[NetName, pex_request_pb2.RNetExtractionRequest] = {}
250 def get_or_create_net_request(net_name: str):
251 v = net_request_by_name.get(net_name, None)
252 if not v:
253 v = rex_request.net_extraction_requests.add()
254 v.net_name = net_name
255 net_request_by_name[net_name] = v
256 return v
258 for pin in rex_request.pins:
259 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin)
261 for device in rex_request.devices:
262 for terminal in device.terminals:
263 get_or_create_net_request(terminal.net_name).device_terminals.add().CopyFrom(terminal)
265 netlist = self.pex_context.lvsdb.netlist()
266 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name)
267 # https://www.klayout.de/doc-qt5/code/class_Circuit.html
268 if not circuit:
269 circuits = [c.name for c in netlist.each_circuit()]
270 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, "
271 f"only available circuits are: {circuits}")
272 LK = tech_pb2.ComputedLayerInfo.Kind
273 for net in circuit.each_net():
274 net_name = net.name or f"${net.cluster_id}"
275 for lvs_gds_pair, lyr_info in self.pex_context.extracted_layers.items():
276 for lyr in lyr_info.source_layers:
277 li = self.pex_context.tech.computed_layer_info_by_gds_pair[lyr.gds_pair]
278 match li.kind:
279 case LK.KIND_PIN:
280 continue # skip
281 case LK.KIND_REGULAR | LK.KIND_DEVICE_CAPACITOR | LK.KIND_DEVICE_RESISTOR:
282 r = self.pex_context.shapes_of_net(lyr.gds_pair, net)
283 if not r:
284 continue
285 l2r = get_or_create_net_request(net_name).region_by_layer.add()
286 l2r.layer.id = self.pex_context.annotated_layout.layer(*lvs_gds_pair)
287 l2r.layer.canonical_layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair]
288 l2r.layer.lvs_layer_name = lyr.lvs_layer_name
289 self.shapes_converter.klayout_region_to_pb(r, l2r.region)
290 case _:
291 raise NotImplementedError()
293 return rex_request
295 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult:
296 rex_result = pex_result_pb2.RExtractionResult()
298 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech)
300 Label = str
301 LayerName = str
302 NetName = str
303 DeviceID = int
304 TerminalID = int
306 # dicts keyed by id / klayout_index
307 layer_names: Dict[int, LayerName] = {}
309 wire_layer_ids: Set[int] = set()
310 via_layer_ids: Set[int] = set()
312 for c in rex_request.tech.conductors:
313 layer_names[c.layer.id] = c.layer.canonical_layer_name
314 wire_layer_ids.add(c.layer.id)
316 for v in rex_request.tech.vias:
317 layer_names[v.layer.id] = v.layer.canonical_layer_name
318 via_layer_ids.add(c.layer.id)
320 for net_extraction_request in rex_request.net_extraction_requests:
321 vertex_ports: Dict[int, List[kdb.Point]] = defaultdict(list)
322 polygon_ports: Dict[int, List[kdb.Polygon]] = defaultdict(list)
323 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]] = defaultdict(list)
324 polygon_port_device_terminals: Dict[int, List[device_pb2.Device.Terminal]] = defaultdict(list)
325 regions: Dict[int, kdb.Region] = defaultdict(kdb.Region)
327 for t in net_extraction_request.device_terminals:
328 for l2r in t.region_by_layer:
329 for sh in l2r.region.shapes:
330 sh_kly = self.shapes_converter.klayout_shape(sh)
331 polygon_ports[l2r.layer.id].append(sh_kly)
332 polygon_port_device_terminals[l2r.layer.id].append(t)
334 for pin in net_extraction_request.pins:
335 p = self.shapes_converter.klayout_point(pin.label_point)
336 vertex_ports[pin.layer.id].append(p)
337 vertex_port_pins[pin.layer.id].append((pin.label, pin.net_name))
339 for l2r in net_extraction_request.region_by_layer:
340 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region)
342 rex = klp.RNetExtractor(self.pex_context.dbu)
343 resistor_network = rex.extract(rex_tech_kly,
344 regions,
345 vertex_ports,
346 polygon_ports)
348 result_network = rex_result.networks.add()
349 result_network.net_name = net_extraction_request.net_name
351 for rn in resistor_network.each_node():
352 node_by_node_id: Dict[int, r_network_pb2.RNode] = {}
354 loc = rn.location()
355 layer_id = rn.layer()
356 canonical_layer_name = layer_names[layer_id]
358 r_node = result_network.nodes.add()
359 r_node.node_id = rn.object_id()
360 r_node.node_name = rn.to_s()
361 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO!
362 r_node.layer_name = canonical_layer_name
364 match rn.type():
365 case klp.RNodeType.VertexPort: # pins!
366 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_POINT
367 p = loc.center().to_itype(self.pex_context.dbu)
368 r_node.location.point.x = p.x
369 r_node.location.point.y = p.y
370 case klp.RNodeType.PolygonPort | klp.RNodeType.Internal:
371 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX
372 p1 = loc.p1.to_itype(self.pex_context.dbu)
373 p2 = loc.p2.to_itype(self.pex_context.dbu)
374 r_node.location.box.lower_left.x = p1.x
375 r_node.location.box.lower_left.y = p1.y
376 r_node.location.box.upper_right.x = p2.x
377 r_node.location.box.upper_right.y = p2.y
378 case _:
379 raise NotImplementedError()
381 match rn.type():
382 case klp.RNodeType.VertexPort:
383 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_PIN
384 port_idx = rn.port_index()
385 r_node.node_name, r_node.net_name = vertex_port_pins[rn.layer()][port_idx][0:2]
386 r_node.location.point.net = r_node.net_name
388 case klp.RNodeType.PolygonPort:
389 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL
390 port_idx = rn.port_index()
391 nn = polygon_port_device_terminals[rn.layer()][port_idx].net_name
392 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}"
393 r_node.location.box.net = r_node.net_name
394 case klp.RNodeType.Internal:
395 if rn.layer() in via_layer_ids:
396 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_VIA_JUNCTION
397 elif rn.layer() in wire_layer_ids:
398 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION
399 else:
400 raise NotImplementedError()
402 # NOTE: network prefix, as node name is only unique per network
403 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}"
404 r_node.location.box.net = r_node.net_name
405 case _:
406 raise NotImplementedError()
408 node_by_node_id[r_node.node_id] = r_node
410 for el in resistor_network.each_element():
411 r_element = result_network.elements.add()
412 r_element.element_id = el.object_id()
413 r_element.node_a.node_id = el.a().object_id()
414 r_element.node_b.node_id = el.b().object_id()
415 r_element.resistance = el.resistance()
417 return rex_result