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

171 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-17 17:24 +0000

1#! /usr/bin/env python3 

2# 

3# -------------------------------------------------------------------------------- 

4# SPDX-FileCopyrightText: 2024 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/martinjankoehler/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 

49import klayout_pex_protobuf.process_stack_pb2 as process_stack_pb2 

50 

51 

52class FasterCapInputBuilder: 

53 def __init__(self, 

54 pex_context: KLayoutExtractionContext, 

55 tech_info: TechInfo, 

56 k_void: float = 3.5, 

57 delaunay_amax: float = 0.0, 

58 delaunay_b: float = 1.0): 

59 self.pex_context = pex_context 

60 self.tech_info = tech_info 

61 self.k_void = k_void 

62 self.delaunay_amax = delaunay_amax 

63 self.delaunay_b = delaunay_b 

64 

65 @cached_property 

66 def dbu(self) -> float: 

67 return self.pex_context.dbu 

68 

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

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

71 if not gds_pair: 

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

73 if not gds_pair: 

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

75 return None 

76 return gds_pair 

77 

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

79 gds_pair = self.gds_pair(layer_name=layer_name) 

80 if not gds_pair: 

81 return None 

82 

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

84 if not shapes: 

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

86 return shapes 

87 

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

89 gds_pair = self.gds_pair(layer_name=layer_name) 

90 if not gds_pair: 

91 return None 

92 

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

94 if not shapes: 

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

96 return shapes 

97 

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

99 return self.pex_context.top_cell_bbox() 

100 

101 def build(self) -> FasterCapModelGenerator: 

102 lvsdb = self.pex_context.lvsdb 

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

104 

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

106 td = t.terminal_def() 

107 d = t.device() 

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

109 

110 model_builder = FasterCapModelBuilder( 

111 dbu=self.dbu, 

112 k_void=self.k_void, 

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

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

115 ) 

116 

117 fox_layer = self.tech_info.field_oxide_layer 

118 

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

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

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

122 

123 circuit = netlist.circuit_by_name(self.pex_context.top_cell.name) 

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

125 if not circuit: 

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

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

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

129 

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

131 

132 for net in circuit.each_net(): 

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

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

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

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

137 

138 net_name = net.expanded_name() 

139 

140 for metal_layer in self.tech_info.process_metal_layers: 

141 metal_layer_name = metal_layer.name 

142 metal_layer = metal_layer.metal_layer 

143 

144 metal_z_bottom = metal_layer.height 

145 metal_z_top = metal_z_bottom + metal_layer.thickness 

146 

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

148 if shapes: 

149 if shapes.count() >= 1: 

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

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

152 model_builder.add_conductor(net_name=net_name, 

153 layer=shapes, 

154 z=metal_layer.height, 

155 height=metal_layer.thickness) 

156 

157 if metal_layer.HasField('contact_above'): 

158 contact = metal_layer.contact_above 

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

160 if shapes and not shapes.is_empty(): 

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

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

163 model_builder.add_conductor(net_name=net_name, 

164 layer=shapes, 

165 z=metal_z_top, 

166 height=contact.thickness) 

167 

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

169 # if diel_above: 

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

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

172 # pass 

173 # TODO: add stuff 

174 

175 # DIFF / TAP 

176 for diffusion_layer in self.tech_info.process_diffusion_layers: 

177 diffusion_layer_name = diffusion_layer.name 

178 diffusion_layer = diffusion_layer.diffusion_layer 

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

180 if shapes and not shapes.is_empty(): 

181 diffusion_regions.append(shapes) 

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

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

184 model_builder.add_conductor(net_name=net_name, 

185 layer=shapes, 

186 z=0, # TODO 

187 height=0.1) # TODO: diffusion_layer.height 

188 

189 contact = diffusion_layer.contact_above 

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

191 if shapes and not shapes.is_empty(): 

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

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

194 model_builder.add_conductor(net_name=net_name, 

195 layer=shapes, 

196 z=0.0, 

197 height=contact.thickness) 

198 

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

200 

201 # 

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

203 # 

204 

205 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer 

206 substrate_region = kdb.Region() 

207 

208 substrate_block = enlarged_top_cell_bbox.dup() 

209 substrate_region.insert(substrate_block) 

210 

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

212 for d in diffusion_regions: 

213 substrate_region -= d.sized(diffusion_margin) 

214 info(f"Substrate VSUBS, " 

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

216 model_builder.add_conductor(net_name="VSUBS", 

217 layer=substrate_region, 

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

219 height=substrate_layer.thickness) 

220 

221 # 

222 # add dielectrics 

223 # 

224 

225 fox_region = kdb.Region() 

226 fox_block = enlarged_top_cell_bbox.dup() 

227 fox_region.insert(fox_block) 

228 

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

230 gate_poly_height = self.tech_info.gate_poly_layer.metal_layer.height 

231 fox_z = 0 

232 fox_height = gate_poly_height - fox_z 

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

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

235 model_builder.add_dielectric(material_name=fox_layer.name, 

236 layer=fox_region, 

237 z=fox_z, 

238 height=fox_height) 

239 

240 for metal_layer in self.tech_info.process_metal_layers: 

241 metal_layer_name = metal_layer.name 

242 metal_layer = metal_layer.metal_layer 

243 

244 metal_z_bottom = metal_layer.height 

245 

246 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name) 

247 

248 sidewall_region: Optional[kdb.Region] = None 

249 sidewall_height = 0 

250 

251 no_metal_region: Optional[kdb.Region] = None 

252 no_metal_height = 0 

253 

254 # 

255 # add sidewall dielectrics 

256 # 

257 if extracted_shapes: 

258 sidewall_height = 0 

259 sidewall_region = extracted_shapes 

260 sidewallee = metal_layer_name 

261 

262 while True: 

263 sidewall = self.tech_info.sidewall_dielectric_layer(sidewallee) 

264 if not sidewall: 

265 break 

266 match sidewall.layer_type: 

267 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC: 

268 d = math.floor(sidewall.sidewall_dielectric_layer.width_outside_sidewall / self.dbu) 

269 sidewall_region = sidewall_region.sized(d) 

270 h_delta = sidewall.sidewall_dielectric_layer.height_above_metal or metal_layer.thickness 

271 # if h_delta == 0: 

272 # h_delta = metal_layer.thickness 

273 sidewall_height += h_delta 

274 info(f"Sidewall dielectric {sidewall.name}: z={metal_layer.height}, height={sidewall_height}") 

275 model_builder.add_dielectric(material_name=sidewall.name, 

276 layer=sidewall_region, 

277 z=metal_layer.height, 

278 height=sidewall_height) 

279 

280 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC: 

281 conf_diel = sidewall.conformal_dielectric_layer 

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

283 sidewall_region = sidewall_region.sized(d) 

284 h_delta = metal_layer.thickness + conf_diel.thickness_over_metal 

285 sidewall_height += h_delta 

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

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

288 model_builder.add_dielectric(material_name=sidewall.name, 

289 layer=sidewall_region, 

290 z=metal_layer.height, 

291 height=sidewall_height) 

292 if conf_diel.thickness_where_no_metal > 0.0: 

293 no_metal_block = enlarged_top_cell_bbox.dup() 

294 no_metal_region = kdb.Region() 

295 no_metal_region.insert(no_metal_block) 

296 no_metal_region -= sidewall_region 

297 no_metal_height = conf_diel.thickness_where_no_metal 

298 info(f"Conformal dielectric (where no metal) {sidewall.name}: " 

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

300 model_builder.add_dielectric(material_name=sidewall.name, 

301 layer=no_metal_region, 

302 z=metal_layer.height, 

303 height=no_metal_height) 

304 

305 sidewallee = sidewall.name 

306 

307 # 

308 # add simple dielectric 

309 # 

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

311 if simple_dielectric: 

312 diel_block = enlarged_top_cell_bbox.dup() 

313 diel_region = kdb.Region() 

314 diel_region.insert(diel_block) 

315 if sidewall_region: 

316 assert sidewall_height >= 0.0 

317 diel_region -= sidewall_region 

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

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

320 model_builder.add_dielectric(material_name=simple_dielectric.name, 

321 layer=sidewall_region, 

322 z=metal_z_bottom + sidewall_height, 

323 height=diel_height - sidewall_height) 

324 if no_metal_region: 

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

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

327 model_builder.add_dielectric(material_name=simple_dielectric.name, 

328 layer=diel_region, 

329 z=metal_z_bottom + no_metal_height, 

330 height=diel_height - no_metal_height) 

331 else: 

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

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

334 model_builder.add_dielectric(material_name=simple_dielectric.name, 

335 layer=diel_region, 

336 z=metal_z_bottom, 

337 height=diel_height) 

338 

339 gen = model_builder.generate() 

340 return gen