Coverage for klayout_pex/rcx25/r/r_extractor.py: 91%
255 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 20:14 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 20: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 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 match computed_layer_info.kind:
105 case tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
106 continue
108 case tech_pb2.ComputedLayerInfo.Kind.KIND_LABEL:
109 continue
111 case _:
112 pass
114 match computed_layer_info.layer_info.purpose:
115 case LP.PURPOSE_NWELL:
116 pass # TODO!
118 case LP.PURPOSE_N_IMPLANT | LP.PURPOSE_P_IMPLANT:
119 # device terminals
120 # - source/drain (e.g. sky130A: nsdm, psdm)
121 # - bulk (e.g. nwell)
122 #
123 # we will consider this only as a pin end-point, there are no wires at all on this layer,
124 # so the resistance does not matter for PEX
125 for source_layer in li.source_layers:
126 cond = rex_tech.conductors.add()
128 cond.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
129 cond.layer.canonical_layer_name = canonical_layer_name
130 cond.layer.lvs_layer_name = source_layer.lvs_layer_name
132 cond.triangulation_min_b = self.delaunay_b
133 cond.triangulation_max_area = self.delaunay_amax
135 cond.algorithm = self.substrate_algorithm
136 cond.resistance = 0 # see comment above
138 case LP.PURPOSE_METAL:
139 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
140 continue
142 layer_resistance = tech.layer_resistance_by_layer_name.get(canonical_layer_name, None)
143 for source_layer in li.source_layers:
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 for source_layer in li.source_layers:
161 contact = tech.contact_by_contact_lvs_layer_name.get(source_layer.lvs_layer_name, None)
162 if contact is None:
163 warning(
164 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), "
165 f"no contact found in tech info")
166 continue
168 contact_resistance = tech.contact_resistance_by_device_layer_name.get(contact.layer_below,
169 None)
170 if contact_resistance is None:
171 warning(
172 f"ignoring LVS layer {source_layer.lvs_layer_name} (layer {canonical_layer_name}), "
173 f"no contact resistance found in tech info")
174 continue
176 via = rex_tech.vias.add()
178 bot_gds_pair = tech.gds_pair(contact.layer_below)
179 top_gds_pair = tech.gds_pair(contact.metal_above)
181 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
182 via.layer.canonical_layer_name = canonical_layer_name
183 via.layer.lvs_layer_name = source_layer.lvs_layer_name
185 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
186 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
188 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_contact(
189 contact=contact,
190 contact_resistance=contact_resistance
191 )
192 via.merge_distance = self.via_merge_distance
194 case LP.PURPOSE_VIA:
195 via_resistance = tech.via_resistance_by_layer_name.get(canonical_layer_name, None)
196 if via_resistance is None:
197 warning(f"ignoring layer {canonical_layer_name}, no via resistance found in tech info")
198 continue
199 for source_layer in li.source_layers:
200 bot_top = tech.bottom_and_top_layer_name_by_via_computed_layer_name.get(
201 source_layer.lvs_layer_name, None)
202 if bot_top is None:
203 warning(f"ignoring layer {canonical_layer_name} (LVS {source_layer.lvs_layer_name}), no bottom/top layers found in tech info")
204 continue
205 via = rex_tech.vias.add()
207 (bot, top) = bot_top
208 bot_gds_pair = tech.gds_pair(bot)
209 top_gds_pair = tech.gds_pair(top)
211 via.layer.id = self.pex_context.annotated_layout.layer(*source_layer.gds_pair)
212 via.layer.canonical_layer_name = canonical_layer_name
213 via.layer.lvs_layer_name = source_layer.lvs_layer_name
215 via.bottom_conductor.id = self.pex_context.annotated_layout.layer(*bot_gds_pair)
216 via.top_conductor.id = self.pex_context.annotated_layout.layer(*top_gds_pair)
218 contact = self.pex_context.tech.contact_by_contact_lvs_layer_name[
219 source_layer.lvs_layer_name]
221 via.resistance = self.pex_context.tech.milliohm_by_cnt_to_ohm_by_square_for_via(
222 contact=contact,
223 via_resistance=via_resistance
224 )
226 via.merge_distance = self.via_merge_distance
228 return rex_tech
230 def prepare_request(self) -> pex_request_pb2.RExtractionRequest:
231 rex_request = pex_request_pb2.RExtractionRequest()
233 # prepare tech info
234 self.prepare_r_extractor_tech_pb(rex_tech=rex_request.tech)
236 # prepare devices
237 devices_by_name = self.pex_context.devices_by_name
238 rex_request.devices.MergeFrom(devices_by_name.values())
240 # prepare pins
241 for pin_list in self.pex_context.pins_pb2_by_layer.values():
242 rex_request.pins.MergeFrom(pin_list)
244 net_request_by_name: Dict[NetName, pex_request_pb2.RNetExtractionRequest] = {}
245 def get_or_create_net_request(net_name: str):
246 v = net_request_by_name.get(net_name, None)
247 if not v:
248 v = rex_request.net_extraction_requests.add()
249 v.net_name = net_name
250 net_request_by_name[net_name] = v
251 return v
253 for pin in rex_request.pins:
254 get_or_create_net_request(pin.net_name).pins.add().CopyFrom(pin)
256 for device in rex_request.devices:
257 for terminal in device.terminals:
258 get_or_create_net_request(terminal.net_name).device_terminals.add().CopyFrom(terminal)
260 netlist = self.pex_context.lvsdb.netlist()
261 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name)
262 # https://www.klayout.de/doc-qt5/code/class_Circuit.html
263 if not circuit:
264 circuits = [c.name for c in netlist.each_circuit()]
265 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, "
266 f"only available circuits are: {circuits}")
267 LK = tech_pb2.ComputedLayerInfo.Kind
268 for net in circuit.each_net():
269 net_name = net.name or f"${net.cluster_id}"
270 for lvs_gds_pair, lyr_info in self.pex_context.extracted_layers.items():
271 for lyr in lyr_info.source_layers:
272 li = self.pex_context.tech.computed_layer_info_by_gds_pair[lyr.gds_pair]
273 match li.kind:
274 case LK.KIND_PIN:
275 continue # skip
276 case LK.KIND_REGULAR | LK.KIND_DEVICE_CAPACITOR | LK.KIND_DEVICE_RESISTOR:
277 r = self.pex_context.shapes_of_net(lyr.gds_pair, net)
278 if not r:
279 continue
280 l2r = get_or_create_net_request(net_name).region_by_layer.add()
281 l2r.layer.id = self.pex_context.annotated_layout.layer(*lvs_gds_pair)
282 l2r.layer.canonical_layer_name = self.pex_context.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair]
283 l2r.layer.lvs_layer_name = lyr.lvs_layer_name
284 self.shapes_converter.klayout_region_to_pb(r, l2r.region)
285 case _:
286 raise NotImplementedError()
288 return rex_request
290 def extract(self, rex_request: pex_request_pb2.RExtractionRequest) -> pex_result_pb2.RExtractionResult:
291 rex_result = pex_result_pb2.RExtractionResult()
293 rex_tech_kly = klayout_r_extractor_tech(rex_request.tech)
295 Label = str
296 LayerName = str
297 NetName = str
298 DeviceID = int
299 TerminalID = int
301 # dicts keyed by id / klayout_index
302 layer_names: Dict[int, LayerName] = {}
304 wire_layer_ids: Set[int] = set()
305 via_layer_ids: Set[int] = set()
307 for c in rex_request.tech.conductors:
308 layer_names[c.layer.id] = c.layer.canonical_layer_name
309 wire_layer_ids.add(c.layer.id)
311 for v in rex_request.tech.vias:
312 layer_names[v.layer.id] = v.layer.canonical_layer_name
313 via_layer_ids.add(c.layer.id)
315 for net_extraction_request in rex_request.net_extraction_requests:
316 vertex_ports: Dict[int, List[kdb.Point]] = defaultdict(list)
317 polygon_ports: Dict[int, List[kdb.Polygon]] = defaultdict(list)
318 vertex_port_pins: Dict[int, List[Tuple[Label, NetName]]] = defaultdict(list)
319 polygon_port_device_terminals: Dict[int, List[device_pb2.Device.Terminal]] = defaultdict(list)
320 regions: Dict[int, kdb.Region] = defaultdict(kdb.Region)
322 for t in net_extraction_request.device_terminals:
323 for l2r in t.region_by_layer:
324 for sh in l2r.region.shapes:
325 sh_kly = self.shapes_converter.klayout_shape(sh)
326 polygon_ports[l2r.layer.id].append(sh_kly)
327 polygon_port_device_terminals[l2r.layer.id].append(t)
329 for pin in net_extraction_request.pins:
330 p = self.shapes_converter.klayout_point(pin.label_point)
331 vertex_ports[pin.layer.id].append(p)
332 vertex_port_pins[pin.layer.id].append((pin.label, pin.net_name))
334 for l2r in net_extraction_request.region_by_layer:
335 regions[l2r.layer.id] = self.shapes_converter.klayout_region(l2r.region)
337 rex = klp.RNetExtractor(self.pex_context.dbu)
338 resistor_network = rex.extract(rex_tech_kly,
339 regions,
340 vertex_ports,
341 polygon_ports)
343 result_network = rex_result.networks.add()
344 result_network.net_name = net_extraction_request.net_name
346 for rn in resistor_network.each_node():
347 node_by_node_id: Dict[int, r_network_pb2.RNode] = {}
349 loc = rn.location()
350 layer_id = rn.layer()
351 canonical_layer_name = layer_names[layer_id]
353 r_node = result_network.nodes.add()
354 r_node.node_id = rn.object_id()
355 r_node.node_name = rn.to_s()
356 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_UNSPECIFIED # TODO!
357 r_node.layer_name = canonical_layer_name
359 match rn.type():
360 case klp.RNodeType.VertexPort: # pins!
361 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_POINT
362 p = loc.center().to_itype(self.pex_context.dbu)
363 r_node.location.point.x = p.x
364 r_node.location.point.y = p.y
365 case klp.RNodeType.PolygonPort | klp.RNodeType.Internal:
366 r_node.location.kind = location_pb2.Location.Kind.LOCATION_KIND_BOX
367 p1 = loc.p1.to_itype(self.pex_context.dbu)
368 p2 = loc.p2.to_itype(self.pex_context.dbu)
369 r_node.location.box.lower_left.x = p1.x
370 r_node.location.box.lower_left.y = p1.y
371 r_node.location.box.upper_right.x = p2.x
372 r_node.location.box.upper_right.y = p2.y
373 case _:
374 raise NotImplementedError()
376 match rn.type():
377 case klp.RNodeType.VertexPort:
378 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_PIN
379 port_idx = rn.port_index()
380 r_node.node_name, r_node.net_name = vertex_port_pins[rn.layer()][port_idx][0:2]
381 r_node.location.point.net = r_node.net_name
383 case klp.RNodeType.PolygonPort:
384 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL
385 port_idx = rn.port_index()
386 nn = polygon_port_device_terminals[rn.layer()][port_idx].net_name
387 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}"
388 r_node.location.box.net = r_node.net_name
389 case klp.RNodeType.Internal:
390 if rn.layer() in via_layer_ids:
391 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_VIA_JUNCTION
392 elif rn.layer() in wire_layer_ids:
393 r_node.node_kind = r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION
394 else:
395 raise NotImplementedError()
397 # NOTE: network prefix, as node name is only unique per network
398 r_node.net_name = f"{result_network.net_name}.{r_node.node_name}"
399 r_node.location.box.net = r_node.net_name
400 case _:
401 raise NotImplementedError()
403 node_by_node_id[r_node.node_id] = r_node
405 for el in resistor_network.each_element():
406 r_element = result_network.elements.add()
407 r_element.element_id = el.object_id()
408 r_element.node_a.node_id = el.a().object_id()
409 r_element.node_b.node_id = el.b().object_id()
410 r_element.resistance = el.resistance()
412 return rex_result