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

161 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 

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 substrate_net_name: str, 

56 k_void: float = 3.5, 

57 delaunay_amax: float = 0.0, 

58 delaunay_b: float = 1.0): 

59 """ 

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

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

62 """ 

63 self.pex_context = pex_context 

64 self.tech_info = tech_info 

65 self.substrate_net_name = substrate_net_name 

66 self.k_void = k_void 

67 self.delaunay_amax = delaunay_amax 

68 self.delaunay_b = delaunay_b 

69 

70 @cached_property 

71 def dbu(self) -> float: 

72 return self.pex_context.dbu 

73 

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

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

76 if not gds_pair: 

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

78 if not gds_pair: 

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

80 return None 

81 return gds_pair 

82 

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

84 gds_pair = self.gds_pair(layer_name=layer_name) 

85 if not gds_pair: 

86 return None 

87 

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

89 if not shapes: 

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

91 return shapes 

92 

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

94 gds_pair = self.gds_pair(layer_name=layer_name) 

95 if not gds_pair: 

96 return None 

97 

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

99 if not shapes: 

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

101 return shapes 

102 

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

104 return self.pex_context.top_cell_bbox() 

105 

106 def build(self) -> FasterCapModelGenerator: 

107 lvsdb = self.pex_context.lvsdb 

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

109 

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

111 td = t.terminal_def() 

112 d = t.device() 

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

114 

115 model_builder = FasterCapModelBuilder( 

116 dbu=self.dbu, 

117 k_void=self.k_void, 

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

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

120 ) 

121 

122 fox_layer = self.tech_info.field_oxide_layer 

123 

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

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

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

127 

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

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

130 if not circuit: 

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

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

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

134 

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

136 

137 for net in circuit.each_net(): 

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

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

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

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

142 

143 net_name = net.expanded_name() 

144 

145 for metal_layer in self.tech_info.process_metal_layers: 

146 metal_layer_name = metal_layer.name 

147 metal_layer = metal_layer.metal_layer 

148 

149 metal_z_bottom = metal_layer.z 

150 metal_z_top = metal_z_bottom + metal_layer.thickness 

151 

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

153 if shapes: 

154 if shapes.count() >= 1: 

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

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

157 model_builder.add_conductor(net_name=net_name, 

158 layer=shapes, 

159 z=metal_layer.z, 

160 height=metal_layer.thickness) 

161 

162 if metal_layer.HasField('contact_above'): 

163 contact = metal_layer.contact_above 

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

165 if shapes and not shapes.is_empty(): 

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

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

168 model_builder.add_conductor(net_name=net_name, 

169 layer=shapes, 

170 z=metal_z_top, 

171 height=contact.thickness) 

172 

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

174 # if diel_above: 

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

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

177 # pass 

178 # TODO: add stuff 

179 

180 # DIFF / TAP 

181 for diffusion_layer in self.tech_info.process_diffusion_layers: 

182 diffusion_layer_name = diffusion_layer.name 

183 diffusion_layer = diffusion_layer.diffusion_layer 

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

185 if shapes and not shapes.is_empty(): 

186 diffusion_regions.append(shapes) 

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

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

189 model_builder.add_conductor(net_name=net_name, 

190 layer=shapes, 

191 z=0, # TODO 

192 height=0.1) # TODO: diffusion_layer.z 

193 

194 contact = diffusion_layer.contact_above 

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

196 if shapes and not shapes.is_empty(): 

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

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

199 model_builder.add_conductor(net_name=net_name, 

200 layer=shapes, 

201 z=0.0, 

202 height=contact.thickness) 

203 

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

205 

206 # 

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

208 # 

209 

210 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer 

211 substrate_region = kdb.Region() 

212 

213 substrate_block = enlarged_top_cell_bbox.dup() 

214 substrate_region.insert(substrate_block) 

215 

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

217 for d in diffusion_regions: 

218 substrate_region -= d.sized(diffusion_margin) 

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

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

221 model_builder.add_conductor(net_name=self.substrate_net_name, 

222 layer=substrate_region, 

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

224 height=substrate_layer.thickness) 

225 

226 # 

227 # add dielectrics 

228 # 

229 

230 fox_region = kdb.Region() 

231 fox_block = enlarged_top_cell_bbox.dup() 

232 fox_region.insert(fox_block) 

233 

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

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

236 fox_z = 0 

237 fox_height = gate_poly_height - fox_z 

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

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

240 model_builder.add_dielectric(material_name=fox_layer.name, 

241 layer=fox_region, 

242 z=fox_z, 

243 height=fox_height) 

244 

245 for metal_layer in self.tech_info.process_metal_layers: 

246 metal_layer_name = metal_layer.name 

247 metal_layer = metal_layer.metal_layer 

248 

249 metal_z_bottom = metal_layer.z 

250 

251 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name) 

252 

253 sidewall_region: Optional[kdb.Region] = None 

254 sidewall_height = 0 

255 

256 no_metal_region: Optional[kdb.Region] = None 

257 no_metal_height = 0 

258 

259 # 

260 # add the conformal dielectric films anchored on this metal 

261 # 

262 if extracted_shapes: 

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

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

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

266 sidewall_height = metal_layer.thickness 

267 sidewall_region = extracted_shapes 

268 wrapped = metal_layer_name 

269 

270 while True: 

271 film = self.tech_info.conformal_dielectric_wrapping(wrapped) 

272 if not film: 

273 break 

274 

275 conf_diel = film.conformal_dielectric_layer 

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

277 sidewall_region = sidewall_region.sized(d) 

278 sidewall_height += conf_diel.thickness_over_metal 

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

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

281 model_builder.add_dielectric(material_name=film.name, 

282 layer=sidewall_region, 

283 z=metal_layer.z, 

284 height=sidewall_height) 

285 if conf_diel.thickness_where_no_metal > 0.0: 

286 no_metal_block = enlarged_top_cell_bbox.dup() 

287 no_metal_region = kdb.Region() 

288 no_metal_region.insert(no_metal_block) 

289 no_metal_region -= sidewall_region 

290 no_metal_height = conf_diel.thickness_where_no_metal 

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

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

293 model_builder.add_dielectric(material_name=film.name, 

294 layer=no_metal_region, 

295 z=metal_layer.z, 

296 height=no_metal_height) 

297 

298 wrapped = film.name 

299 

300 # 

301 # add simple dielectric 

302 # 

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

304 if simple_dielectric: 

305 diel_block = enlarged_top_cell_bbox.dup() 

306 diel_region = kdb.Region() 

307 diel_region.insert(diel_block) 

308 if sidewall_region: 

309 assert sidewall_height >= 0.0 

310 diel_region -= sidewall_region 

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

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

313 model_builder.add_dielectric(material_name=simple_dielectric.name, 

314 layer=sidewall_region, 

315 z=metal_z_bottom + sidewall_height, 

316 height=diel_height - sidewall_height) 

317 if no_metal_region: 

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

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

320 model_builder.add_dielectric(material_name=simple_dielectric.name, 

321 layer=diel_region, 

322 z=metal_z_bottom + no_metal_height, 

323 height=diel_height - no_metal_height) 

324 else: 

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

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

327 model_builder.add_dielectric(material_name=simple_dielectric.name, 

328 layer=diel_region, 

329 z=metal_z_bottom, 

330 height=diel_height) 

331 

332 gen = model_builder.generate() 

333 return gen