Coverage for klayout_pex/rcx25/extractor.py: 39%

225 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-12 13:45 +0000

1#! /usr/bin/env python3 

2# 

3# -------------------------------------------------------------------------------- 

4# SPDX-FileCopyrightText: 2024 Martin Jan Köhler and Harald Pretl 

5# Johannes Kepler University, Institute for Integrated Circuits. 

6# 

7# This file is part of KPEX  

8# (see https://github.com/martinjankoehler/klayout-pex). 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the GNU General Public License 

21# along with this program. If not, see <http://www.gnu.org/licenses/>. 

22# SPDX-License-Identifier: GPL-3.0-or-later 

23# -------------------------------------------------------------------------------- 

24# 

25import math 

26 

27import klayout.db as kdb 

28 

29from .r.conductance import Conductance 

30from ..klayout.lvsdb_extractor import KLayoutExtractionContext, GDSPair 

31from ..log import ( 

32 debug, 

33 warning, 

34 error, 

35 info, 

36 subproc 

37) 

38from ..tech_info import TechInfo 

39from .extraction_results import * 

40from .extraction_reporter import ExtractionReporter 

41from .pex_mode import PEXMode 

42from klayout_pex.rcx25.c.overlap_extractor import OverlapExtractor 

43from klayout_pex.rcx25.c.sidewall_and_fringe_extractor import SidewallAndFringeExtractor 

44from .r.resistor_extraction import ResistorExtraction 

45from .r.resistor_network import ( 

46 ResistorNetworks, 

47 ViaResistor, 

48 ViaJunction, 

49 DeviceTerminal, 

50 MultiLayerResistanceNetwork 

51) 

52 

53 

54class RCX25Extractor: 

55 def __init__(self, 

56 pex_context: KLayoutExtractionContext, 

57 pex_mode: PEXMode, 

58 scale_ratio_to_fit_halo: bool, 

59 delaunay_amax: float, 

60 delaunay_b: float, 

61 tech_info: TechInfo, 

62 report_path: str): 

63 self.pex_context = pex_context 

64 self.pex_mode = pex_mode 

65 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo 

66 self.delaunay_amax = delaunay_amax 

67 self.delaunay_b = delaunay_b 

68 self.tech_info = tech_info 

69 self.report_path = report_path 

70 

71 if "PolygonWithProperties" not in kdb.__all__: 

72 raise Exception("KLayout version does not support properties (needs 0.30 at least)") 

73 

74 def gds_pair(self, layer_name) -> Optional[GDSPair]: 

75 gds_pair = self.tech_info.gds_pair_for_computed_layer_name.get(layer_name, None) 

76 if not gds_pair: 

77 gds_pair = self.tech_info.gds_pair_for_layer_name.get(layer_name, None) 

78 if not gds_pair: 

79 warning(f"Can't find GDS pair for layer {layer_name}") 

80 return None 

81 return gds_pair 

82 

83 def shapes_of_layer(self, layer_name: str) -> Optional[kdb.Region]: 

84 gds_pair = self.gds_pair(layer_name=layer_name) 

85 if not gds_pair: 

86 return None 

87 

88 shapes = self.pex_context.shapes_of_layer(gds_pair=gds_pair) 

89 if not shapes: 

90 debug(f"Nothing extracted for layer {layer_name}") 

91 

92 return shapes 

93 

94 def extract(self) -> ExtractionResults: 

95 extraction_results = ExtractionResults() 

96 

97 # TODO: for now, we always flatten and have only 1 cell 

98 cell_name = self.pex_context.annotated_top_cell.name 

99 extraction_report = ExtractionReporter(cell_name=cell_name, 

100 dbu=self.pex_context.dbu) 

101 cell_extraction_results = CellExtractionResults(cell_name=cell_name) 

102 

103 # Explicitly log the stacktrace here, because otherwise Exceptions  

104 # raised in the callbacks of *NeighborhoodVisitors can cause RuntimeErrors 

105 # that are not traceable beyond the Region.complex_op() calls 

106 try: 

107 self.extract_cell(results=cell_extraction_results, 

108 report=extraction_report) 

109 except RuntimeError as e: 

110 import traceback 

111 print(f"Caught a RuntimeError: {e}") 

112 traceback.print_exc() 

113 raise 

114 

115 extraction_results.cell_extraction_results[cell_name] = cell_extraction_results 

116 

117 extraction_report.save(self.report_path) 

118 

119 return extraction_results 

120 

121 def extract_cell(self, 

122 results: CellExtractionResults, 

123 report: ExtractionReporter): 

124 netlist: kdb.Netlist = self.pex_context.lvsdb.netlist() 

125 dbu = self.pex_context.dbu 

126 # ------------------------------------------------------------------------ 

127 

128 layer_regions_by_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region) 

129 

130 all_region = kdb.Region() 

131 all_region.enable_properties() 

132 

133 substrate_region = kdb.Region() 

134 substrate_region.enable_properties() 

135 

136 side_halo_um = self.tech_info.tech.process_parasitics.side_halo 

137 substrate_region.insert(self.pex_context.top_cell_bbox().enlarged(side_halo_um / dbu)) # e.g. 8 µm halo 

138 

139 layer_regions_by_name[self.tech_info.internal_substrate_layer_name] = substrate_region 

140 

141 via_name_below_layer_name: Dict[LayerName, Optional[LayerName]] = {} 

142 via_name_above_layer_name: Dict[LayerName, Optional[LayerName]] = {} 

143 via_regions_by_via_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region) 

144 

145 previous_via_name: Optional[str] = None 

146 

147 for metal_layer in self.tech_info.process_metal_layers: 

148 layer_name = metal_layer.name 

149 gds_pair = self.gds_pair(layer_name) 

150 canonical_layer_name = self.tech_info.canonical_layer_name_by_gds_pair[gds_pair] 

151 

152 all_layer_shapes = self.shapes_of_layer(layer_name) 

153 if all_layer_shapes is not None: 

154 all_layer_shapes.enable_properties() 

155 

156 layer_regions_by_name[canonical_layer_name] += all_layer_shapes 

157 layer_regions_by_name[canonical_layer_name].enable_properties() 

158 all_region += all_layer_shapes 

159 

160 if metal_layer.metal_layer.HasField('contact_above'): 

161 contact = metal_layer.metal_layer.contact_above 

162 

163 via_regions = self.shapes_of_layer(contact.name) 

164 if via_regions is not None: 

165 via_regions.enable_properties() 

166 via_regions_by_via_name[contact.name] += via_regions 

167 via_name_above_layer_name[canonical_layer_name] = contact.name 

168 via_name_below_layer_name[canonical_layer_name] = previous_via_name 

169 

170 previous_via_name = contact.name 

171 else: 

172 previous_via_name = None 

173 

174 all_layer_names = list(layer_regions_by_name.keys()) 

175 

176 # ------------------------------------------------------------------------ 

177 if self.pex_mode.need_capacitance(): 

178 overlap_extractor = OverlapExtractor( 

179 all_layer_names=all_layer_names, 

180 layer_regions_by_name=layer_regions_by_name, 

181 dbu=dbu, 

182 tech_info=self.tech_info, 

183 results=results, 

184 report=report 

185 ) 

186 overlap_extractor.extract() 

187 

188 sidewall_and_fringe_extractor = SidewallAndFringeExtractor( 

189 all_layer_names=all_layer_names, 

190 layer_regions_by_name=layer_regions_by_name, 

191 dbu=dbu, 

192 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo, 

193 tech_info=self.tech_info, 

194 results=results, 

195 report=report 

196 ) 

197 sidewall_and_fringe_extractor.extract() 

198 

199 # ------------------------------------------------------------------------ 

200 if self.pex_mode.need_resistance(): 

201 rex = ResistorExtraction(b=self.delaunay_b, amax=self.delaunay_amax) 

202 

203 c: kdb.Circuit = netlist.top_circuit() 

204 info(f"LVSDB: found {c.pin_count()}pins") 

205 

206 result_network = MultiLayerResistanceNetwork( 

207 resistor_networks_by_layer={}, 

208 via_resistors=[] 

209 ) 

210 

211 devices_by_name = self.pex_context.devices_by_name 

212 report.output_devices(devices_by_name) 

213 

214 node_count_by_net: Dict[str, int] = defaultdict(int) 

215 

216 for layer_name, region in layer_regions_by_name.items(): 

217 if layer_name == self.tech_info.internal_substrate_layer_name: 

218 continue 

219 

220 layer_sheet_resistance = self.tech_info.layer_resistance_by_layer_name.get(layer_name, None) 

221 if layer_sheet_resistance is None: 

222 continue 

223 

224 gds_pair = self.gds_pair(layer_name) 

225 pins = self.pex_context.pins_of_layer(gds_pair) 

226 labels = self.pex_context.labels_of_layer(gds_pair) 

227 

228 nodes = kdb.Region() 

229 nodes.enable_properties() 

230 

231 pin_labels: kdb.Texts = labels & pins 

232 for l in pin_labels: 

233 l: kdb.Text 

234 # NOTE: because we want more like a point as a junction 

235 # and folx create huge pins (covering the whole metal) 

236 # we create our own "mini squares" 

237 # (ResistorExtractor will subtract the pins from the metal polygons, 

238 # so in the extreme case the polygons could become empty) 

239 pin_point = l.bbox().enlarge(5) 

240 nodes.insert(pin_point) 

241 

242 report.output_pin(layer_name=layer_name, 

243 pin_point=pin_point, 

244 label=l) 

245 

246 def create_nodes_for_region(region: kdb.Region): 

247 for p in region: 

248 p: kdb.PolygonWithProperties 

249 cp: kdb.Point = p.bbox().center() 

250 b = kdb.Box(w=6, h=6) 

251 b.move(cp.x - b.width() / 2, 

252 cp.y - b.height() / 2) 

253 bwp = kdb.BoxWithProperties(b, p.properties()) 

254 

255 net = bwp.property('net') 

256 if net is None or net == '': 

257 error(f"Could not find net for via at {cp}") 

258 else: 

259 label_text = f"{net}.n{node_count_by_net[net]}" 

260 node_count_by_net[net] += 1 

261 label = kdb.Text(label_text, cp.x, cp.y) 

262 labels.insert(label) 

263 

264 nodes.insert(bwp) 

265 

266 # create additional nodes for vias 

267 via_above = via_name_above_layer_name.get(layer_name, None) 

268 if via_above is not None: 

269 create_nodes_for_region(via_regions_by_via_name[via_above]) 

270 via_below = via_name_below_layer_name.get(layer_name, None) 

271 if via_below is not None: 

272 create_nodes_for_region(via_regions_by_via_name[via_below]) 

273 

274 extracted_resistor_networks = rex.extract(polygons=region, pins=nodes, labels=labels) 

275 resistor_networks = ResistorNetworks( 

276 layer_name=layer_name, 

277 layer_sheet_resistance=layer_sheet_resistance.resistance, 

278 networks=extracted_resistor_networks 

279 ) 

280 

281 result_network.resistor_networks_by_layer[layer_name] = resistor_networks 

282 

283 subproc(f"Layer {layer_name} (R_coeff = {layer_sheet_resistance.resistance}):") 

284 for rn in resistor_networks.networks: 

285 # print(rn.to_string(True)) 

286 if not rn.node_to_s: 

287 continue 

288 

289 subproc("\tNodes:") 

290 for node_id in rn.node_to_s.keys(): 

291 loc = rn.locations[node_id] 

292 node_name = rn.node_names[node_id] 

293 subproc(f"\t\tNode #{node_id} {node_name} at {loc} ({loc.x * dbu} µm, {loc.y * dbu} µm)") 

294 

295 subproc("\tResistors:") 

296 visited_resistors: Set[Conductance] = set() 

297 for node_id, resistors in rn.node_to_s.items(): 

298 node_name = rn.node_names[node_id] 

299 for conductance, other_node_id in resistors: 

300 if conductance in visited_resistors: 

301 continue # we don't want to add it twice, only once per direction! 

302 visited_resistors.add(conductance) 

303 

304 other_node_name = rn.node_names[other_node_id] 

305 ohm = layer_sheet_resistance.resistance / 1000.0 / conductance.cond 

306 # TODO: layer_sheet_resistance.corner_adjustment_fraction not yet used !!! 

307 subproc(f"\t\t{node_name} ↔︎ {other_node_name}: {round(ohm, 3)} Ω (internally: {conductance.cond})") 

308 

309 # "Stitch" in the VIAs into the graph 

310 for layer_idx_bottom, layer_name_bottom in enumerate(all_layer_names): 

311 if layer_name_bottom == self.tech_info.internal_substrate_layer_name: 

312 continue 

313 if (layer_idx_bottom + 1) == len(all_layer_names): 

314 break 

315 

316 via = self.tech_info.contact_above_metal_layer_name.get(layer_name_bottom, None) 

317 if via is None: 

318 continue 

319 

320 via_gds_pair = self.gds_pair(via.name) 

321 canonical_via_name = self.tech_info.canonical_layer_name_by_gds_pair[via_gds_pair] 

322 

323 via_region = via_regions_by_via_name.get(canonical_via_name) 

324 if via_region is None: 

325 continue 

326 

327 # NOTE: poly layer stands for poly/nsdm/psdm, this will be in contacts, not in vias 

328 via_resistance = self.tech_info.via_resistance_by_layer_name.get(canonical_via_name, None) 

329 r_coeff: Optional[float] = None 

330 if via_resistance is None: 

331 r_coeff = self.tech_info.contact_resistance_by_layer_name[layer_name_bottom].resistance 

332 else: 

333 r_coeff = via_resistance.resistance 

334 

335 layer_name_top = all_layer_names[layer_idx_bottom + 1] 

336 

337 networks_bottom = result_network.resistor_networks_by_layer[layer_name_bottom] 

338 networks_top = result_network.resistor_networks_by_layer[layer_name_top] 

339 

340 for via_polygon in via_region: 

341 net_name = via_polygon.property('net') 

342 matches_bottom = networks_bottom.find_network_nodes(location=via_polygon) 

343 

344 device_terminal: Optional[Tuple[DeviceTerminal, float]] = None 

345 

346 if len(matches_bottom) == 0: 

347 ignored_device_layers: Set[str] = set() 

348 

349 def find_device_terminal(via_region: kdb.Region) -> Optional[Tuple[DeviceTerminal, float]]: 

350 for d in devices_by_name.values(): 

351 for dt in d.terminals.terminals: 

352 for ln, r in dt.regions_by_layer_name.items(): 

353 res = self.tech_info.contact_resistance_by_layer_name.get(ln, None) 

354 if res is None: 

355 ignored_device_layers.add(ln) 

356 continue 

357 elif r.overlapping(via_region): 

358 return (DeviceTerminal(device=d, device_terminal=dt), res) 

359 return None 

360 

361 if layer_name_bottom in self.tech_info.contact_resistance_by_layer_name.keys(): 

362 device_terminal = find_device_terminal(via_region=kdb.Region(via_polygon)) 

363 if device_terminal is None: 

364 warning(f"Couldn't find bottom network node (on {layer_name_bottom}) " 

365 f"for location {via_polygon}, " 

366 f"but could not find a device terminal either " 

367 f"(ignored layers: {ignored_device_layers})") 

368 else: 

369 r_coeff = device_terminal[1].resistance 

370 

371 matches_top = networks_top.find_network_nodes(location=via_polygon) 

372 if len(matches_top) == 0: 

373 error(f"Could not find top network nodes for location {via_polygon}") 

374 

375 # given a drawn via area, we calculate the actual via matrix 

376 approx_width = math.sqrt(via_polygon.area()) * dbu 

377 n_xy = 1 + math.floor((approx_width - (via.width + 2 * via.border)) / (via.width + via.spacing)) 

378 if n_xy < 1: 

379 n_xy = 1 

380 r_via_ohm = r_coeff / n_xy**2 / 1000.0 # mΩ -> Ω 

381 

382 info(f"via ({canonical_via_name}) found between " 

383 f"metals {layer_name_bottom}{layer_name_top} at {via_polygon}, " 

384 f"{n_xy}x{n_xy} (w={via.width}, sp={via.spacing}, border={via.border}), " 

385 f"{r_via_ohm} Ω") 

386 

387 report.output_via(via_name=canonical_via_name, 

388 bottom_layer=layer_name_bottom, 

389 top_layer=layer_name_top, 

390 net=net_name, 

391 via_width=via.width, 

392 via_spacing=via.spacing, 

393 via_border=via.border, 

394 polygon=via_polygon, 

395 ohm=r_via_ohm, 

396 comment=f"({len(matches_bottom)} bottom, {len(matches_top)} top)") 

397 

398 match_top = matches_top[0] if len(matches_top) >= 1 else (None, -1) 

399 

400 bottom: ViaJunction | DeviceTerminal 

401 if device_terminal is None: 

402 match_bottom = matches_bottom[0] if len(matches_bottom) >= 1 else (None, -1) 

403 bottom = ViaJunction(layer_name=layer_name_bottom, 

404 network=match_bottom[0], 

405 node_id=match_bottom[1]) 

406 else: 

407 bottom = device_terminal[0] 

408 

409 via_resistor = ViaResistor( 

410 bottom=bottom, 

411 top=ViaJunction(layer_name=layer_name_top, 

412 network=match_top[0], 

413 node_id=match_top[1]), 

414 resistance=r_via_ohm 

415 ) 

416 result_network.via_resistors.append(via_resistor) 

417 

418 # import rich.pretty 

419 # rich.pretty.pprint(result_network) 

420 results.resistor_network = result_network 

421 

422 return results