Coverage for klayout_pex/rcx25/r/r_extractor.py: 9%
230 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-08 18:54 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-08 18:54 +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/martinjankoehler/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 computed_layer_info = tech.computed_layer_info_by_gds_pair.get(gds_pair, None)
96 if computed_layer_info is None:
97 warning(f"ignoring layer {gds_pair}, no computed layer info found in tech info")
98 continue
100 canonical_layer_name = tech.canonical_layer_name_by_gds_pair[gds_pair]
102 LP = tech_pb2.LayerInfo.Purpose
104 if computed_layer_info.kind != tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
105 match computed_layer_info.layer_info.purpose:
106 case LP.PURPOSE_NWELL:
107 pass # TODO!
109 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT:
110 # device terminals
111 # - source/drain (e.g. sky130A: nsdm, psdm)
112 # - bulk (e.g. nwell)
113 #
114 # we will consider this only as a pin end-point, there are no wires at all on this layer,
115 # so the resistance does not matter for PEX
116 for source_layer in li.source_layers:
117 cond = rex_tech.conductors.add()
119 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
120 cond.layer.canonical_layer_name = canonical_layer_name
121 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
123 cond.triangulation_min_b = self.delaunay_b
124 cond.triangulation_max_area = self.delaunay_amax
126 cond.algorithm = self.substrate_algorithm
127 cond.resistance = 0 # see comment above
129 case LP.PURPOSE_METAL:
130 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
131 continue
133 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None)
134 for source_layer in li.source_layers:
135 cond = rex_tech.conductors.add()
137 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
138 cond.layer.canonical_layer_name = canonical_layer_name
139 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
141 cond.triangulation_min_b = self.delaunay_b
142 cond.triangulation_max_area = self.delaunay_amax
144 if canonical_layer_name == tech.internal_substrate_layer_name:
145 cond.algorithm = self.substrate_algorithm
146 else:
147 cond.algorithm = self.wire_algorithm
148 cond.resistance = self.pex_context.tech.milliohm_to_ohm(layer_resistance.resistance)
150 case LP.PURPOSE_CONTACT:
151 for source_layer in li.source_layers:
152 contact = tech.contact_by_contact_lvs_layer_name.get(source_layer.lvs_layer_name, None)
153 if contact is None:
154 warning(
155 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), "
156 f"no contact found in tech info")
157 continue
159 contact_resistance = tech.contact_resistance_by_device_layer_name.get(contact.layer_below,
160 None)
161 if contact_resistance is None:
162 warning(
163 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), "
164 f"no contact resistance found in tech info")
165 continue
167 via = rex_tech.vias.add()
169 bot_gds_pair = tech.gds_pair(contact.layer_below)
170 top_gds_pair = tech.gds_pair(contact.metal_above)
172 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
173 via.layer.canonical_layer_name = canonical_layer_name
174 via.layer.lvs_layer_name = source_layer.lvs_layer_name
176 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
177 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
179 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_contact(
180 contact=contact,
181 contact_resistance=contact_resistance
182 )
183 via.merge_distance = self.via_merge_distance
185 case LP.PURPOSE_VIA:
186 via_resistance = tech.via_resistance_by_layer_name.get(canonical_layer_name, None)
187 if via_resistance is None:
188 warning(f"ignoring layer {canonical_layer_name}, no via resistance found in tech info")
189 continue
190 for source_layer in li.source_layers:
191 via = rex_tech.vias.add()
192 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get(
193 source_layer.lvs_layer_name, None)
194 if bot_top is None:
195 warning(f"ignoring layer {canonical_layer_name} (LVS {source_layer.lvs_layer_name}), no bottom/top layers found in tech info")
196 continue
198 (bot, top) = bot_top
199 bot_gds_pair = tech.gds_pair(bot)
200 top_gds_pair = tech.gds_pair(top)
202 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
203 via.layer.canonical_layer_name = canonical_layer_name
204 via.layer.lvs_layer_name = source_layer.lvs_layer_name
206 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
207 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
209 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[
210 source_layer.lvs_layer_name]
212 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_via(
213 contact=contact,
214 via_resistance=via_resistance
215 )
217 via.merge_distance = self.via_merge_distance
219 return rex_tech
221 def prepare_request(self) -> pex_request_pb2.RExtractionRequest:
222 rex_request = pex_request_pb2.RExtractionRequest()
224 # prepare tech info
225 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech)
227 # prepare devices
228 devices_by_name = self.pex_context.devices_by_name
229 rex_request.devices.MergeFrom(devices_by_name.values())
231 # prepare pins
232 for pin_list in self.pex_context.pins_pb2_by_layer.values():
233 rex_request.pins.MergeFrom(pin_list)
235 net_request_by_name: Dict[NetName, pex_request_pb2.RNetExtractionRequest] = {}
236 def get_or_create_net_request(net_name: str):
237 v = net_request_by_name.get(net_name, None)
238 if not v:
239 v = rex_request.net_extraction_requests.add()
240 v.net_name = net_name
241 net_request_by_name[net_name] = v
242 return v
244 for pin in rex_request.pins:
245 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin)
247 for device in rex_request.devices:
248 for terminal in device.terminals:
249 get_or_create_net_request(terminal.net_name).device_terminals.add().CopyFrom(terminal)
251 netlist = self.pex_context.lvsdb.netlist()
252 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name)
253 # https://www.klayout.de/doc-qt5/code/class_Circuit.html
254 if not circuit:
255 circuits = [c.name for c in netlist.each_circuit()]
256 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, "
257 f"only available circuits are: {circuits}")
258 LK = tech_pb2.ComputedLayerInfo.Kind
259 for net in circuit.each_net():
260 for lvs_gds_pair, lyr_info in self.pex_context.extracted_layers.items():
261 for lyr in lyr_info.source_layers:
262 li = self.pex_context.tech.computed_layer_info_by_gds_pair[lyr.gds_pair]
263 match li.kind:
264 case LK.KIND_PIN:
265 continue # skip
266 case LK.KIND_REGULAR | LK.KIND_DEVICE_CAPACITOR | LK.KIND_DEVICE_RESISTOR:
267 r = self.pex_context.shapes_of_net(lyr.gds_pair, net)
268 if not r:
269 continue
270 l2r = get_or_create_net_request(net.name).region_by_layer.add()
271 l2r.layer.id = self.pex_context.annotated_layout.layer(*lvs_gds_pair)
272 l2r.layer.canonical_layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair]
273 l2r.layer.lvs_layer_name = lyr.lvs_layer_name
274 self.shapes_converter.klayout_region_to_pb(r, l2r.region)
276 return rex_request
278 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult:
279 rex_result = pex_result_pb2.RExtractionResult()
281 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech)
283 Label = str
284 LayerName = str
285 NetName = str
286 DeviceID = int
287 TerminalID = int
289 # dicts keyed by id / klayout_index
290 layer_names: Dict[int, LayerName] = {}
292 for c in rex_request.tech.conductors:
293 layer_names[c.layer.id] = c.layer.canonical_layer_name
295 for v in rex_request.tech.vias:
296 layer_names[v.layer.id] = v.layer.canonical_layer_name
298 for net_extraction_request in rex_request.net_extraction_requests:
300 vertex_ports: Dict[int, List[kdb.Point]] = defaultdict(list)
301 polygon_ports: Dict[int, List[kdb.Polygon]] = defaultdict(list)
302 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]] = defaultdict(list)
303 polygon_port_device_terminals: Dict[int, List[device_pb2.Device.Terminal]] = defaultdict(list)
304 regions: Dict[int, kdb.Region] = defaultdict(kdb.Region)
306 for t in net_extraction_request.device_terminals:
307 for l2r in t.region_by_layer:
308 for p in l2r.region.polygons:
309 p_kly = self.shapes_converter.klayout_polygon(p)
310 polygon_ports[l2r.layer.id].append(p_kly)
311 polygon_port_device_terminals[l2r.layer.id].append(t)
313 for pin in net_extraction_request.pins:
314 p = self.shapes_converter.klayout_point(pin.label_point)
315 vertex_ports[pin.layer.id].append(p)
316 vertex_port_pins[pin.layer.id].append((pin.label, pin.net_name))
318 for l2r in net_extraction_request.region_by_layer:
319 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region)
321 rex = klp.RNetExtractor(self.pex_context.dbu)
322 resistor_network = rex.extract(rex_tech_kly,
323 regions,
324 vertex_ports,
325 polygon_ports)
327 node_by_node_id: Dict[int, r_network_pb2.RNode] = {}
329 result_network = rex_result.networks.add()
330 result_network.net_name = net_extraction_request.net_name
332 for rn in resistor_network.each_node():
333 loc = rn.location()
334 layer_id = rn.layer()
335 canonical_layer_name = layer_names[layer_id]
337 r_node = result_network.nodes.add()
338 r_node.node_id = rn.object_id()
339 r_node.node_name = rn.to_s()
340 r_node.node_type = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO!
341 r_node.layer_name = canonical_layer_name
343 match rn.type():
344 case klp.RNodeType.VertexPort:
345 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_POINT
346 p = loc.center().to_itype(self.pex_context.dbu)
347 r_node.location.point.x = p.x
348 r_node.location.point.y = p.y
349 case klp.RNodeType.PolygonPort | _:
350 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX
351 p1 = loc.p1.to_itype(self.pex_context.dbu)
352 p2 = loc.p2.to_itype(self.pex_context.dbu)
353 r_node.location.box.lower_left.x = p1.x
354 r_node.location.box.lower_left.y = p1.y
355 r_node.location.box.upper_right.x = p2.x
356 r_node.location.box.upper_right.y = p2.y
358 match rn.type():
359 case klp.RNodeType.VertexPort:
360 port_idx = rn.port_index()
361 r_node.net_name = vertex_port_pins[rn.layer()][port_idx][1]
362 r_node.location.point.net = r_node.net_name
363 case klp.RNodeType.PolygonPort:
364 port_idx = rn.port_index()
365 r_node.net_name = polygon_port_device_terminals[rn.layer()][port_idx].net_name
366 r_node.location.box.net = r_node.net_name
367 case _:
368 r_node.net_name = r_node.node_name
369 r_node.location.box.net = r_node.net_name
371 node_by_node_id[r_node.node_id] = r_node
373 for el in resistor_network.each_element():
374 r_element = result_network.elements.add()
375 r_element.element_id = el.object_id()
376 r_element.node_a.node_id = el.a().object_id()
377 r_element.node_b.node_id = el.b().object_id()
378 r_element.resistance = el.resistance()
380 return rex_result