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
« 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#
27#
28# Protocol Buffer Schema for FasterCap Input Files
29# https://www.fastfieldsolvers.com/software.htm#fastercap
30#
32from typing import *
33from functools import cached_property
34import math
36import klayout.db as kdb
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
49import klayout_pex_protobuf.process_stack_pb2 as process_stack_pb2
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
65 @cached_property
66 def dbu(self) -> float:
67 return self.pex_context.dbu
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
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
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
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
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
98 def top_cell_bbox(self) -> kdb.Box:
99 return self.pex_context.top_cell_bbox()
101 def build(self) -> FasterCapModelGenerator:
102 lvsdb = self.pex_context.lvsdb
103 netlist: kdb.Netlist = lvsdb.netlist()
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}"
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 )
117 fox_layer = self.tech_info.field_oxide_layer
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)
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}")
130 diffusion_regions: List[kdb.Region] = []
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()))}")
138 net_name = net.expanded_name()
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
144 metal_z_bottom = metal_layer.height
145 metal_z_top = metal_z_bottom + metal_layer.thickness
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)
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)
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
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
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)
199 enlarged_top_cell_bbox = self.top_cell_bbox().enlarged(math.floor(8 / self.dbu)) # 8µm fringe halo
201 #
202 # global substrate block below everything. independent of nets!
203 #
205 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer
206 substrate_region = kdb.Region()
208 substrate_block = enlarged_top_cell_bbox.dup()
209 substrate_region.insert(substrate_block)
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)
221 #
222 # add dielectrics
223 #
225 fox_region = kdb.Region()
226 fox_block = enlarged_top_cell_bbox.dup()
227 fox_region.insert(fox_block)
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)
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
244 metal_z_bottom = metal_layer.height
246 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name)
248 sidewall_region: Optional[kdb.Region] = None
249 sidewall_height = 0
251 no_metal_region: Optional[kdb.Region] = None
252 no_metal_height = 0
254 #
255 # add sidewall dielectrics
256 #
257 if extracted_shapes:
258 sidewall_height = 0
259 sidewall_region = extracted_shapes
260 sidewallee = metal_layer_name
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)
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)
305 sidewallee = sidewall.name
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)
339 gen = model_builder.generate()
340 return gen