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

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# 

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, LayerName 

32from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer 

33from klayout_pex.klayout.shapes_pb2_converter import ShapesConverter 

34 

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 

43 

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

45 

46 

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) 

57 

58 @cached_property 

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

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

61 

62 @cached_property 

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

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

65 

66 @cached_property 

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

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

69 

70 @cached_property 

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

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

73 

74 @cached_property 

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

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

77 

78 @cached_property 

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

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

81 

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

85 

86 @cached_property 

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

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

89 

90 @cached_property 

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

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

93 

94 @cached_property 

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

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

97 

98 @cached_property 

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

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

101 

102 @cached_property 

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

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

105 

106 @cached_property 

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

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

109 

110 @cached_property 

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

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

113 

114 @cached_property 

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

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

117 

118 @cached_property 

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

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

121 

122 @cached_property 

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

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

125 

126 def save(self, path: str): 

127 self.report.save(path) 

128 

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 

139 

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 ) 

157 

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) 

161 

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 

173 

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 ) 

182 

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 

197 

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 ) 

203 

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

205 

206 shapes = kdb.Shapes() 

207 shapes.insert(outside_polygon) 

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

209 

210 if lateral_shield is not None: 

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

212 [lateral_shield]) 

213 

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

223 

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

232 

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 ) 

240 

241 def output_devices(self, 

242 devices: List[device_pb2.Device]): 

243 for d in devices: 

244 self.output_device(d) 

245 

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) 

252 

253 self.output_shapes( 

254 category, 

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

256 r 

257 ) 

258 

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

268 

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

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

271 

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) 

275 

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

284 

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 ) 

300 

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

302 

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 ) 

309 

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) 

318 

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} 

321 

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 ) 

340 

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

351 

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) 

356 

357 for r in request.net_extraction_requests: 

358 self.output_net_extraction_request(r) 

359 

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

364 

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

366 

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) 

372 

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) 

377 

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 

388 

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) 

400 

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

410 

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) 

419 

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] 

426 

427 if element.resistance >= 0.001: 

428 ohm = f"{round(element.resistance, 3)} Ω" 

429 else: 

430 ohm = f"{round(element.resistance * 1000.0, 6)}" 

431 

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