Coverage for klayout_pex/tech_info.py: 40%

231 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-08 18:54 +0000

1#! /usr/bin/env python3 

2# 

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

4# SPDX-FileCopyrightText: 2024-2025 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# 

25 

26from __future__ import annotations # allow class type hints within same class 

27from typing import * 

28from functools import cached_property 

29import google.protobuf.json_format 

30 

31from .util.multiple_choice import MultipleChoicePattern 

32from .log import ( 

33 warning 

34) 

35 

36import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

37import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as process_stack_pb2 

38import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2 

39 

40class TechInfo: 

41 """Helper class for Protocol Buffer tech_pb2.Technology""" 

42 

43 GDSPair = Tuple[int, int] 

44 

45 @staticmethod 

46 def parse_tech_def(jsonpb_path: str) -> tech_pb2.Technology: 

47 with open(jsonpb_path, 'r') as f: 

48 contents = f.read() 

49 tech = google.protobuf.json_format.Parse(contents, tech_pb2.Technology()) 

50 return tech 

51 

52 @classmethod 

53 def from_json(cls, 

54 jsonpb_path: str, 

55 dielectric_filter: Optional[MultipleChoicePattern]) -> TechInfo: 

56 tech = cls.parse_tech_def(jsonpb_path=jsonpb_path) 

57 return TechInfo(tech=tech, 

58 dielectric_filter=dielectric_filter) 

59 

60 def __init__(self, 

61 tech: tech_pb2.Technology, 

62 dielectric_filter: Optional[MultipleChoicePattern]): 

63 self.tech = tech 

64 self.dielectric_filter = dielectric_filter or MultipleChoicePattern(pattern='all') 

65 

66 @cached_property 

67 def gds_pair_for_computed_layer_name(self) -> Dict[str, GDSPair]: 

68 return {lyr.layer_info.name: (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype) 

69 for lyr in self.tech.lvs_computed_layers} 

70 

71 @cached_property 

72 def computed_layer_info_by_name(self) -> Dict[str, tech_pb2.ComputedLayerInfo]: 

73 return {lyr.layer_info.name: lyr for lyr in self.tech.lvs_computed_layers} 

74 

75 @cached_property 

76 def computed_layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.ComputedLayerInfo]: 

77 return { 

78 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr 

79 for lyr in self.tech.lvs_computed_layers 

80 } 

81 

82 @cached_property 

83 def canonical_layer_name_by_gds_pair(self) -> Dict[GDSPair, str]: 

84 return { 

85 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr.original_layer_name 

86 for lyr in self.tech.lvs_computed_layers 

87 } 

88 

89 @cached_property 

90 def layer_info_by_name(self) -> Dict[str, tech_pb2.LayerInfo]: 

91 return {lyr.name: lyr for lyr in self.tech.layers} 

92 

93 @cached_property 

94 def pin_layer_mapping_for_drw_gds_pair(self) -> Dict[GDSPair, tech_pb2.PinLayerMapping]: 

95 return { 

96 (m.drw_gds_layer, m.drw_gds_datatype): (m.pin_gds_layer, m.pin_gds_datatype) 

97 for m in self.tech.pin_layer_mappings 

98 } 

99 

100 @cached_property 

101 def gds_pair_for_layer_name(self) -> Dict[str, GDSPair]: 

102 return {lyr.name: (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype) for lyr in self.tech.layers} 

103 

104 @cached_property 

105 def layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.LayerInfo]: 

106 return {(lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): lyr for lyr in self.tech.layers} 

107 

108 @cached_property 

109 def process_stack_layer_by_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.LayerInfo]: 

110 return {lyr.name: lyr for lyr in self.tech.process_stack.layers} 

111 

112 @cached_property 

113 def process_stack_layer_by_gds_pair(self) -> Dict[GDSPair, process_stack_pb2.ProcessStackInfo.LayerInfo]: 

114 return { 

115 (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): self.process_stack_layer_by_name[lyr.name] 

116 for lyr in self.tech.process_stack.layers 

117 } 

118 

119 @cached_property 

120 def process_substrate_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo: 

121 return list( 

122 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SUBSTRATE, 

123 self.tech.process_stack.layers) 

124 )[0] 

125 

126 @cached_property 

127 def process_diffusion_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]: 

128 return list( 

129 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_DIFFUSION, 

130 self.tech.process_stack.layers) 

131 ) 

132 

133 @cached_property 

134 def gate_poly_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo: 

135 return self.process_metal_layers[0] 

136 

137 @cached_property 

138 def field_oxide_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo: 

139 return list( 

140 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_FIELD_OXIDE, 

141 self.tech.process_stack.layers) 

142 )[0] 

143 

144 @cached_property 

145 def process_metal_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]: 

146 return list( 

147 filter(lambda lyr: lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL, 

148 self.tech.process_stack.layers) 

149 ) 

150 

151 @cached_property 

152 def filtered_dielectric_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]: 

153 layers = [] 

154 for pl in self.tech.process_stack.layers: 

155 match pl.layer_type: 

156 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC | \ 

157 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC | \ 

158 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC: 

159 if self.dielectric_filter.is_included(pl.name): 

160 layers.append(pl) 

161 return layers 

162 

163 @cached_property 

164 def dielectric_by_name(self) -> Dict[str, float]: 

165 diel_by_name = {} 

166 for pl in self.filtered_dielectric_layers: 

167 match pl.layer_type: 

168 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC: 

169 diel_by_name[pl.name] = pl.simple_dielectric_layer.dielectric_k 

170 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

171 diel_by_name[pl.name] = pl.conformal_dielectric_layer.dielectric_k 

172 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC: 

173 diel_by_name[pl.name] = pl.sidewall_dielectric_layer.dielectric_k 

174 return diel_by_name 

175 

176 def sidewall_dielectric_layer(self, layer_name: str) -> Optional[process_stack_pb2.ProcessStackInfo.LayerInfo]: 

177 found_layers: List[process_stack_pb2.ProcessStackInfo.LayerInfo] = [] 

178 for lyr in self.filtered_dielectric_layers: 

179 match lyr.layer_type: 

180 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC: 

181 if lyr.sidewall_dielectric_layer.reference == layer_name: 

182 found_layers.append(lyr) 

183 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

184 if lyr.conformal_dielectric_layer.reference == layer_name: 

185 found_layers.append(lyr) 

186 case _: 

187 continue 

188 

189 if len(found_layers) == 0: 

190 return None 

191 if len(found_layers) >= 2: 

192 raise Exception(f"found multiple sidewall dielectric layers for {layer_name}") 

193 return found_layers[0] 

194 

195 def simple_dielectric_above_metal(self, layer_name: str) -> Tuple[Optional[process_stack_pb2.ProcessStackInfo.LayerInfo], float]: 

196 """ 

197 Returns a tuple of the dielectric layer and it's (maximum) height. 

198 Maximum would be the case where no metal and other dielectrics are present. 

199 """ 

200 found_layer: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None 

201 diel_lyr: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None 

202 for lyr in self.tech.process_stack.layers: 

203 if lyr.name == layer_name: 

204 found_layer = lyr 

205 elif found_layer: 

206 if not diel_lyr and lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC: 

207 if not self.dielectric_filter.is_included(lyr.name): 

208 return None, 0.0 

209 diel_lyr = lyr 

210 # search for next metal or end of stack 

211 if lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL: 

212 return diel_lyr, lyr.metal_layer.z - found_layer.metal_layer.z 

213 return diel_lyr, 5.0 # air TODO 

214 

215 @cached_property 

216 def contact_above_metal_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]: 

217 d = {} 

218 for lyr in self.process_metal_layers: 

219 contact = lyr.metal_layer.contact_above 

220 via_gds_pair = self.gds_pair(contact) 

221 canonical_via_name = self.canonical_layer_name_by_gds_pair[via_gds_pair] 

222 d[lyr.name] = canonical_via_name 

223 return d 

224 

225 @cached_property 

226 def contact_by_device_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]: 

227 d = {} 

228 LT = process_stack_pb2.ProcessStackInfo.LayerType 

229 for lyr in self.tech.process_stack.layers: 

230 match lyr.layer_type: 

231 case LT.LAYER_TYPE_NWELL: 

232 d[lyr.name] = lyr.nwell_layer.contact_above 

233 

234 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm 

235 d[lyr.name] = lyr.diffusion_layer.contact_above 

236 return d 

237 

238 @cached_property 

239 def contact_by_contact_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]: 

240 d = {} 

241 LT = process_stack_pb2.ProcessStackInfo.LayerType 

242 for lyr in self.tech.process_stack.layers: 

243 match lyr.layer_type: 

244 case LT.LAYER_TYPE_NWELL: 

245 d[lyr.nwell_layer.contact_above.name] = lyr.nwell_layer.contact_above 

246 

247 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm 

248 d[lyr.diffusion_layer.contact_above.name] = lyr.diffusion_layer.contact_above 

249 

250 case LT.LAYER_TYPE_METAL: 

251 d[lyr.metal_layer.contact_above.name] = lyr.metal_layer.contact_above 

252 return d 

253 

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

255 gds_pair = self.gds_pair_for_computed_layer_name.get(layer_name, None) 

256 if not gds_pair: 

257 gds_pair = self.gds_pair_for_layer_name.get(layer_name, None) 

258 if not gds_pair: 

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

260 return None 

261 return gds_pair 

262 

263 @cached_property 

264 def bottom_and_top_layer_name_by_via_computed_layer_name(self) -> Dict[str, Tuple[str, str]]: 

265 # NOTE: vias under the same name can be used in multiple situations 

266 # e.g. in sky130A, via3 has two (bot, top) cases: {(met3, met4), (met3, cmim)}, 

267 # therefore the canonical name must not be used, 

268 # but really the LVS computed name, that is also used in the process stack 

269 # 

270 # the metal layers however are canonical! 

271 

272 d = {} 

273 for metal_layer in self.process_metal_layers: 

274 layer_name = metal_layer.name 

275 gds_pair = self.gds_pair(layer_name) 

276 

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

278 contact = metal_layer.metal_layer.contact_above 

279 d[contact.name] = (contact.layer_below, contact.metal_above) 

280 

281 return d 

282 #-------------------------------- 

283 

284 @cached_property 

285 def layer_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.LayerResistance]: 

286 return {r.layer_name: r for r in self.tech.process_parasitics.resistance.layers} 

287 

288 @cached_property 

289 def contact_resistance_by_device_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ContactResistance]: 

290 return {r.device_layer_name: r for r in self.tech.process_parasitics.resistance.contacts} 

291 

292 @cached_property 

293 def via_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ViaResistance]: 

294 return {r.via_name: r for r in self.tech.process_parasitics.resistance.vias} 

295 

296 @staticmethod 

297 def milliohm_to_ohm(milliohm: float) -> float: 

298 # NOTE: tech_pb2 has mΩ/µm^2 

299 # RExtractorTech.Conductor.resistance is in Ω/µm^2 

300 return milliohm / 1000.0 

301 

302 @staticmethod 

303 def milliohm_by_cnt_to_ohm_by_square_for_contact( 

304 contact: process_stack_pb2.ProcessStackInfo.Contact, 

305 contact_resistance: process_parasitics_pb2.ResistanceInfo.ContactResistance) -> float: 

306 # NOTE: ContactResistance ... mΩ/CNT 

307 # 

308 ohm_by_square = contact_resistance.resistance / 1000.0 * contact.width ** 2 

309 return ohm_by_square 

310 

311 @staticmethod 

312 def milliohm_by_cnt_to_ohm_by_square_for_via( 

313 contact: process_stack_pb2.ProcessStackInfo.Contact, 

314 via_resistance: process_parasitics_pb2.ResistanceInfo.ViaResistance) -> float: 

315 ohm_by_square = via_resistance.resistance / 1000.0 * contact.width ** 2 

316 return ohm_by_square 

317 

318 #-------------------------------- 

319 

320 @cached_property 

321 def substrate_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance]: 

322 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.substrates} 

323 

324 @cached_property 

325 def overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance]]: 

326 """ 

327 usage: dict[top_layer_name][bottom_layer_name] 

328 """ 

329 

330 def convert_substrate_to_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \ 

331 -> process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance: 

332 oc = process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance() 

333 oc.top_layer_name = sc.layer_name 

334 oc.bottom_layer_name = self.internal_substrate_layer_name 

335 oc.capacitance = sc.area_capacitance 

336 return oc 

337 

338 d = { 

339 ln: { 

340 self.internal_substrate_layer_name: convert_substrate_to_overlap_cap(sc) 

341 } for ln, sc in self.substrate_cap_by_layer_name.items() 

342 } 

343 

344 d2 = { 

345 oc.top_layer_name: { 

346 oc_bot.bottom_layer_name: oc_bot 

347 for oc_bot in self.tech.process_parasitics.capacitance.overlaps if oc_bot.top_layer_name == oc.top_layer_name 

348 } 

349 for oc in self.tech.process_parasitics.capacitance.overlaps 

350 } 

351 

352 for k1, ve in d2.items(): 

353 for k2, v in ve.items(): 

354 if k1 not in d: 

355 d[k1] = {k2: v} 

356 else: 

357 d[k1][k2] = v 

358 return d 

359 

360 @cached_property 

361 def sidewall_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SidewallCapacitance]: 

362 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.sidewalls} 

363 

364 @property 

365 def internal_substrate_layer_name(self) -> str: 

366 return 'VSUBS' 

367 

368 @cached_property 

369 def side_overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance]]: 

370 """ 

371 usage: dict[in_layer_name][out_layer_name] 

372 """ 

373 

374 def convert_substrate_to_side_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \ 

375 -> process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance: 

376 soc = process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance() 

377 soc.in_layer_name = sc.layer_name 

378 soc.out_layer_name = self.internal_substrate_layer_name 

379 soc.capacitance = sc.perimeter_capacitance 

380 return soc 

381 

382 d = { 

383 ln: { 

384 self.internal_substrate_layer_name: convert_substrate_to_side_overlap_cap(sc) 

385 } for ln, sc in self.substrate_cap_by_layer_name.items() 

386 } 

387 

388 d2 = { 

389 oc.in_layer_name: { 

390 oc_bot.out_layer_name: oc_bot 

391 for oc_bot in self.tech.process_parasitics.capacitance.sideoverlaps if oc_bot.in_layer_name == oc.in_layer_name 

392 } 

393 for oc in self.tech.process_parasitics.capacitance.sideoverlaps 

394 } 

395 

396 for k1, ve in d2.items(): 

397 for k2, v in ve.items(): 

398 d[k1][k2] = v 

399 

400 return d 

401