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
« 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#
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
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
64 @cached_property
65 def dbu(self) -> float:
66 return self.pex_context.dbu
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
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
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
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
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
97 def top_cell_bbox(self) -> kdb.Box:
98 return self.pex_context.top_cell_bbox()
100 def build(self) -> FasterCapModelGenerator:
101 lvsdb = self.pex_context.lvsdb
102 netlist: kdb.Netlist = lvsdb.netlist()
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}"
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 )
116 fox_layer = self.tech_info.field_oxide_layer
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)
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}")
129 diffusion_regions: List[kdb.Region] = []
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()))}")
137 net_name = net.expanded_name()
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
143 metal_z_bottom = metal_layer.z
144 metal_z_top = metal_z_bottom + metal_layer.thickness
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)
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)
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
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
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)
198 enlarged_top_cell_bbox = self.top_cell_bbox().enlarged(math.floor(8 / self.dbu)) # 8µm fringe halo
200 #
201 # global substrate block below everything. independent of nets!
202 #
204 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer
205 substrate_region = kdb.Region()
207 substrate_block = enlarged_top_cell_bbox.dup()
208 substrate_region.insert(substrate_block)
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)
220 #
221 # add dielectrics
222 #
224 fox_region = kdb.Region()
225 fox_block = enlarged_top_cell_bbox.dup()
226 fox_region.insert(fox_block)
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)
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
243 metal_z_bottom = metal_layer.z
245 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name)
247 sidewall_region: Optional[kdb.Region] = None
248 sidewall_height = 0
250 no_metal_region: Optional[kdb.Region] = None
251 no_metal_height = 0
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
264 while True:
265 film = self.tech_info.conformal_dielectric_wrapping(wrapped)
266 if not film:
267 break
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)
292 wrapped = film.name
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)
326 gen = model_builder.generate()
327 return gen