Coverage for klayout_pex/tech_info.py: 89%

278 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 23:32 +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 .device_models import DeviceModels 

33from .util.multiple_choice import MultipleChoicePattern 

34from .log import ( 

35 warning 

36) 

37 

38import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

39import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as process_stack_pb2 

40import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2 

41 

42class TechDefError(Exception): 

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

44 

45 

46class TechInfo: 

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

48 

49 LVSLayerName = str 

50 CanonicalLayerName = str 

51 GDSPair = Tuple[int, int] 

52 

53 @staticmethod 

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

55 """ 

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

57 

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

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

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

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

62 wrapping the metal three levels down. 

63 """ 

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

65 'process stack': [], 

66 'layer': [], 

67 'LVS computed layer': [], 

68 'device model mapping': [], 

69 } 

70 

71 for lyr in tech.process_stack.layers: 

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

73 parameters = lyr.WhichOneof('parameters') 

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

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

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

77 if parameters else None 

78 if contact is not None and contact.name: 

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

80 

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

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

83 for lyr in tech.lvs_computed_layers] 

84 namespaces['device model mapping'] += [(dm.lvs_device_class_name, 'LVS device class') 

85 for dm in tech.device_models.device_model_mappings] 

86 

87 problems: List[str] = [] 

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

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

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

91 if count == 1: 

92 continue 

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

94 problems.append( 

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

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

97 f"are dropped") 

98 return problems 

99 

100 @staticmethod 

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

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

103 contents = f.read() 

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

105 

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

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

108 problems = TechInfo.duplicate_names(tech) 

109 if problems: 

110 raise TechDefError( 

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

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

113 return tech 

114 

115 @classmethod 

116 def from_json(cls, 

117 jsonpb_path: str, 

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

119 tech = cls.parse_tech_def(jsonpb_path=jsonpb_path) 

120 return TechInfo(tech=tech, 

121 dielectric_filter=dielectric_filter) 

122 

123 def __init__(self, 

124 tech: tech_pb2.Technology, 

125 dielectric_filter: Optional[MultipleChoicePattern]): 

126 self.tech = tech 

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

128 

129 @cached_property 

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

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

132 for lyr in self.tech.lvs_computed_layers} 

133 

134 @cached_property 

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

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

137 

138 @cached_property 

139 def device_models(self) -> DeviceModels: 

140 return DeviceModels(self.tech.device_models) 

141 

142 @cached_property 

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

144 return { 

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

146 for lyr in self.tech.lvs_computed_layers 

147 } 

148 

149 @cached_property 

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

151 return { 

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

153 for lyr in self.tech.lvs_computed_layers 

154 } 

155 

156 @cached_property 

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

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

159 

160 @cached_property 

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

162 return { 

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

164 for m in self.tech.pin_layer_mappings 

165 } 

166 

167 @cached_property 

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

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

170 

171 @cached_property 

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

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

174 

175 @cached_property 

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

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

178 

179 @cached_property 

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

181 return { 

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

183 for lyr in self.tech.process_stack.layers 

184 } 

185 

186 @cached_property 

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

188 return list( 

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

190 self.tech.process_stack.layers) 

191 )[0] 

192 

193 @cached_property 

194 def process_conductor_gds_pairs(self) -> List[GDSPair]: 

195 """ 

196 The GDS pairs of the conductors of the process stack (its diffusion and metal layers), from the bottom up 

197 """ 

198 gds_pairs: List[TechInfo.GDSPair] = [] 

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

200 if lyr.layer_type not in (process_stack_pb2.ProcessStackInfo.LAYER_TYPE_DIFFUSION, 

201 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL): 

202 continue 

203 gds_pair = self.gds_pair_for_computed_layer_name.get(lyr.name, None) or \ 

204 self.gds_pair_for_layer_name.get(lyr.name, None) 

205 if gds_pair is not None and gds_pair not in gds_pairs: 

206 gds_pairs.append(gds_pair) 

207 return gds_pairs 

208 

209 @cached_property 

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

211 return list( 

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

213 self.tech.process_stack.layers) 

214 ) 

215 

216 @cached_property 

217 def process_diffusion_layer_names(self) -> List[CanonicalLayerName]: 

218 """ 

219 The canonical names of the diffusion layers, which the capacitances are looked up by 

220 (e.g. nsdm and psdm, of the LVS layers nsd and psd) 

221 """ 

222 gds_pairs = [self.gds_pair(lyr.name) for lyr in self.process_diffusion_layers] 

223 return [self.canonical_layer_name_by_gds_pair[gds_pair] for gds_pair in gds_pairs if gds_pair is not None] 

224 

225 @cached_property 

226 def device_capacitance_layer_names(self) -> Set[CanonicalLayerName]: 

227 """ 

228 The layers between which the diffusion has the capacitances of the devices (see is_device_capacitance) 

229 """ 

230 gate_poly_layer_name = self.canonical_layer_name_by_gds_pair.get(self.gds_pair(self.gate_poly_layer.name), None) 

231 return {self.internal_substrate_layer_name, gate_poly_layer_name, *self.process_diffusion_layer_names} 

232 

233 def is_device_capacitance(self, layer_name1: CanonicalLayerName, layer_name2: CanonicalLayerName) -> bool: 

234 """ 

235 Whether the capacitance between the layers is the one of a device, which its model has, 

236 rather than a parasitic one: between the diffusion and the substrate or another diffusion (the junctions, 

237 e.g. by the AS, AD, PS and PD of a transistor), or the gate poly (the overlap of the gate) 

238 """ 

239 return (layer_name1 in self.process_diffusion_layer_names or layer_name2 in self.process_diffusion_layer_names) \ 

240 and layer_name1 in self.device_capacitance_layer_names \ 

241 and layer_name2 in self.device_capacitance_layer_names 

242 

243 @cached_property 

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

245 return self.process_metal_layers[0] 

246 

247 @cached_property 

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

249 return list( 

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

251 self.tech.process_stack.layers) 

252 )[0] 

253 

254 @cached_property 

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

256 return list( 

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

258 self.tech.process_stack.layers) 

259 ) 

260 

261 @cached_property 

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

263 layers = [] 

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

265 match pl.layer_type: 

266 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC | \ 

267 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

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

269 layers.append(pl) 

270 return layers 

271 

272 @cached_property 

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

274 diel_by_name = {} 

275 for pl in self.filtered_dielectric_layers: 

276 match pl.layer_type: 

277 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC: 

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

279 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

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

281 return diel_by_name 

282 

283 def conformal_dielectric_wrapping(self, layer_name: str) \ 

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

285 """ 

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

287 

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

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

290 """ 

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

292 for lyr in self.filtered_dielectric_layers: 

293 match lyr.layer_type: 

294 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

295 if lyr.conformal_dielectric_layer.reference == layer_name: 

296 found_layers.append(lyr) 

297 case _: 

298 continue 

299 

300 if len(found_layers) == 0: 

301 return None 

302 if len(found_layers) >= 2: 

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

304 return found_layers[0] 

305 

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

307 """ 

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

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

310 """ 

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

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

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

314 if lyr.name == layer_name: 

315 found_layer = lyr 

316 elif found_layer: 

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

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

319 return None, 0.0 

320 diel_lyr = lyr 

321 # search for next metal or end of stack 

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

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

324 return diel_lyr, 5.0 # air TODO 

325 

326 @cached_property 

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

328 d = {} 

329 for lyr in self.process_metal_layers: 

330 contact = lyr.metal_layer.contact_above 

331 via_gds_pair = self.gds_pair(contact) 

332 canonical_via_name = self.canonical_layer_name_by_gds_pair[via_gds_pair] 

333 d[lyr.name] = canonical_via_name 

334 return d 

335 

336 @cached_property 

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

338 d = {} 

339 LT = process_stack_pb2.ProcessStackInfo.LayerType 

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

341 match lyr.layer_type: 

342 case LT.LAYER_TYPE_NWELL: 

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

344 

345 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm 

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

347 return d 

348 

349 @cached_property 

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

351 d = {} 

352 LT = process_stack_pb2.ProcessStackInfo.LayerType 

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

354 match lyr.layer_type: 

355 case LT.LAYER_TYPE_NWELL: 

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

357 

358 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm 

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

360 

361 case LT.LAYER_TYPE_METAL: 

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

363 return d 

364 

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

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

367 if not gds_pair: 

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

369 if not gds_pair: 

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

371 return None 

372 return gds_pair 

373 

374 @cached_property 

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

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

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

378 # therefore the canonical name must not be used, 

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

380 # 

381 # the metal layers however are canonical! 

382 

383 d = {} 

384 for metal_layer in self.process_metal_layers: 

385 layer_name = metal_layer.name 

386 gds_pair = self.gds_pair(layer_name) 

387 

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

389 contact = metal_layer.metal_layer.contact_above 

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

391 

392 return d 

393 #-------------------------------- 

394 

395 @cached_property 

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

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

398 

399 @cached_property 

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

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

402 

403 @cached_property 

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

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

406 

407 @staticmethod 

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

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

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

411 return milliohm / 1000.0 

412 

413 @staticmethod 

414 def milliohm_by_cnt_to_ohm_by_square_for_contact( 

415 contact: process_stack_pb2.ProcessStackInfo.Contact, 

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

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

418 # 

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

420 return ohm_by_square 

421 

422 @staticmethod 

423 def milliohm_by_cnt_to_ohm_by_square_for_via( 

424 contact: process_stack_pb2.ProcessStackInfo.Contact, 

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

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

427 return ohm_by_square 

428 

429 #-------------------------------- 

430 

431 @cached_property 

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

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

434 

435 @cached_property 

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

437 """ 

438 usage: dict[top_layer_name][bottom_layer_name] 

439 """ 

440 

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

442 -> process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance: 

443 oc = process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance() 

444 oc.top_layer_name = sc.layer_name 

445 oc.bottom_layer_name = self.internal_substrate_layer_name 

446 oc.capacitance = sc.area_capacitance 

447 return oc 

448 

449 d = { 

450 ln: { 

451 self.internal_substrate_layer_name: convert_substrate_to_overlap_cap(sc) 

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

453 } 

454 

455 d2 = { 

456 oc.top_layer_name: { 

457 oc_bot.bottom_layer_name: oc_bot 

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

459 } 

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

461 } 

462 

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

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

465 if k1 not in d: 

466 d[k1] = {k2: v} 

467 else: 

468 d[k1][k2] = v 

469 return d 

470 

471 @cached_property 

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

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

474 

475 @property 

476 def internal_substrate_layer_name(self) -> str: 

477 return 'VSUBS' 

478 

479 @cached_property 

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

481 """ 

482 usage: dict[in_layer_name][out_layer_name] 

483 """ 

484 

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

486 -> process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance: 

487 soc = process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance() 

488 soc.in_layer_name = sc.layer_name 

489 soc.out_layer_name = self.internal_substrate_layer_name 

490 soc.capacitance = sc.perimeter_capacitance 

491 return soc 

492 

493 d = { 

494 ln: { 

495 self.internal_substrate_layer_name: convert_substrate_to_side_overlap_cap(sc) 

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

497 } 

498 

499 d2 = { 

500 oc.in_layer_name: { 

501 oc_bot.out_layer_name: oc_bot 

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

503 } 

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

505 } 

506 

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

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

509 d[k1][k2] = v 

510 

511 return d 

512