Coverage for klayout_pex/rcx25/extraction_reporter.py: 87%
248 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 functools import cached_property
27import klayout.rdb as rdb
28import klayout.db as kdb
30from .extraction_results import *
31from .types import EdgeNeighborhood
32from ..types import LayerName
33from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer
34from klayout_pex.klayout.output_files import write_output_file
35from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter
37import klayout_pex_protobuf.kpex.geometry.shapes_pb2 as shapes_pb2
38import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2
39import klayout_pex_protobuf.kpex.layout.pin_pb2 as pin_pb2
40import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2
41import klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 as r_extractor_tech_pb2
42import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2
43import klayout_pex_protobuf.kpex.request.pex_request_pb2 as pex_request_pb2
44import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2
46VarShapes = kdb.Shapes | kdb.Region | List[kdb.Edge] | List[kdb.Polygon | kdb.Box]
49class ExtractionReporter:
50 def __init__(self,
51 cell_name: str,
52 dbu: float,
53 report_caps: bool):
54 """
55 :param report_caps: a category for each capacitance contribution (overlap, sidewall, fringe),
56 with its shapes, which takes much memory and time on large layouts
57 (e.g. 0.8 GB of 1.8 GB and 12 of 51 s for the RC extraction of adc_comp_latch)
58 """
59 self.report = rdb.ReportDatabase(f"PEX {cell_name}")
60 self.cell = self.report.create_cell(cell_name)
61 self.dbu = dbu
62 self.dbu_trans = kdb.CplxTrans(mag=dbu)
63 self.category_name_counter: Dict[str, int] = defaultdict(int)
64 self.shapes_converter = ShapesConverter(dbu=dbu)
65 self.report_caps = report_caps
67 @cached_property
68 def cat_common(self) -> rdb.RdbCategory:
69 return self.report.create_category('Common')
71 @cached_property
72 def cat_pins(self) -> rdb.RdbCategory:
73 return self.report.create_category("Pins")
75 @cached_property
76 def cat_rex_request(self) -> rdb.RdbCategory:
77 return self.report.create_category("[R] Extraction Request")
79 @cached_property
80 def cat_rex_tech(self) -> rdb.RdbCategory:
81 return self.report.create_category(self.cat_rex_request, "[R] Extraction Tech")
83 @cached_property
84 def cat_rex_request_devices(self) -> rdb.RdbCategory:
85 return self.report.create_category(self.cat_rex_request, "Devices")
87 @cached_property
88 def cat_rex_request_pins(self) -> rdb.RdbCategory:
89 return self.report.create_category(self.cat_rex_request, "Pins")
91 @cached_property
92 def cat_rex_request_network_extraction(self) -> rdb.RdbCategory:
93 return self.report.create_category(self.cat_rex_request, "Network Extraction Request")
95 @cached_property
96 def cat_rex_result(self) -> rdb.RdbCategory:
97 return self.report.create_category("[R] Extraction Result")
99 @cached_property
100 def cat_rex_result_networks(self) -> rdb.RdbCategory:
101 return self.report.create_category(self.cat_rex_result, "Networks")
103 @cached_property
104 def cat_rex_nodes(self) -> rdb.RdbCategory:
105 return self.report.create_category(self.cat_rex_result, "Nodes")
107 @cached_property
108 def cat_rex_elements(self) -> rdb.RdbCategory:
109 return self.report.create_category(self.cat_rex_result, "Net Elements (Edges)")
111 @cached_property
112 def cat_devices(self) -> rdb.RdbCategory:
113 return self.report.create_category("Devices")
115 @cached_property
116 def cat_vias(self) -> rdb.RdbCategory:
117 return self.report.create_category("Vias")
119 @cached_property
120 def cat_overlap(self) -> rdb.RdbCategory:
121 return self.report.create_category("[C] Overlap")
123 @cached_property
124 def cat_sidewall(self) -> rdb.RdbCategory:
125 return self.report.create_category("[C] Sidewall")
127 @cached_property
128 def cat_fringe(self) -> rdb.RdbCategory:
129 return self.report.create_category("[C] Fringe / Side Overlap")
131 @cached_property
132 def cat_edge_neighborhood(self) -> rdb.RdbCategory:
133 return self.report.create_category("[C] Edge Neighborhood Visitor")
135 def save(self, path: str):
136 write_output_file(self.report.save, path)
138 def output_shapes(self,
139 parent_category: rdb.RdbCategory,
140 category_name: str,
141 shapes: VarShapes) -> rdb.RdbCategory:
142 rdb_cat = self.report.create_category(parent_category, category_name)
143 self.report.create_items(self.cell.rdb_id(), ## TODO: if later hierarchical mode is introduced
144 rdb_cat.rdb_id(),
145 self.dbu_trans,
146 shapes)
147 return rdb_cat
149 def output_overlap(self,
150 overlap_cap: OverlapCap,
151 bottom_polygon: kdb.PolygonWithProperties,
152 top_polygon: kdb.PolygonWithProperties,
153 overlap_area: kdb.Region):
154 if not self.report_caps:
155 return
156 cat_overlap_top_layer = self.report.create_category(self.cat_overlap,
157 f"top_layer={overlap_cap.key.layer_top}")
158 cat_overlap_bot_layer = self.report.create_category(cat_overlap_top_layer,
159 f'bot_layer={overlap_cap.key.layer_bot}')
160 cat_overlap_nets = self.report.create_category(cat_overlap_bot_layer,
161 f'{overlap_cap.key.net_top} – {overlap_cap.key.net_bot}')
162 self.category_name_counter[cat_overlap_nets.path()] += 1
163 cat_overlap_cap = self.report.create_category(
164 cat_overlap_nets,
165 f"#{self.category_name_counter[cat_overlap_nets.path()]} "
166 f"{round(overlap_cap.cap_value, 3)} fF",
167 )
169 self.output_shapes(cat_overlap_cap, "Top Polygon", [top_polygon])
170 self.output_shapes(cat_overlap_cap, "Bottom Polygon", [bottom_polygon])
171 self.output_shapes(cat_overlap_cap, "Overlap Area", overlap_area)
173 def output_sidewall(self,
174 sidewall_cap: SidewallCap,
175 inside_edge: kdb.Edge,
176 outside_edge: kdb.Edge):
177 if not self.report_caps:
178 return
179 cat_sidewall_layer = self.report.create_category(self.cat_sidewall,
180 f"layer={sidewall_cap.key.layer}")
181 cat_sidewall_net_inside = self.report.create_category(cat_sidewall_layer,
182 f'inside={sidewall_cap.key.net1}')
183 cat_sidewall_net_outside = self.report.create_category(cat_sidewall_net_inside,
184 f'outside={sidewall_cap.key.net2}')
185 self.category_name_counter[cat_sidewall_net_outside.path()] += 1
187 self.output_shapes(
188 cat_sidewall_net_outside,
189 f"#{self.category_name_counter[cat_sidewall_net_outside.path()]}: "
190 f"len {sidewall_cap.length} µm, "
191 f"distance {sidewall_cap.distance} µm, "
192 f"{round(sidewall_cap.cap_value, 3)} fF",
193 [inside_edge, outside_edge]
194 )
196 def output_sideoverlap(self,
197 sideoverlap_cap: SideOverlapCap,
198 inside_edge: kdb.Edge,
199 outside_polygon: kdb.Polygon,
200 lateral_shield: Optional[kdb.Region]):
201 if not self.report_caps:
202 return
203 cat_sideoverlap_layer_inside = self.report.create_category(self.cat_fringe,
204 f"inside_layer={sideoverlap_cap.key.layer_inside}")
205 cat_sideoverlap_net_inside = self.report.create_category(cat_sideoverlap_layer_inside,
206 f'inside_net={sideoverlap_cap.key.net_inside}')
207 cat_sideoverlap_layer_outside = self.report.create_category(cat_sideoverlap_net_inside,
208 f'outside_layer={sideoverlap_cap.key.layer_outside}')
209 cat_sideoverlap_net_outside = self.report.create_category(cat_sideoverlap_layer_outside,
210 f'outside_net={sideoverlap_cap.key.net_outside}')
211 self.category_name_counter[cat_sideoverlap_net_outside.path()] += 1
213 cat_sideoverlap_cap = self.report.create_category(
214 cat_sideoverlap_net_outside,
215 f"#{self.category_name_counter[cat_sideoverlap_net_outside.path()]}: "
216 f"{round(sideoverlap_cap.cap_value, 3)} fF"
217 )
219 self.output_shapes(cat_sideoverlap_cap, 'Inside Edge', inside_edge)
221 shapes = kdb.Shapes()
222 shapes.insert(outside_polygon)
223 self.output_shapes(cat_sideoverlap_cap, 'Outside Polygon', shapes)
225 if lateral_shield is not None:
226 self.output_shapes(cat_sideoverlap_cap, 'Lateral Shield',
227 [lateral_shield])
229 def output_edge_neighborhood(self,
230 inside_layer: LayerName,
231 all_layer_names: List[LayerName],
232 edge: kdb.EdgeWithProperties,
233 neighborhood: EdgeNeighborhood,
234 geometry_restorer: GeometryRestorer):
235 cat_en_layer_inside = self.report.create_category(self.cat_edge_neighborhood, f"inside_layer={inside_layer}")
236 inside_net = edge.property('net')
237 cat_en_net_inside = self.report.create_category(cat_en_layer_inside, f'inside_net={inside_net}')
239 for edge_interval, polygons_by_child in neighborhood:
240 cat_en_edge_interval = self.report.create_category(cat_en_net_inside, f"Edge Interval: {edge_interval}")
241 self.category_name_counter[cat_en_edge_interval.path()] += 1
242 cat_en_edge = self.report.create_category(
243 cat_en_edge_interval,
244 f"#{self.category_name_counter[cat_en_edge_interval.path()]}"
245 )
246 self.output_shapes(cat_en_edge, "Edge", [edge]) # geometry_restorer.restore_edge(edge))
248 for child_index, polygons in polygons_by_child.items():
249 self.output_shapes(
250 cat_en_edge,
251 f"Child {child_index}: "
252 f"{child_index < len(all_layer_names) and all_layer_names[child_index] or 'None'}",
253 [geometry_restorer.restore_polygon(p) for p in polygons]
254 )
256 def output_devices(self,
257 devices: List[device_pb2.Device]):
258 for d in devices:
259 self.output_device(d)
261 def output_device_terminals(self,
262 terminals: List[device_pb2.Device.Terminal],
263 category: rdb.RdbCategory):
264 for t in terminals:
265 for l2r in t.region_by_layer:
266 r = self.shapes_converter.klayout_region(l2r.region)
268 self.output_shapes(
269 category,
270 f"{t.name}: net {t.net_name}, layer {l2r.layer.canonical_layer_name}",
271 r
272 )
274 def output_device(self,
275 device: device_pb2.Device):
276 cat_device = self.report.create_category(
277 self.cat_rex_request_devices,
278 f"{device.device_name}: {device.device_class_name}"
279 )
280 cat_device_params = self.report.create_category(cat_device, 'Params')
281 for p in device.parameters:
282 self.report.create_category(cat_device_params, f"{p.name}: {p.value}")
284 cat_device_terminals = self.report.create_category(cat_device, 'Terminals')
285 self.output_device_terminals(terminals=device.terminals, category=cat_device_terminals)
287 def output_pins(self, pins: List[pin_pb2.Pin], category: rdb.RdbCategory):
288 for p in pins:
289 self.output_pb_pin(p, category)
291 def output_pb_pin(self, pin: pin_pb2.Pin, category: rdb.RdbCategory):
292 cat_pin = self.report.create_category(
293 category,
294 f"{pin.label} (net {pin.net_name} on layer {pin.layer.canonical_layer_name})"
295 )
296 marker_box = self.marker_box_for_pb_point(pin.label_point)
297 self.output_shapes(cat_pin, "label point",
298 [self.shapes_converter.klayout_box(marker_box)])
300 def output_via(self,
301 via_name: LayerName,
302 bottom_layer: LayerName,
303 top_layer: LayerName,
304 net: str,
305 via_width: float,
306 via_spacing: float,
307 via_border: float,
308 polygon: kdb.Polygon,
309 ohm: float,
310 comment: str):
311 cat_via_layers = self.report.create_category(
312 self.cat_vias,
313 f"{via_name} ({bottom_layer} ↔ {top_layer}) (w={via_width}, sp={via_spacing}, b={via_border})"
314 )
316 self.category_name_counter[cat_via_layers.path()] += 1
318 self.output_shapes(
319 cat_via_layers,
320 f"#{self.category_name_counter[cat_via_layers.path()]} "
321 f"{ohm} Ω (net {net}) | {comment}",
322 [polygon]
323 )
325 def output_pin(self,
326 layer_name: LayerName,
327 pin_point: kdb.Box,
328 label: kdb.Text):
329 cat_pin_layer = self.report.create_category(self.cat_pins, layer_name)
330 sh = kdb.Shapes()
331 sh.insert(pin_point)
332 self.output_shapes(cat_pin_layer, label.string, sh)
334 def output_rex_tech(self, tech: r_extractor_tech_pb2.RExtractorTech):
335 layer_by_id = {c.layer.id: c.layer for c in tech.conductors}
337 self.report.create_category(self.cat_rex_tech, f"Skip simplify: {tech.skip_simplify}")
338 cat_conductors = self.report.create_category(self.cat_rex_tech, 'Conductors')
339 cat_vias = self.report.create_category(self.cat_rex_tech, 'Vias')
340 for c in tech.conductors:
341 self.report.create_category(
342 cat_conductors,
343 f"{c.layer.id}: {c.layer.canonical_layer_name} (LVS {c.layer.lvs_layer_name}), "
344 f"{round(c.resistance, 3)} mΩ/µm^2"
345 )
346 for v in tech.vias:
347 bot = layer_by_id[v.bottom_conductor.id].canonical_layer_name
348 top = layer_by_id[v.top_conductor.id].canonical_layer_name
349 self.report.create_category(
350 cat_vias,
351 f"{v.layer.id}: {v.layer.canonical_layer_name} (LVS {v.layer.lvs_layer_name}, "
352 f"{bot}↔︎{top}), "
353 f"{round(v.resistance, 3)} mΩ/µm^2"
354 )
356 def output_net_extraction_request(self, request: pex_request_pb2.RNetExtractionRequest):
357 cat_req = self.report.create_category(self.cat_rex_request_network_extraction, f"Net {request.net_name}")
358 cat_pins = self.report.create_category(cat_req, "Pins")
359 cat_device_terminals = self.report.create_category(cat_req, "Device Terminals")
360 cat_layer_regions = self.report.create_category(cat_req, "Layer Regions")
361 self.output_pins(request.pins, cat_pins)
362 self.output_device_terminals(terminals=request.device_terminals, category=cat_device_terminals)
363 for l2r in request.region_by_layer:
364 self.output_shapes(cat_layer_regions, f"Layer {l2r.layer.canonical_layer_name}",
365 self.shapes_converter.klayout_region(l2r.region))
367 def output_rex_request(self, request: pex_request_pb2.RExtractionRequest):
368 self.output_rex_tech(request.tech)
369 self.output_devices(request.devices)
370 self.output_pins(request.pins, category=self.cat_rex_request_pins)
372 for r in request.net_extraction_requests:
373 self.output_net_extraction_request(r)
375 def output_rex_result_network(self, network: r_network_pb2.RNetwork):
376 cat_network = self.report.create_category(self.cat_rex_result_networks, f"Net {network.net_name}")
377 cat_nodes = self.report.create_category(cat_network, f"Nodes")
378 cat_elements = self.report.create_category(cat_network, f"Elements")
380 node_id_to_node: Dict[int, r_network_pb2.RNode] = {}
382 for node in network.nodes:
383 self.output_node(node, category=cat_nodes)
384 node_id_to_node[node.node_id] = node
385 for element in network.elements:
386 self.output_element(element, node_id_to_node, category=cat_elements)
388 def output_rex_result(self,
389 result: pex_result_pb2.RExtractionResult):
390 for network in result.networks:
391 self.output_rex_result_network(network)
393 def marker_box_for_pb_point(self, point: shapes_pb2.Point) -> shapes_pb2.Box:
394 sized_value = 5
395 box = shapes_pb2.Box()
396 box.lower_left.x = point.x - sized_value
397 box.lower_left.y = point.y - sized_value
398 box.upper_right.x = point.x + sized_value
399 box.upper_right.y = point.y + sized_value
400 if point.net:
401 box.net = point.net
402 return box
404 def marker_box_for_node_location(self, node: r_network_pb2.RNode) -> kdb.Box:
405 box: shapes_pb2.Box
406 match node.location.kind:
407 case location_pb2.Location.Kind.LOCATION_KIND_POINT:
408 # create marker around point for better visiblity
409 box = self.marker_box_for_pb_point(node.location.point)
410 case location_pb2.Location.Kind.LOCATION_KIND_BOX:
411 box = node.location.box
412 case _:
413 raise NotImplementedError("unknown location type: {node.location_type}")
414 return self.shapes_converter.klayout_box(box)
416 def marker_arrow_between_nodes(self,
417 node_a: r_network_pb2.RNode,
418 node_b: r_network_pb2.RNode) -> kdb.Polygon:
419 a_center = self.marker_box_for_node_location(node_a).center()
420 b_center = self.marker_box_for_node_location(node_b).center()
421 path = kdb.Path([self.shapes_converter.klayout_point(a_center),
422 self.shapes_converter.klayout_point(b_center)],
423 width=5)
424 return path.polygon()
426 def output_node(self,
427 node: r_network_pb2.RNode,
428 category: rdb.RdbCategory):
429 node_kind: str
430 match node.node_kind:
431 case r_network_pb2.RNode.Kind.KIND_UNSPECIFIED:
432 node_kind = '???'
433 case r_network_pb2.RNode.Kind.KIND_PIN:
434 node_kind = 'Pin'
435 case r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL:
436 node_kind = 'Device Terminal'
437 case r_network_pb2.RNode.Kind.KIND_WIRE_JUNCTION:
438 node_kind = 'Wire Junction'
439 case r_network_pb2.RNode.Kind.KIND_VIA_JUNCTION:
440 node_kind = 'Via Junction'
441 case r_network_pb2.RNode.Kind.KIND_EXCLUSION_PORT:
442 node_kind = 'Exclusion Port'
443 case _:
444 raise NotImplementedError()
446 node_title = f"[{node_kind}] {node.node_name}, port net {node.net_name}, " \
447 f"layer {node.layer_name}"
448 sh = kdb.Shapes()
449 sh.insert(self.marker_box_for_node_location(node))
450 self.output_shapes(category, node_title, sh)
452 def output_element(self,
453 element: r_network_pb2.RElement,
454 node_id_to_node: Dict[int, r_network_pb2.RNode],
455 category: rdb.RdbCategory):
456 a = node_id_to_node[element.node_a.node_id]
457 b = node_id_to_node[element.node_b.node_id]
459 if element.resistance >= 0.001:
460 ohm = f"{round(element.resistance, 3)} Ω"
461 else:
462 ohm = f"{round(element.resistance * 1000.0, 6)} mΩ"
464 element_title = f"{a.node_name} ({a.layer_name}) ↔︎ " \
465 f"{b.node_name} ({b.layer_name})" \
466 f": {ohm}"
467 polygon = self.marker_arrow_between_nodes(a, b)
468 self.output_shapes(category, element_title, [polygon])