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

219 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-03-31 19:36 +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 RCExtractor: 

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 self.extract_cell(results=cell_extraction_results, 

104 report=extraction_report) 

105 extraction_results.cell_extraction_results[cell_name] = cell_extraction_results 

106 

107 extraction_report.save(self.report_path) 

108 

109 return extraction_results 

110 

111 def extract_cell(self, 

112 results: CellExtractionResults, 

113 report: ExtractionReporter): 

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

115 dbu = self.pex_context.dbu 

116 # ------------------------------------------------------------------------ 

117 

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

119 

120 all_region = kdb.Region() 

121 all_region.enable_properties() 

122 

123 substrate_region = kdb.Region() 

124 substrate_region.enable_properties() 

125 

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

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

128 

129 layer_regions_by_name[self.tech_info.internal_substrate_layer_name] = substrate_region 

130 

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

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

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

134 

135 previous_via_name: Optional[str] = None 

136 

137 for metal_layer in self.tech_info.process_metal_layers: 

138 layer_name = metal_layer.name 

139 gds_pair = self.gds_pair(layer_name) 

140 canonical_layer_name = self.tech_info.canonical_layer_name_by_gds_pair[gds_pair] 

141 

142 all_layer_shapes = self.shapes_of_layer(layer_name) 

143 if all_layer_shapes is not None: 

144 all_layer_shapes.enable_properties() 

145 

146 layer_regions_by_name[canonical_layer_name] += all_layer_shapes 

147 layer_regions_by_name[canonical_layer_name].enable_properties() 

148 all_region += all_layer_shapes 

149 

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

151 contact = metal_layer.metal_layer.contact_above 

152 

153 via_regions = self.shapes_of_layer(contact.name) 

154 if via_regions is not None: 

155 via_regions.enable_properties() 

156 via_regions_by_via_name[contact.name] += via_regions 

157 via_name_above_layer_name[canonical_layer_name] = contact.name 

158 via_name_below_layer_name[canonical_layer_name] = previous_via_name 

159 

160 previous_via_name = contact.name 

161 else: 

162 previous_via_name = None 

163 

164 all_layer_names = list(layer_regions_by_name.keys()) 

165 

166 # ------------------------------------------------------------------------ 

167 if self.pex_mode.need_capacitance(): 

168 overlap_extractor = OverlapExtractor( 

169 all_layer_names=all_layer_names, 

170 layer_regions_by_name=layer_regions_by_name, 

171 dbu=dbu, 

172 tech_info=self.tech_info, 

173 results=results, 

174 report=report 

175 ) 

176 overlap_extractor.extract() 

177 

178 sidewall_and_fringe_extractor = SidewallAndFringeExtractor( 

179 all_layer_names=all_layer_names, 

180 layer_regions_by_name=layer_regions_by_name, 

181 dbu=dbu, 

182 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo, 

183 tech_info=self.tech_info, 

184 results=results, 

185 report=report 

186 ) 

187 sidewall_and_fringe_extractor.extract() 

188 

189 # ------------------------------------------------------------------------ 

190 if self.pex_mode.need_resistance(): 

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

192 

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

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

195 

196 result_network = MultiLayerResistanceNetwork( 

197 resistor_networks_by_layer={}, 

198 via_resistors=[] 

199 ) 

200 

201 devices_by_name = self.pex_context.devices_by_name 

202 report.output_devices(devices_by_name) 

203 

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

205 

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

207 if layer_name == self.tech_info.internal_substrate_layer_name: 

208 continue 

209 

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

211 if layer_sheet_resistance is None: 

212 continue 

213 

214 gds_pair = self.gds_pair(layer_name) 

215 pins = self.pex_context.pins_of_layer(gds_pair) 

216 labels = self.pex_context.labels_of_layer(gds_pair) 

217 

218 nodes = kdb.Region() 

219 nodes.enable_properties() 

220 

221 pin_labels: kdb.Texts = labels & pins 

222 for l in pin_labels: 

223 l: kdb.Text 

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

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

226 # we create our own "mini squares" 

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

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

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

230 nodes.insert(pin_point) 

231 

232 report.output_pin(layer_name=layer_name, 

233 pin_point=pin_point, 

234 label=l) 

235 

236 def create_nodes_for_region(region: kdb.Region): 

237 for p in region: 

238 p: kdb.PolygonWithProperties 

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

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

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

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

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

244 

245 net = bwp.property('net') 

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

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

248 else: 

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

250 node_count_by_net[net] += 1 

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

252 labels.insert(label) 

253 

254 nodes.insert(bwp) 

255 

256 # create additional nodes for vias 

257 via_above = via_name_above_layer_name.get(layer_name, None) 

258 if via_above is not None: 

259 create_nodes_for_region(via_regions_by_via_name[via_above]) 

260 via_below = via_name_below_layer_name.get(layer_name, None) 

261 if via_below is not None: 

262 create_nodes_for_region(via_regions_by_via_name[via_below]) 

263 

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

265 resistor_networks = ResistorNetworks( 

266 layer_name=layer_name, 

267 layer_sheet_resistance=layer_sheet_resistance.resistance, 

268 networks=extracted_resistor_networks 

269 ) 

270 

271 result_network.resistor_networks_by_layer[layer_name] = resistor_networks 

272 

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

274 for rn in resistor_networks.networks: 

275 # print(rn.to_string(True)) 

276 if not rn.node_to_s: 

277 continue 

278 

279 subproc("\tNodes:") 

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

281 loc = rn.locations[node_id] 

282 node_name = rn.node_names[node_id] 

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

284 

285 subproc("\tResistors:") 

286 visited_resistors: Set[Conductance] = set() 

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

288 node_name = rn.node_names[node_id] 

289 for conductance, other_node_id in resistors: 

290 if conductance in visited_resistors: 

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

292 visited_resistors.add(conductance) 

293 

294 other_node_name = rn.node_names[other_node_id] 

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

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

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

298 

299 # "Stitch" in the VIAs into the graph 

300 for layer_idx_bottom, layer_name_bottom in enumerate(all_layer_names): 

301 if layer_name_bottom == self.tech_info.internal_substrate_layer_name: 

302 continue 

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

304 break 

305 

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

307 if via is None: 

308 continue 

309 

310 via_gds_pair = self.gds_pair(via.name) 

311 canonical_via_name = self.tech_info.canonical_layer_name_by_gds_pair[via_gds_pair] 

312 

313 via_region = via_regions_by_via_name.get(canonical_via_name) 

314 if via_region is None: 

315 continue 

316 

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

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

319 r_coeff: Optional[float] = None 

320 if via_resistance is None: 

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

322 else: 

323 r_coeff = via_resistance.resistance 

324 

325 layer_name_top = all_layer_names[layer_idx_bottom + 1] 

326 

327 networks_bottom = result_network.resistor_networks_by_layer[layer_name_bottom] 

328 networks_top = result_network.resistor_networks_by_layer[layer_name_top] 

329 

330 for via_polygon in via_region: 

331 net_name = via_polygon.property('net') 

332 matches_bottom = networks_bottom.find_network_nodes(location=via_polygon) 

333 

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

335 

336 if len(matches_bottom) == 0: 

337 ignored_device_layers: Set[str] = set() 

338 

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

340 for d in devices_by_name.values(): 

341 for dt in d.terminals.terminals: 

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

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

344 if res is None: 

345 ignored_device_layers.add(ln) 

346 continue 

347 elif r.overlapping(via_region): 

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

349 return None 

350 

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

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

353 if device_terminal is None: 

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

355 f"for location {via_polygon}, " 

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

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

358 else: 

359 r_coeff = device_terminal[1].resistance 

360 

361 matches_top = networks_top.find_network_nodes(location=via_polygon) 

362 if len(matches_top) == 0: 

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

364 

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

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

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

368 if n_xy < 1: 

369 n_xy = 1 

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

371 

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

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

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

375 f"{r_via_ohm} Ω") 

376 

377 report.output_via(via_name=canonical_via_name, 

378 bottom_layer=layer_name_bottom, 

379 top_layer=layer_name_top, 

380 net=net_name, 

381 via_width=via.width, 

382 via_spacing=via.spacing, 

383 via_border=via.border, 

384 polygon=via_polygon, 

385 ohm=r_via_ohm, 

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

387 

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

389 

390 bottom: ViaJunction | DeviceTerminal 

391 if device_terminal is None: 

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

393 bottom = ViaJunction(layer_name=layer_name_bottom, 

394 network=match_bottom[0], 

395 node_id=match_bottom[1]) 

396 else: 

397 bottom = device_terminal[0] 

398 

399 via_resistor = ViaResistor( 

400 bottom=bottom, 

401 top=ViaJunction(layer_name=layer_name_top, 

402 network=match_top[0], 

403 node_id=match_top[1]), 

404 resistance=r_via_ohm 

405 ) 

406 result_network.via_resistors.append(via_resistor) 

407 

408 # import rich.pretty 

409 # rich.pretty.pprint(result_network) 

410 results.resistor_network = result_network 

411 

412 return results