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

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# 

24 

25from functools import cached_property 

26 

27import klayout.rdb as rdb 

28import klayout.db as kdb 

29 

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 

36 

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 

45 

46VarShapes = kdb.Shapes | kdb.Region | List[kdb.Edge] | List[kdb.Polygon | kdb.Box] 

47 

48 

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 

66 

67 @cached_property 

68 def cat_common(self) -> rdb.RdbCategory: 

69 return self.report.create_category('Common') 

70 

71 @cached_property 

72 def cat_pins(self) -> rdb.RdbCategory: 

73 return self.report.create_category("Pins") 

74 

75 @cached_property 

76 def cat_rex_request(self) -> rdb.RdbCategory: 

77 return self.report.create_category("[R] Extraction Request") 

78 

79 @cached_property 

80 def cat_rex_tech(self) -> rdb.RdbCategory: 

81 return self.report.create_category(self.cat_rex_request, "[R] Extraction Tech") 

82 

83 @cached_property 

84 def cat_rex_request_devices(self) -> rdb.RdbCategory: 

85 return self.report.create_category(self.cat_rex_request, "Devices") 

86 

87 @cached_property 

88 def cat_rex_request_pins(self) -> rdb.RdbCategory: 

89 return self.report.create_category(self.cat_rex_request, "Pins") 

90 

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") 

94 

95 @cached_property 

96 def cat_rex_result(self) -> rdb.RdbCategory: 

97 return self.report.create_category("[R] Extraction Result") 

98 

99 @cached_property 

100 def cat_rex_result_networks(self) -> rdb.RdbCategory: 

101 return self.report.create_category(self.cat_rex_result, "Networks") 

102 

103 @cached_property 

104 def cat_rex_nodes(self) -> rdb.RdbCategory: 

105 return self.report.create_category(self.cat_rex_result, "Nodes") 

106 

107 @cached_property 

108 def cat_rex_elements(self) -> rdb.RdbCategory: 

109 return self.report.create_category(self.cat_rex_result, "Net Elements (Edges)") 

110 

111 @cached_property 

112 def cat_devices(self) -> rdb.RdbCategory: 

113 return self.report.create_category("Devices") 

114 

115 @cached_property 

116 def cat_vias(self) -> rdb.RdbCategory: 

117 return self.report.create_category("Vias") 

118 

119 @cached_property 

120 def cat_overlap(self) -> rdb.RdbCategory: 

121 return self.report.create_category("[C] Overlap") 

122 

123 @cached_property 

124 def cat_sidewall(self) -> rdb.RdbCategory: 

125 return self.report.create_category("[C] Sidewall") 

126 

127 @cached_property 

128 def cat_fringe(self) -> rdb.RdbCategory: 

129 return self.report.create_category("[C] Fringe / Side Overlap") 

130 

131 @cached_property 

132 def cat_edge_neighborhood(self) -> rdb.RdbCategory: 

133 return self.report.create_category("[C] Edge Neighborhood Visitor") 

134 

135 def save(self, path: str): 

136 write_output_file(self.report.save, path) 

137 

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 

148 

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 ) 

168 

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) 

172 

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 

186 

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 ) 

195 

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 

212 

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 ) 

218 

219 self.output_shapes(cat_sideoverlap_cap, 'Inside Edge', inside_edge) 

220 

221 shapes = kdb.Shapes() 

222 shapes.insert(outside_polygon) 

223 self.output_shapes(cat_sideoverlap_cap, 'Outside Polygon', shapes) 

224 

225 if lateral_shield is not None: 

226 self.output_shapes(cat_sideoverlap_cap, 'Lateral Shield', 

227 [lateral_shield]) 

228 

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}') 

238 

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)) 

247 

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 ) 

255 

256 def output_devices(self, 

257 devices: List[device_pb2.Device]): 

258 for d in devices: 

259 self.output_device(d) 

260 

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) 

267 

268 self.output_shapes( 

269 category, 

270 f"{t.name}: net {t.net_name}, layer {l2r.layer.canonical_layer_name}", 

271 r 

272 ) 

273 

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}") 

283 

284 cat_device_terminals = self.report.create_category(cat_device, 'Terminals') 

285 self.output_device_terminals(terminals=device.terminals, category=cat_device_terminals) 

286 

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) 

290 

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)]) 

299 

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 ) 

315 

316 self.category_name_counter[cat_via_layers.path()] += 1 

317 

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 ) 

324 

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) 

333 

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} 

336 

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 ) 

355 

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)) 

366 

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) 

371 

372 for r in request.net_extraction_requests: 

373 self.output_net_extraction_request(r) 

374 

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") 

379 

380 node_id_to_node: Dict[int, r_network_pb2.RNode] = {} 

381 

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) 

387 

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) 

392 

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 

403 

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) 

415 

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() 

425 

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() 

445 

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) 

451 

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] 

458 

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Ω" 

463 

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])