Coverage for klayout_pex/rcx25/extraction_reporter.py: 33%
228 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 functools import cached_property
27import klayout.rdb as rdb
28import klayout.db as kdb
30from .extraction_results import *
31from .types import EdgeNeighborhood, LayerName
32from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer
33from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter
35import klayout_pex_protobuf.kpex.geometry.shapes_pb2 as shapes_pb2
36import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2
37import klayout_pex_protobuf.kpex.layout.pin_pb2 as pin_pb2
38import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2
39import klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 as r_extractor_tech_pb2
40import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2
41import klayout_pex_protobuf.kpex.request.pex_request_pb2 as pex_request_pb2
42import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2
44VarShapes = kdb.Shapes | kdb.Region | List[kdb.Edge] | List[kdb.Polygon | kdb.Box]
47class ExtractionReporter:
48 def __init__(self,
49 cell_name: str,
50 dbu: float):
51 self.report = rdb.ReportDatabase(f"PEX {cell_name}")
52 self.cell = self.report.create_cell(cell_name)
53 self.dbu = dbu
54 self.dbu_trans = kdb.CplxTrans(mag=dbu)
55 self.category_name_counter: Dict[str, int] = defaultdict(int)
56 self.shapes_converter = ShapesConverter(dbu=dbu)
58 @cached_property
59 def cat_common(self) -> rdb.RdbCategory:
60 return self.report.create_category('Common')
62 @cached_property
63 def cat_pins(self) -> rdb.RdbCategory:
64 return self.report.create_category("Pins")
66 @cached_property
67 def cat_rex_request(self) -> rdb.RdbCategory:
68 return self.report.create_category("[R] Extraction Request")
70 @cached_property
71 def cat_rex_tech(self) -> rdb.RdbCategory:
72 return self.report.create_category(self.cat_rex_request, "[R] Extraction Tech")
74 @cached_property
75 def cat_rex_request_devices(self) -> rdb.RdbCategory:
76 return self.report.create_category(self.cat_rex_request, "Devices")
78 @cached_property
79 def cat_rex_request_pins(self) -> rdb.RdbCategory:
80 return self.report.create_category(self.cat_rex_request, "Pins")
82 @cached_property
83 def cat_rex_request_network_extraction(self) -> rdb.RdbCategory:
84 return self.report.create_category(self.cat_rex_request, "Network Extraction Request")
86 @cached_property
87 def cat_rex_result(self) -> rdb.RdbCategory:
88 return self.report.create_category("[R] Extraction Result")
90 @cached_property
91 def cat_rex_result_networks(self) -> rdb.RdbCategory:
92 return self.report.create_category(self.cat_rex_result, "Networks")
94 @cached_property
95 def cat_rex_nodes(self) -> rdb.RdbCategory:
96 return self.report.create_category(self.cat_rex_result, "Nodes")
98 @cached_property
99 def cat_rex_elements(self) -> rdb.RdbCategory:
100 return self.report.create_category(self.cat_rex_result, "Net Elements (Edges)")
102 @cached_property
103 def cat_devices(self) -> rdb.RdbCategory:
104 return self.report.create_category("Devices")
106 @cached_property
107 def cat_vias(self) -> rdb.RdbCategory:
108 return self.report.create_category("Vias")
110 @cached_property
111 def cat_overlap(self) -> rdb.RdbCategory:
112 return self.report.create_category("[C] Overlap")
114 @cached_property
115 def cat_sidewall(self) -> rdb.RdbCategory:
116 return self.report.create_category("[C] Sidewall")
118 @cached_property
119 def cat_fringe(self) -> rdb.RdbCategory:
120 return self.report.create_category("[C] Fringe / Side Overlap")
122 @cached_property
123 def cat_edge_neighborhood(self) -> rdb.RdbCategory:
124 return self.report.create_category("[C] Edge Neighborhood Visitor")
126 def save(self, path: str):
127 self.report.save(path)
129 def output_shapes(self,
130 parent_category: rdb.RdbCategory,
131 category_name: str,
132 shapes: VarShapes) -> rdb.RdbCategory:
133 rdb_cat = self.report.create_category(parent_category, category_name)
134 self.report.create_items(self.cell.rdb_id(), ## TODO: if later hierarchical mode is introduced
135 rdb_cat.rdb_id(),
136 self.dbu_trans,
137 shapes)
138 return rdb_cat
140 def output_overlap(self,
141 overlap_cap: OverlapCap,
142 bottom_polygon: kdb.PolygonWithProperties,
143 top_polygon: kdb.PolygonWithProperties,
144 overlap_area: kdb.Region):
145 cat_overlap_top_layer = self.report.create_category(self.cat_overlap,
146 f"top_layer={overlap_cap.key.layer_top}")
147 cat_overlap_bot_layer = self.report.create_category(cat_overlap_top_layer,
148 f'bot_layer={overlap_cap.key.layer_bot}')
149 cat_overlap_nets = self.report.create_category(cat_overlap_bot_layer,
150 f'{overlap_cap.key.net_top} – {overlap_cap.key.net_bot}')
151 self.category_name_counter[cat_overlap_nets.path()] += 1
152 cat_overlap_cap = self.report.create_category(
153 cat_overlap_nets,
154 f"#{self.category_name_counter[cat_overlap_nets.path()]} "
155 f"{round(overlap_cap.cap_value, 3)} fF",
156 )
158 self.output_shapes(cat_overlap_cap, "Top Polygon", [top_polygon])
159 self.output_shapes(cat_overlap_cap, "Bottom Polygon", [bottom_polygon])
160 self.output_shapes(cat_overlap_cap, "Overlap Area", overlap_area)
162 def output_sidewall(self,
163 sidewall_cap: SidewallCap,
164 inside_edge: kdb.Edge,
165 outside_edge: kdb.Edge):
166 cat_sidewall_layer = self.report.create_category(self.cat_sidewall,
167 f"layer={sidewall_cap.key.layer}")
168 cat_sidewall_net_inside = self.report.create_category(cat_sidewall_layer,
169 f'inside={sidewall_cap.key.net1}')
170 cat_sidewall_net_outside = self.report.create_category(cat_sidewall_net_inside,
171 f'outside={sidewall_cap.key.net2}')
172 self.category_name_counter[cat_sidewall_net_outside.path()] += 1
174 self.output_shapes(
175 cat_sidewall_net_outside,
176 f"#{self.category_name_counter[cat_sidewall_net_outside.path()]}: "
177 f"len {sidewall_cap.length} µm, "
178 f"distance {sidewall_cap.distance} µm, "
179 f"{round(sidewall_cap.cap_value, 3)} fF",
180 [inside_edge, outside_edge]
181 )
183 def output_sideoverlap(self,
184 sideoverlap_cap: SideOverlapCap,
185 inside_edge: kdb.Edge,
186 outside_polygon: kdb.Polygon,
187 lateral_shield: Optional[kdb.Region]):
188 cat_sideoverlap_layer_inside = self.report.create_category(self.cat_fringe,
189 f"inside_layer={sideoverlap_cap.key.layer_inside}")
190 cat_sideoverlap_net_inside = self.report.create_category(cat_sideoverlap_layer_inside,
191 f'inside_net={sideoverlap_cap.key.net_inside}')
192 cat_sideoverlap_layer_outside = self.report.create_category(cat_sideoverlap_net_inside,
193 f'outside_layer={sideoverlap_cap.key.layer_outside}')
194 cat_sideoverlap_net_outside = self.report.create_category(cat_sideoverlap_layer_outside,
195 f'outside_net={sideoverlap_cap.key.net_outside}')
196 self.category_name_counter[cat_sideoverlap_net_outside.path()] += 1
198 cat_sideoverlap_cap = self.report.create_category(
199 cat_sideoverlap_net_outside,
200 f"#{self.category_name_counter[cat_sideoverlap_net_outside.path()]}: "
201 f"{round(sideoverlap_cap.cap_value, 3)} fF"
202 )
204 self.output_shapes(cat_sideoverlap_cap, 'Inside Edge', inside_edge)
206 shapes = kdb.Shapes()
207 shapes.insert(outside_polygon)
208 self.output_shapes(cat_sideoverlap_cap, 'Outside Polygon', shapes)
210 if lateral_shield is not None:
211 self.output_shapes(cat_sideoverlap_cap, 'Lateral Shield',
212 [lateral_shield])
214 def output_edge_neighborhood(self,
215 inside_layer: LayerName,
216 all_layer_names: List[LayerName],
217 edge: kdb.EdgeWithProperties,
218 neighborhood: EdgeNeighborhood,
219 geometry_restorer: GeometryRestorer):
220 cat_en_layer_inside = self.report.create_category(self.cat_edge_neighborhood, f"inside_layer={inside_layer}")
221 inside_net = edge.property('net')
222 cat_en_net_inside = self.report.create_category(cat_en_layer_inside, f'inside_net={inside_net}')
224 for edge_interval, polygons_by_child in neighborhood:
225 cat_en_edge_interval = self.report.create_category(cat_en_net_inside, f"Edge Interval: {edge_interval}")
226 self.category_name_counter[cat_en_edge_interval.path()] += 1
227 cat_en_edge = self.report.create_category(
228 cat_en_edge_interval,
229 f"#{self.category_name_counter[cat_en_edge_interval.path()]}"
230 )
231 self.output_shapes(cat_en_edge, "Edge", [edge]) # geometry_restorer.restore_edge(edge))
233 for child_index, polygons in polygons_by_child.items():
234 self.output_shapes(
235 cat_en_edge,
236 f"Child {child_index}: "
237 f"{child_index < len(all_layer_names) and all_layer_names[child_index] or 'None'}",
238 [geometry_restorer.restore_polygon(p) for p in polygons]
239 )
241 def output_devices(self,
242 devices: List[device_pb2.Device]):
243 for d in devices:
244 self.output_device(d)
246 def output_device_terminals(self,
247 terminals: List[device_pb2.Device.Terminal],
248 category: rdb.RdbCategory):
249 for t in terminals:
250 for l2r in t.region_by_layer:
251 r = self.shapes_converter.klayout_region(l2r.region)
253 self.output_shapes(
254 category,
255 f"{t.name}: net {t.net_name}, layer {l2r.layer.canonical_layer_name}",
256 r
257 )
259 def output_device(self,
260 device: device_pb2.Device):
261 cat_device = self.report.create_category(
262 self.cat_rex_request_devices,
263 f"{device.device_name}: {device.device_class_name}"
264 )
265 cat_device_params = self.report.create_category(cat_device, 'Params')
266 for p in device.parameters:
267 self.report.create_category(cat_device_params, f"{p.name}: {p.value}")
269 cat_device_terminals = self.report.create_category(cat_device, 'Terminals')
270 self.output_device_terminals(terminals=device.terminals, category=cat_device_terminals)
272 def output_pins(self, pins: List[pin_pb2.Pin], category: rdb.RdbCategory):
273 for p in pins:
274 self.output_pb_pin(p, category)
276 def output_pb_pin(self, pin: pin_pb2.Pin, category: rdb.RdbCategory):
277 cat_pin = self.report.create_category(
278 category,
279 f"{pin.label} (net {pin.net_name} on layer {pin.layer.canonical_layer_name})"
280 )
281 marker_box = self.marker_box_for_pb_point(pin.label_point)
282 self.output_shapes(cat_pin, "label point",
283 [self.shapes_converter.klayout_box(marker_box)])
285 def output_via(self,
286 via_name: LayerName,
287 bottom_layer: LayerName,
288 top_layer: LayerName,
289 net: str,
290 via_width: float,
291 via_spacing: float,
292 via_border: float,
293 polygon: kdb.Polygon,
294 ohm: float,
295 comment: str):
296 cat_via_layers = self.report.create_category(
297 self.cat_vias,
298 f"{via_name} ({bottom_layer} ↔ {top_layer}) (w={via_width}, sp={via_spacing}, b={via_border})"
299 )
301 self.category_name_counter[cat_via_layers.path()] += 1
303 self.output_shapes(
304 cat_via_layers,
305 f"#{self.category_name_counter[cat_via_layers.path()]} "
306 f"{ohm} Ω (net {net}) | {comment}",
307 [polygon]
308 )
310 def output_pin(self,
311 layer_name: LayerName,
312 pin_point: kdb.Box,
313 label: kdb.Text):
314 cat_pin_layer = self.report.create_category(self.cat_pins, layer_name)
315 sh = kdb.Shapes()
316 sh.insert(pin_point)
317 self.output_shapes(cat_pin_layer, label.string, sh)
319 def output_rex_tech(self, tech: r_extractor_tech_pb2.RExtractorTech):
320 layer_by_id = {c.layer.id: c.layer for c in tech.conductors}
322 self.report.create_category(self.cat_rex_tech, f"Skip simplify: {tech.skip_simplify}")
323 cat_conductors = self.report.create_category(self.cat_rex_tech, 'Conductors')
324 cat_vias = self.report.create_category(self.cat_rex_tech, 'Vias')
325 for c in tech.conductors:
326 self.report.create_category(
327 cat_conductors,
328 f"{c.layer.id}: {c.layer.canonical_layer_name} (LVS {c.layer.lvs_layer_name}), "
329 f"{round(c.resistance, 3)} mΩ/µm^2"
330 )
331 for v in tech.vias:
332 bot = layer_by_id[v.bottom_conductor.id].canonical_layer_name
333 top = layer_by_id[v.top_conductor.id].canonical_layer_name
334 self.report.create_category(
335 cat_vias,
336 f"{v.layer.id}: {v.layer.canonical_layer_name} (LVS {v.layer.lvs_layer_name}, "
337 f"{bot}↔︎{top}), "
338 f"{round(v.resistance, 3)} mΩ/µm^2"
339 )
341 def output_net_extraction_request(self, request: pex_request_pb2.RNetExtractionRequest):
342 cat_req = self.report.create_category(self.cat_rex_request_network_extraction, f"Net {request.net_name}")
343 cat_pins = self.report.create_category(cat_req, "Pins")
344 cat_device_terminals = self.report.create_category(cat_req, "Device Terminals")
345 cat_layer_regions = self.report.create_category(cat_req, "Layer Regions")
346 self.output_pins(request.pins, cat_pins)
347 self.output_device_terminals(terminals=request.device_terminals, category=cat_device_terminals)
348 for l2r in request.region_by_layer:
349 self.output_shapes(cat_layer_regions, f"Layer {l2r.layer.canonical_layer_name}",
350 self.shapes_converter.klayout_region(l2r.region))
352 def output_rex_request(self, request: pex_request_pb2.RExtractionRequest):
353 self.output_rex_tech(request.tech)
354 self.output_devices(request.devices)
355 self.output_pins(request.pins, category=self.cat_rex_request_pins)
357 for r in request.net_extraction_requests:
358 self.output_net_extraction_request(r)
360 def output_rex_result_network(self, network: r_network_pb2.RNetwork):
361 cat_network = self.report.create_category(self.cat_rex_result_networks, f"Net {network.net_name}")
362 cat_nodes = self.report.create_category(cat_network, f"Nodes")
363 cat_elements = self.report.create_category(cat_network, f"Elements")
365 node_id_to_node: Dict[int, r_network_pb2.RNode] = {}
367 for node in network.nodes:
368 self.output_node(node, category=cat_nodes)
369 node_id_to_node[node.node_id] = node
370 for element in network.elements:
371 self.output_element(element, node_id_to_node, category=cat_elements)
373 def output_rex_result(self,
374 result: pex_result_pb2.RExtractionResult):
375 for network in result.networks:
376 self.output_rex_result_network(network)
378 def marker_box_for_pb_point(self, point: shapes_pb2.Point) -> shapes_pb2.Box:
379 sized_value = 5
380 box = shapes_pb2.Box()
381 box.lower_left.x = point.x - sized_value
382 box.lower_left.y = point.y - sized_value
383 box.upper_right.x = point.x + sized_value
384 box.upper_right.y = point.y + sized_value
385 if point.net:
386 box.net = point.net
387 return box
389 def marker_box_for_node_location(self, node: r_network_pb2.RNode) -> kdb.Box:
390 box: shapes_pb2.Box
391 match node.location.kind:
392 case location_pb2.Location.Kind.LOCATION_KIND_POINT:
393 # create marker around point for better visiblity
394 box = self.marker_box_for_pb_point(node.location.point)
395 case location_pb2.Location.Kind.LOCATION_KIND_BOX:
396 box = node.location.box
397 case _:
398 raise NotImplementedError("unknown location type: {node.location_type}")
399 return self.shapes_converter.klayout_box(box)
401 def marker_arrow_between_nodes(self,
402 node_a: r_network_pb2.RNode,
403 node_b: r_network_pb2.RNode) -> kdb.Polygon:
404 a_center = self.marker_box_for_node_location(node_a).center()
405 b_center = self.marker_box_for_node_location(node_b).center()
406 path = kdb.Path([self.shapes_converter.klayout_point(a_center),
407 self.shapes_converter.klayout_point(b_center)],
408 width=5)
409 return path.polygon()
411 def output_node(self,
412 node: r_network_pb2.RNode,
413 category: rdb.RdbCategory):
414 node_title = f"{node.node_name}, port net {node.net_name}, " \
415 f"layer {node.layer_name}"
416 sh = kdb.Shapes()
417 sh.insert(self.marker_box_for_node_location(node))
418 self.output_shapes(category, node_title, sh)
420 def output_element(self,
421 element: r_network_pb2.RElement,
422 node_id_to_node: Dict[int, r_network_pb2.RNode],
423 category: rdb.RdbCategory):
424 a = node_id_to_node[element.node_a.node_id]
425 b = node_id_to_node[element.node_b.node_id]
427 if element.resistance >= 0.001:
428 ohm = f"{round(element.resistance, 3)} Ω"
429 else:
430 ohm = f"{round(element.resistance * 1000.0, 6)} mΩ"
432 element_title = f"{a.node_name} ({a.layer_name}) ↔︎ " \
433 f"{b.node_name} ({b.layer_name})" \
434 f": {ohm}"
435 polygon = self.marker_arrow_between_nodes(a, b)
436 self.output_shapes(category, element_title, [polygon])