Coverage for klayout_pex/fastercap/fastercap_input_builder.py: 97%

186 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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 

26 

27# 

28# Protocol Buffer Schema for FasterCap Input Files 

29# https://www.fastfieldsolvers.com/software.htm#fastercap 

30# 

31 

32from collections import defaultdict 

33from typing import * 

34from functools import cached_property 

35import math 

36 

37import klayout.db as kdb 

38 

39from ..klayout.lvsdb_extractor import KLayoutExtractionContext 

40from .fastercap_model_generator import FasterCapModelBuilder, FasterCapModelGenerator 

41from ..log import ( 

42 console, 

43 debug, 

44 info, 

45 warning, 

46 error 

47) 

48from ..rcx25.exclusion import ConductorPart, Exclusions 

49from ..tech_info import TechInfo 

50from ..types import GDSPair, LayerName 

51 

52 

53 

54class FasterCapInputBuilder: 

55 def __init__(self, 

56 pex_context: KLayoutExtractionContext, 

57 tech_info: TechInfo, 

58 substrate_net_name: str, 

59 k_void: float = 3.5, 

60 delaunay_amax: float = 0.0, 

61 delaunay_b: float = 1.0, 

62 exclusions: Optional[Exclusions] = None): 

63 """ 

64 :param substrate_net_name: the conductor of the substrate block, the substrate net (e.g. VSS), 

65 whose metal is then the same conductor, or a name for a port of its own 

66 :param exclusions: what a model has the parasitics of, so the nets have conductors per block 

67 (see Exclusions.capacitance_matrix_of_nets) 

68 """ 

69 self.pex_context = pex_context 

70 self.tech_info = tech_info 

71 self.substrate_net_name = substrate_net_name 

72 self.k_void = k_void 

73 self.delaunay_amax = delaunay_amax 

74 self.delaunay_b = delaunay_b 

75 self.exclusions = exclusions 

76 self.conductor_parts_by_layer: Dict[str, kdb.Region] = defaultdict(kdb.Region) 

77 

78 @cached_property 

79 def dbu(self) -> float: 

80 return self.pex_context.dbu 

81 

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

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

84 if not gds_pair: 

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

86 if not gds_pair: 

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

88 return None 

89 return gds_pair 

90 

91 # NOTE: layers of the process stack can share a GDS pair (e.g. a metal without and with a MIM cap above), 

92 # so each of them has the shapes of its own LVS layers only 

93 def shapes_of_net(self, layer_name: str, net: kdb.Net) -> Optional[kdb.Region]: 

94 gds_pair = self.gds_pair(layer_name=layer_name) 

95 if not gds_pair: 

96 return None 

97 

98 lvs_layer_names = self.tech_info.lvs_layer_names_by_process_layer_name.get(layer_name, []) 

99 shapes = self.pex_context.shapes_of_net(gds_pair=gds_pair, net=net, lvs_layer_names=lvs_layer_names) 

100 if not shapes: 

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

102 return shapes 

103 

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

105 gds_pair = self.gds_pair(layer_name=layer_name) 

106 if not gds_pair: 

107 return None 

108 

109 lvs_layer_names = self.tech_info.lvs_layer_names_by_process_layer_name.get(layer_name, []) 

110 shapes = self.pex_context.shapes_of_layer(gds_pair=gds_pair, lvs_layer_names=lvs_layer_names) 

111 if not shapes: 

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

113 return shapes 

114 

115 def top_cell_bbox(self) -> kdb.Box: 

116 return self.pex_context.top_cell_bbox() 

117 

118 def canonical_layer_name(self, layer_name: str) -> LayerName: 

119 """ 

120 :return: the name of the layer of the stack by the one of the process stack, the masks go by these 

121 """ 

122 return self.tech_info.canonical_layer_name_by_gds_pair.get(self.gds_pair(layer_name=layer_name), layer_name) 

123 

124 def add_conductor(self, 

125 model_builder: FasterCapModelBuilder, 

126 net_name: str, 

127 layer_name: str, 

128 shapes: kdb.Region, 

129 z: float, 

130 height: float): 

131 """ 

132 :param layer_name: the layer of the process stack 

133 """ 

134 if self.exclusions is None: 

135 model_builder.add_conductor(net_name=net_name, layer=shapes, z=z, height=height) 

136 return 

137 for part, conductor_name in self.exclusions.conductor_parts_of_shapes(shapes, net_name, 

138 self.canonical_layer_name(layer_name)): 

139 model_builder.add_conductor(net_name=conductor_name, layer=part, z=z, height=height, group_name=net_name) 

140 self.conductor_parts_by_layer[layer_name] += part 

141 

142 def add_via(self, 

143 model_builder: FasterCapModelBuilder, 

144 net_name: str, 

145 layer_below_name: str, 

146 contact: Any, 

147 shapes: kdb.Region, 

148 z: float): 

149 """ 

150 :param layer_below_name: the layer of the process stack that has the contact above it 

151 :param contact: the contact, of the process stack 

152 """ 

153 if self.exclusions is None: 

154 model_builder.add_conductor(net_name=net_name, layer=shapes, z=z, height=contact.thickness) 

155 return 

156 for part, conductor_name in self.exclusions.conductor_parts_of_vias( 

157 shapes, net_name, 

158 self.canonical_layer_name(layer_below_name), 

159 self.canonical_layer_name(contact.metal_above or layer_below_name)): 

160 model_builder.add_conductor(net_name=conductor_name, layer=part, z=z, height=contact.thickness, 

161 group_name=net_name) 

162 

163 def build(self) -> FasterCapModelGenerator: 

164 lvsdb = self.pex_context.lvsdb 

165 netlist: kdb.Netlist = lvsdb.netlist() 

166 

167 def format_terminal(t: kdb.NetTerminalRef) -> str: 

168 td = t.terminal_def() 

169 d = t.device() 

170 return f"{d.expanded_name()}/{td.name}/{td.description}" 

171 

172 model_builder = FasterCapModelBuilder( 

173 dbu=self.dbu, 

174 k_void=self.k_void, 

175 delaunay_amax=self.delaunay_amax, # test/compare with smaller, e.g. 0.05 => more triangles 

176 delaunay_b=self.delaunay_b # test/compare with 1.0 => more triangles at edges 

177 ) 

178 

179 fox_layer = self.tech_info.field_oxide_layer 

180 

181 model_builder.add_material(name=fox_layer.name, k=fox_layer.field_oxide_layer.dielectric_k) 

182 for diel_name, diel_k in self.tech_info.dielectric_by_name.items(): 

183 model_builder.add_material(name=diel_name, k=diel_k) 

184 

185 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name) 

186 # https://www.klayout.de/doc-qt5/code/class_Circuit.html 

187 if not circuit: 

188 circuits = [c.name for c in netlist.each_circuit()] 

189 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, " 

190 f"only available circuits are: {circuits}") 

191 

192 diffusion_regions: List[kdb.Region] = [] 

193 

194 for net in circuit.each_net(): 

195 # https://www.klayout.de/doc-qt5/code/class_Net.html 

196 debug(f"Net name={net.name}, expanded_name={net.expanded_name()}, pin_count={net.pin_count()}, " 

197 f"is_floating={net.is_floating()}, is_passive={net.is_passive()}, " 

198 f"terminals={list(map(lambda t: format_terminal(t), net.each_terminal()))}") 

199 

200 net_name = net.expanded_name() 

201 

202 for metal_layer in self.tech_info.process_metal_layers: 

203 metal_layer_name = metal_layer.name 

204 metal_layer = metal_layer.metal_layer 

205 

206 metal_z_bottom = metal_layer.z 

207 metal_z_top = metal_z_bottom + metal_layer.thickness 

208 

209 shapes = self.shapes_of_net(layer_name=metal_layer_name, net=net) 

210 if shapes: 

211 if shapes.count() >= 1: 

212 info(f"Conductor {net_name}, metal {metal_layer_name}, " 

213 f"z={metal_layer.z}, height={metal_layer.thickness}") 

214 self.add_conductor(model_builder, 

215 net_name=net_name, 

216 layer_name=metal_layer_name, 

217 shapes=shapes, 

218 z=metal_layer.z, 

219 height=metal_layer.thickness) 

220 

221 if metal_layer.HasField('contact_above'): 

222 contact = metal_layer.contact_above 

223 shapes = self.shapes_of_net(layer_name=contact.name, net=net) 

224 if shapes and not shapes.is_empty(): 

225 info(f"Conductor {net_name}, via {contact.name}, " 

226 f"z={metal_z_top}, height={contact.thickness}") 

227 self.add_via(model_builder, 

228 net_name=net_name, 

229 layer_below_name=metal_layer_name, 

230 contact=contact, 

231 shapes=shapes, 

232 z=metal_z_top) 

233 

234 # diel_above = self.tech_info.process_stack_layer_by_name.get(metal_layer.reference_above, None) 

235 # if diel_above: 

236 # #model_builder.add_dielectric(material_name=metal_layer.reference_above, 

237 # # layer=kdb.Region().) 

238 # pass 

239 # TODO: add stuff 

240 

241 # DIFF / TAP 

242 for diffusion_layer in self.tech_info.process_diffusion_layers: 

243 diffusion_layer_name = diffusion_layer.name 

244 diffusion_layer = diffusion_layer.diffusion_layer 

245 shapes = self.shapes_of_net(layer_name=diffusion_layer_name, net=net) 

246 if shapes and not shapes.is_empty(): 

247 diffusion_regions.append(shapes) 

248 info(f"Diffusion {net_name}, layer {diffusion_layer_name}, " 

249 f"z={0}, height={0.1}") 

250 self.add_conductor(model_builder, 

251 net_name=net_name, 

252 layer_name=diffusion_layer_name, 

253 shapes=shapes, 

254 z=0, # TODO 

255 height=0.1) # TODO: diffusion_layer.z 

256 

257 contact = diffusion_layer.contact_above 

258 shapes = self.shapes_of_net(layer_name=contact.name, net=net) 

259 if shapes and not shapes.is_empty(): 

260 info(f"Diffusion {net_name}, contact {contact.name}, " 

261 f"z={0}, height={contact.thickness}") 

262 self.add_via(model_builder, 

263 net_name=net_name, 

264 layer_below_name=diffusion_layer_name, 

265 contact=contact, 

266 shapes=shapes, 

267 z=0.0) 

268 

269 enlarged_top_cell_bbox = self.top_cell_bbox().enlarged(math.floor(8 / self.dbu)) # 8µm fringe halo 

270 

271 # 

272 # global substrate block below everything, a conductor of the substrate net 

273 # 

274 

275 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer 

276 substrate_region = kdb.Region() 

277 

278 substrate_block = enlarged_top_cell_bbox.dup() 

279 substrate_region.insert(substrate_block) 

280 

281 diffusion_margin = math.floor(1 / self.dbu) # 1 µm 

282 for d in diffusion_regions: 

283 substrate_region -= d.sized(diffusion_margin) 

284 info(f"Substrate {self.substrate_net_name}, " 

285 f"z={0 - substrate_layer.height - substrate_layer.thickness}, height={substrate_layer.thickness}") 

286 # NOTE: with exclusions, the substrate is apart from the metal of its net (e.g. VSS), 

287 # which can be within a block 

288 substrate_conductor_name = self.substrate_net_name 

289 if self.exclusions is not None: 

290 substrate_conductor_name = self.exclusions.conductor_name(ConductorPart(self.substrate_net_name, 

291 is_substrate=True)) 

292 model_builder.add_conductor(net_name=substrate_conductor_name, 

293 layer=substrate_region, 

294 z=0 - substrate_layer.height - substrate_layer.thickness, 

295 height=substrate_layer.thickness, 

296 group_name=self.substrate_net_name) 

297 

298 # 

299 # add dielectrics 

300 # 

301 

302 fox_region = kdb.Region() 

303 fox_block = enlarged_top_cell_bbox.dup() 

304 fox_region.insert(fox_block) 

305 

306 # field oxide goes from substrate/diff/well up to below the gate-poly 

307 gate_poly_height = self.tech_info.gate_poly_layer.metal_layer.z 

308 fox_z = 0 

309 fox_height = gate_poly_height - fox_z 

310 info(f"Simple dielectric (field oxide) {fox_layer.name}: " 

311 f"z={fox_z}, height={fox_height}") 

312 model_builder.add_dielectric(material_name=fox_layer.name, 

313 layer=fox_region, 

314 z=fox_z, 

315 height=fox_height) 

316 

317 for metal_layer in self.tech_info.process_metal_layers: 

318 metal_layer_name = metal_layer.name 

319 metal_layer = metal_layer.metal_layer 

320 

321 metal_z_bottom = metal_layer.z 

322 

323 # NOTE: as the conductors have them with exclusions, any-angle edges have the cuts as vertices then, 

324 # which would leave slivers between these conductors and dielectrics of the shapes as they are 

325 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name) if self.exclusions is None \ 

326 else self.conductor_parts_by_layer.get(metal_layer_name, None) 

327 

328 sidewall_region: Optional[kdb.Region] = None 

329 sidewall_height = 0 

330 

331 no_metal_region: Optional[kdb.Region] = None 

332 no_metal_height = 0 

333 

334 # 

335 # add the conformal dielectric films anchored on this metal 

336 # 

337 if extracted_shapes: 

338 # Films grow incrementally along the chain: each one is measured from 

339 # the surface of the film it wraps, so the metal's own thickness is 

340 # added once, by the first link, and not again by the ones above it. 

341 sidewall_height = metal_layer.thickness 

342 sidewall_region = extracted_shapes 

343 wrapped = metal_layer_name 

344 

345 while True: 

346 film = self.tech_info.conformal_dielectric_wrapping(wrapped) 

347 if not film: 

348 break 

349 

350 conf_diel = film.conformal_dielectric_layer 

351 d = math.floor(conf_diel.thickness_sidewall / self.dbu) 

352 sidewall_region = sidewall_region.sized(d) 

353 sidewall_height += conf_diel.thickness_over_metal 

354 info(f"Conformal dielectric (sidewall) {film.name}: " 

355 f"z={metal_layer.z}, height={sidewall_height}") 

356 model_builder.add_dielectric(material_name=film.name, 

357 layer=sidewall_region, 

358 z=metal_layer.z, 

359 height=sidewall_height) 

360 if conf_diel.thickness_where_no_metal > 0.0: 

361 no_metal_block = enlarged_top_cell_bbox.dup() 

362 no_metal_region = kdb.Region() 

363 no_metal_region.insert(no_metal_block) 

364 no_metal_region -= sidewall_region 

365 no_metal_height = conf_diel.thickness_where_no_metal 

366 info(f"Conformal dielectric (where no metal) {film.name}: " 

367 f"z={metal_layer.z}, height={no_metal_height}") 

368 model_builder.add_dielectric(material_name=film.name, 

369 layer=no_metal_region, 

370 z=metal_layer.z, 

371 height=no_metal_height) 

372 

373 wrapped = film.name 

374 

375 # 

376 # add simple dielectric 

377 # 

378 simple_dielectric, diel_height = self.tech_info.simple_dielectric_above_metal(metal_layer_name) 

379 if simple_dielectric: 

380 diel_block = enlarged_top_cell_bbox.dup() 

381 diel_region = kdb.Region() 

382 diel_region.insert(diel_block) 

383 if sidewall_region: 

384 assert sidewall_height >= 0.0 

385 diel_region -= sidewall_region 

386 info(f"Simple dielectric (sidewall) {simple_dielectric.name}: " 

387 f"z={metal_z_bottom + sidewall_height}, height={diel_height - sidewall_height}") 

388 model_builder.add_dielectric(material_name=simple_dielectric.name, 

389 layer=sidewall_region, 

390 z=metal_z_bottom + sidewall_height, 

391 height=diel_height - sidewall_height) 

392 if no_metal_region: 

393 info(f"Simple dielectric (no metal) {simple_dielectric.name}: " 

394 f"z={metal_z_bottom + no_metal_height}, height={diel_height - no_metal_height}") 

395 model_builder.add_dielectric(material_name=simple_dielectric.name, 

396 layer=diel_region, 

397 z=metal_z_bottom + no_metal_height, 

398 height=diel_height - no_metal_height) 

399 else: 

400 info(f"Simple dielectric {simple_dielectric.name}: " 

401 f"z={metal_z_bottom}, height={diel_height}") 

402 model_builder.add_dielectric(material_name=simple_dielectric.name, 

403 layer=diel_region, 

404 z=metal_z_bottom, 

405 height=diel_height) 

406 

407 gen = model_builder.generate() 

408 return gen