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

160 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 

26 

27# 

28# Protocol Buffer Schema for FasterCap Input Files 

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

30# 

31 

32from typing import * 

33from functools import cached_property 

34import math 

35 

36import klayout.db as kdb 

37 

38from ..klayout.lvsdb_extractor import KLayoutExtractionContext, GDSPair 

39from .fastercap_model_generator import FasterCapModelBuilder, FasterCapModelGenerator 

40from ..log import ( 

41 console, 

42 debug, 

43 info, 

44 warning, 

45 error 

46) 

47from ..tech_info import TechInfo 

48 

49 

50 

51class FasterCapInputBuilder: 

52 def __init__(self, 

53 pex_context: KLayoutExtractionContext, 

54 tech_info: TechInfo, 

55 k_void: float = 3.5, 

56 delaunay_amax: float = 0.0, 

57 delaunay_b: float = 1.0): 

58 self.pex_context = pex_context 

59 self.tech_info = tech_info 

60 self.k_void = k_void 

61 self.delaunay_amax = delaunay_amax 

62 self.delaunay_b = delaunay_b 

63 

64 @cached_property 

65 def dbu(self) -> float: 

66 return self.pex_context.dbu 

67 

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

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

70 if not gds_pair: 

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

72 if not gds_pair: 

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

74 return None 

75 return gds_pair 

76 

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

78 gds_pair = self.gds_pair(layer_name=layer_name) 

79 if not gds_pair: 

80 return None 

81 

82 shapes = self.pex_context.shapes_of_net(gds_pair=gds_pair, net=net) 

83 if not shapes: 

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

85 return shapes 

86 

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

88 gds_pair = self.gds_pair(layer_name=layer_name) 

89 if not gds_pair: 

90 return None 

91 

92 shapes = self.pex_context.shapes_of_layer(gds_pair=gds_pair) 

93 if not shapes: 

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

95 return shapes 

96 

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

98 return self.pex_context.top_cell_bbox() 

99 

100 def build(self) -> FasterCapModelGenerator: 

101 lvsdb = self.pex_context.lvsdb 

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

103 

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

105 td = t.terminal_def() 

106 d = t.device() 

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

108 

109 model_builder = FasterCapModelBuilder( 

110 dbu=self.dbu, 

111 k_void=self.k_void, 

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

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

114 ) 

115 

116 fox_layer = self.tech_info.field_oxide_layer 

117 

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

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

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

121 

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

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

124 if not circuit: 

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

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

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

128 

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

130 

131 for net in circuit.each_net(): 

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

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

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

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

136 

137 net_name = net.expanded_name() 

138 

139 for metal_layer in self.tech_info.process_metal_layers: 

140 metal_layer_name = metal_layer.name 

141 metal_layer = metal_layer.metal_layer 

142 

143 metal_z_bottom = metal_layer.z 

144 metal_z_top = metal_z_bottom + metal_layer.thickness 

145 

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

147 if shapes: 

148 if shapes.count() >= 1: 

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

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

151 model_builder.add_conductor(net_name=net_name, 

152 layer=shapes, 

153 z=metal_layer.z, 

154 height=metal_layer.thickness) 

155 

156 if metal_layer.HasField('contact_above'): 

157 contact = metal_layer.contact_above 

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

159 if shapes and not shapes.is_empty(): 

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

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

162 model_builder.add_conductor(net_name=net_name, 

163 layer=shapes, 

164 z=metal_z_top, 

165 height=contact.thickness) 

166 

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

168 # if diel_above: 

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

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

171 # pass 

172 # TODO: add stuff 

173 

174 # DIFF / TAP 

175 for diffusion_layer in self.tech_info.process_diffusion_layers: 

176 diffusion_layer_name = diffusion_layer.name 

177 diffusion_layer = diffusion_layer.diffusion_layer 

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

179 if shapes and not shapes.is_empty(): 

180 diffusion_regions.append(shapes) 

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

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

183 model_builder.add_conductor(net_name=net_name, 

184 layer=shapes, 

185 z=0, # TODO 

186 height=0.1) # TODO: diffusion_layer.z 

187 

188 contact = diffusion_layer.contact_above 

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

190 if shapes and not shapes.is_empty(): 

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

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

193 model_builder.add_conductor(net_name=net_name, 

194 layer=shapes, 

195 z=0.0, 

196 height=contact.thickness) 

197 

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

199 

200 # 

201 # global substrate block below everything. independent of nets! 

202 # 

203 

204 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer 

205 substrate_region = kdb.Region() 

206 

207 substrate_block = enlarged_top_cell_bbox.dup() 

208 substrate_region.insert(substrate_block) 

209 

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

211 for d in diffusion_regions: 

212 substrate_region -= d.sized(diffusion_margin) 

213 info(f"Substrate VSUBS, " 

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

215 model_builder.add_conductor(net_name="VSUBS", 

216 layer=substrate_region, 

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

218 height=substrate_layer.thickness) 

219 

220 # 

221 # add dielectrics 

222 # 

223 

224 fox_region = kdb.Region() 

225 fox_block = enlarged_top_cell_bbox.dup() 

226 fox_region.insert(fox_block) 

227 

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

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

230 fox_z = 0 

231 fox_height = gate_poly_height - fox_z 

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

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

234 model_builder.add_dielectric(material_name=fox_layer.name, 

235 layer=fox_region, 

236 z=fox_z, 

237 height=fox_height) 

238 

239 for metal_layer in self.tech_info.process_metal_layers: 

240 metal_layer_name = metal_layer.name 

241 metal_layer = metal_layer.metal_layer 

242 

243 metal_z_bottom = metal_layer.z 

244 

245 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name) 

246 

247 sidewall_region: Optional[kdb.Region] = None 

248 sidewall_height = 0 

249 

250 no_metal_region: Optional[kdb.Region] = None 

251 no_metal_height = 0 

252 

253 # 

254 # add the conformal dielectric films anchored on this metal 

255 # 

256 if extracted_shapes: 

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

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

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

260 sidewall_height = metal_layer.thickness 

261 sidewall_region = extracted_shapes 

262 wrapped = metal_layer_name 

263 

264 while True: 

265 film = self.tech_info.conformal_dielectric_wrapping(wrapped) 

266 if not film: 

267 break 

268 

269 conf_diel = film.conformal_dielectric_layer 

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

271 sidewall_region = sidewall_region.sized(d) 

272 sidewall_height += conf_diel.thickness_over_metal 

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

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

275 model_builder.add_dielectric(material_name=film.name, 

276 layer=sidewall_region, 

277 z=metal_layer.z, 

278 height=sidewall_height) 

279 if conf_diel.thickness_where_no_metal > 0.0: 

280 no_metal_block = enlarged_top_cell_bbox.dup() 

281 no_metal_region = kdb.Region() 

282 no_metal_region.insert(no_metal_block) 

283 no_metal_region -= sidewall_region 

284 no_metal_height = conf_diel.thickness_where_no_metal 

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

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

287 model_builder.add_dielectric(material_name=film.name, 

288 layer=no_metal_region, 

289 z=metal_layer.z, 

290 height=no_metal_height) 

291 

292 wrapped = film.name 

293 

294 # 

295 # add simple dielectric 

296 # 

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

298 if simple_dielectric: 

299 diel_block = enlarged_top_cell_bbox.dup() 

300 diel_region = kdb.Region() 

301 diel_region.insert(diel_block) 

302 if sidewall_region: 

303 assert sidewall_height >= 0.0 

304 diel_region -= sidewall_region 

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

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

307 model_builder.add_dielectric(material_name=simple_dielectric.name, 

308 layer=sidewall_region, 

309 z=metal_z_bottom + sidewall_height, 

310 height=diel_height - sidewall_height) 

311 if no_metal_region: 

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

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

314 model_builder.add_dielectric(material_name=simple_dielectric.name, 

315 layer=diel_region, 

316 z=metal_z_bottom + no_metal_height, 

317 height=diel_height - no_metal_height) 

318 else: 

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

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

321 model_builder.add_dielectric(material_name=simple_dielectric.name, 

322 layer=diel_region, 

323 z=metal_z_bottom, 

324 height=diel_height) 

325 

326 gen = model_builder.generate() 

327 return gen