Coverage for klayout_pex/tech_info.py: 88%

253 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-17 19:08 +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/iic-jku/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 collections import Counter 

29from functools import cached_property 

30import google.protobuf.json_format 

31 

32from .util.multiple_choice import MultipleChoicePattern 

33from .log import ( 

34 warning 

35) 

36 

37import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

38import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as process_stack_pb2 

39import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2 

40 

41class TechDefError(Exception): 

42 """A defect in the technology definition itself, not in a layout.""" 

43 

44 

45class TechInfo: 

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

47 

48 LVSLayerName = str 

49 CanonicalLayerName = str 

50 GDSPair = Tuple[int, int] 

51 

52 @staticmethod 

53 def duplicate_names(tech: tech_pb2.Technology) -> List[str]: 

54 """ 

55 Report every name one of the technology's namespaces declares twice. 

56 

57 Each of them is looked up by name, and the process stack's is also the 

58 PEX25D profile namespace, so a repeat adds nothing: it replaces or 

59 drops whatever it collides with. sky130A declaring 'capild' once per 

60 MiM-cap variant cost the second film, and left the dielectric above it 

61 wrapping the metal three levels down. 

62 """ 

63 namespaces: Dict[str, List[Tuple[str, str]]] = { 

64 'process stack': [], 

65 'layer': [], 

66 'LVS computed layer': [], 

67 } 

68 

69 for lyr in tech.process_stack.layers: 

70 namespaces['process stack'].append((lyr.name, 'layer')) 

71 parameters = lyr.WhichOneof('parameters') 

72 # Only some layer kinds can carry a contact, and an unset one has 

73 # no name, so the name is the test rather than the layer type. 

74 contact = getattr(getattr(lyr, parameters), 'contact_above', None) \ 

75 if parameters else None 

76 if contact is not None and contact.name: 

77 namespaces['process stack'].append((contact.name, 'contact')) 

78 

79 namespaces['layer'] += [(lyr.name, 'layer') for lyr in tech.layers] 

80 namespaces['LVS computed layer'] += [(lyr.layer_info.name, 'layer') 

81 for lyr in tech.lvs_computed_layers] 

82 

83 problems: List[str] = [] 

84 for namespace, declarations in namespaces.items(): 

85 counts = Counter(name for name, _ in declarations) 

86 for name, count in sorted(counts.items()): 

87 if count == 1: 

88 continue 

89 kinds = sorted({kind for n, kind in declarations if n == name}) 

90 problems.append( 

91 f"the {namespace} namespace declares '{name}' {count} times " 

92 f"(as {', '.join(kinds)}), so all but the first declaration " 

93 f"are dropped") 

94 return problems 

95 

96 @staticmethod 

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

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

99 contents = f.read() 

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

101 

102 # Checked here rather than where a name is used: by then the collision 

103 # has already happened, and what it cost is no longer visible. 

104 problems = TechInfo.duplicate_names(tech) 

105 if problems: 

106 raise TechDefError( 

107 f"Names have to be unique, but in {jsonpb_path}" 

108 + ''.join(f"\n - {p}" for p in problems)) 

109 return tech 

110 

111 @classmethod 

112 def from_json(cls, 

113 jsonpb_path: str, 

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

115 tech = cls.parse_tech_def(jsonpb_path=jsonpb_path) 

116 return TechInfo(tech=tech, 

117 dielectric_filter=dielectric_filter) 

118 

119 def __init__(self, 

120 tech: tech_pb2.Technology, 

121 dielectric_filter: Optional[MultipleChoicePattern]): 

122 self.tech = tech 

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

124 

125 @cached_property 

126 def gds_pair_for_computed_layer_name(self) -> Dict[LVSLayerName, GDSPair]: 

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

128 for lyr in self.tech.lvs_computed_layers} 

129 

130 @cached_property 

131 def computed_layer_info_by_name(self) -> Dict[LVSLayerName, tech_pb2.ComputedLayerInfo]: 

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

133 

134 @cached_property 

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

136 return { 

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

138 for lyr in self.tech.lvs_computed_layers 

139 } 

140 

141 @cached_property 

142 def canonical_layer_name_by_gds_pair(self) -> Dict[GDSPair, CanonicalLayerName]: 

143 return { 

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

145 for lyr in self.tech.lvs_computed_layers 

146 } 

147 

148 @cached_property 

149 def layer_info_by_name(self) -> Dict[CanonicalLayerName, tech_pb2.LayerInfo]: 

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

151 

152 @cached_property 

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

154 return { 

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

156 for m in self.tech.pin_layer_mappings 

157 } 

158 

159 @cached_property 

160 def gds_pair_for_layer_name(self) -> Dict[CanonicalLayerName, GDSPair]: 

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

162 

163 @cached_property 

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

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

166 

167 @cached_property 

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

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

170 

171 @cached_property 

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

173 return { 

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

175 for lyr in self.tech.process_stack.layers 

176 } 

177 

178 @cached_property 

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

180 return list( 

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

182 self.tech.process_stack.layers) 

183 )[0] 

184 

185 @cached_property 

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

187 return list( 

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

189 self.tech.process_stack.layers) 

190 ) 

191 

192 @cached_property 

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

194 return self.process_metal_layers[0] 

195 

196 @cached_property 

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

198 return list( 

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

200 self.tech.process_stack.layers) 

201 )[0] 

202 

203 @cached_property 

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

205 return list( 

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

207 self.tech.process_stack.layers) 

208 ) 

209 

210 @cached_property 

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

212 layers = [] 

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

214 match pl.layer_type: 

215 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC | \ 

216 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

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

218 layers.append(pl) 

219 return layers 

220 

221 @cached_property 

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

223 diel_by_name = {} 

224 for pl in self.filtered_dielectric_layers: 

225 match pl.layer_type: 

226 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC: 

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

228 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

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

230 return diel_by_name 

231 

232 def conformal_dielectric_wrapping(self, layer_name: str) \ 

233 -> Optional[process_stack_pb2.ProcessStackInfo.LayerInfo]: 

234 """ 

235 The conformal dielectric anchored on ``layer_name``, if any. 

236 

237 Films form a chain: a metal is wrapped by a film, which may itself be 

238 wrapped by the next one. Calling this repeatedly walks that chain. 

239 """ 

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

241 for lyr in self.filtered_dielectric_layers: 

242 match lyr.layer_type: 

243 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

244 if lyr.conformal_dielectric_layer.reference == layer_name: 

245 found_layers.append(lyr) 

246 case _: 

247 continue 

248 

249 if len(found_layers) == 0: 

250 return None 

251 if len(found_layers) >= 2: 

252 raise Exception(f"found multiple conformal dielectric layers wrapping {layer_name}") 

253 return found_layers[0] 

254 

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

256 """ 

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

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

259 """ 

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

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

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

263 if lyr.name == layer_name: 

264 found_layer = lyr 

265 elif found_layer: 

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

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

268 return None, 0.0 

269 diel_lyr = lyr 

270 # search for next metal or end of stack 

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

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

273 return diel_lyr, 5.0 # air TODO 

274 

275 @cached_property 

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

277 d = {} 

278 for lyr in self.process_metal_layers: 

279 contact = lyr.metal_layer.contact_above 

280 via_gds_pair = self.gds_pair(contact) 

281 canonical_via_name = self.canonical_layer_name_by_gds_pair[via_gds_pair] 

282 d[lyr.name] = canonical_via_name 

283 return d 

284 

285 @cached_property 

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

287 d = {} 

288 LT = process_stack_pb2.ProcessStackInfo.LayerType 

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

290 match lyr.layer_type: 

291 case LT.LAYER_TYPE_NWELL: 

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

293 

294 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm 

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

296 return d 

297 

298 @cached_property 

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

300 d = {} 

301 LT = process_stack_pb2.ProcessStackInfo.LayerType 

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

303 match lyr.layer_type: 

304 case LT.LAYER_TYPE_NWELL: 

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

306 

307 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm 

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

309 

310 case LT.LAYER_TYPE_METAL: 

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

312 return d 

313 

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

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

316 if not gds_pair: 

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

318 if not gds_pair: 

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

320 return None 

321 return gds_pair 

322 

323 @cached_property 

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

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

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

327 # therefore the canonical name must not be used, 

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

329 # 

330 # the metal layers however are canonical! 

331 

332 d = {} 

333 for metal_layer in self.process_metal_layers: 

334 layer_name = metal_layer.name 

335 gds_pair = self.gds_pair(layer_name) 

336 

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

338 contact = metal_layer.metal_layer.contact_above 

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

340 

341 return d 

342 #-------------------------------- 

343 

344 @cached_property 

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

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

347 

348 @cached_property 

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

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

351 

352 @cached_property 

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

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

355 

356 @staticmethod 

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

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

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

360 return milliohm / 1000.0 

361 

362 @staticmethod 

363 def milliohm_by_cnt_to_ohm_by_square_for_contact( 

364 contact: process_stack_pb2.ProcessStackInfo.Contact, 

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

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

367 # 

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

369 return ohm_by_square 

370 

371 @staticmethod 

372 def milliohm_by_cnt_to_ohm_by_square_for_via( 

373 contact: process_stack_pb2.ProcessStackInfo.Contact, 

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

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

376 return ohm_by_square 

377 

378 #-------------------------------- 

379 

380 @cached_property 

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

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

383 

384 @cached_property 

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

386 """ 

387 usage: dict[top_layer_name][bottom_layer_name] 

388 """ 

389 

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

391 -> process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance: 

392 oc = process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance() 

393 oc.top_layer_name = sc.layer_name 

394 oc.bottom_layer_name = self.internal_substrate_layer_name 

395 oc.capacitance = sc.area_capacitance 

396 return oc 

397 

398 d = { 

399 ln: { 

400 self.internal_substrate_layer_name: convert_substrate_to_overlap_cap(sc) 

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

402 } 

403 

404 d2 = { 

405 oc.top_layer_name: { 

406 oc_bot.bottom_layer_name: oc_bot 

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

408 } 

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

410 } 

411 

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

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

414 if k1 not in d: 

415 d[k1] = {k2: v} 

416 else: 

417 d[k1][k2] = v 

418 return d 

419 

420 @cached_property 

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

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

423 

424 @property 

425 def internal_substrate_layer_name(self) -> str: 

426 return 'VSUBS' 

427 

428 @cached_property 

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

430 """ 

431 usage: dict[in_layer_name][out_layer_name] 

432 """ 

433 

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

435 -> process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance: 

436 soc = process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance() 

437 soc.in_layer_name = sc.layer_name 

438 soc.out_layer_name = self.internal_substrate_layer_name 

439 soc.capacitance = sc.perimeter_capacitance 

440 return soc 

441 

442 d = { 

443 ln: { 

444 self.internal_substrate_layer_name: convert_substrate_to_side_overlap_cap(sc) 

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

446 } 

447 

448 d2 = { 

449 oc.in_layer_name: { 

450 oc_bot.out_layer_name: oc_bot 

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

452 } 

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

454 } 

455 

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

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

458 d[k1][k2] = v 

459 

460 return d 

461