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
« 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#
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 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
70 @cached_property
71 def dbu(self) -> float:
72 return self.pex_context.dbu
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
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
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
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
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
103 def top_cell_bbox(self) -> kdb.Box:
104 return self.pex_context.top_cell_bbox()
106 def build(self) -> FasterCapModelGenerator:
107 lvsdb = self.pex_context.lvsdb
108 netlist: kdb.Netlist = lvsdb.netlist()
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}"
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 )
122 fox_layer = self.tech_info.field_oxide_layer
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)
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}")
135 diffusion_regions: List[kdb.Region] = []
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()))}")
143 net_name = net.expanded_name()
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
149 metal_z_bottom = metal_layer.z
150 metal_z_top = metal_z_bottom + metal_layer.thickness
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)
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)
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
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
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)
204 enlarged_top_cell_bbox = self.top_cell_bbox().enlarged(math.floor(8 / self.dbu)) # 8µm fringe halo
206 #
207 # global substrate block below everything, a conductor of the substrate net
208 #
210 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer
211 substrate_region = kdb.Region()
213 substrate_block = enlarged_top_cell_bbox.dup()
214 substrate_region.insert(substrate_block)
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)
226 #
227 # add dielectrics
228 #
230 fox_region = kdb.Region()
231 fox_block = enlarged_top_cell_bbox.dup()
232 fox_region.insert(fox_block)
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)
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
249 metal_z_bottom = metal_layer.z
251 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name)
253 sidewall_region: Optional[kdb.Region] = None
254 sidewall_height = 0
256 no_metal_region: Optional[kdb.Region] = None
257 no_metal_height = 0
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
270 while True:
271 film = self.tech_info.conformal_dielectric_wrapping(wrapped)
272 if not film:
273 break
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)
298 wrapped = film.name
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)
332 gen = model_builder.generate()
333 return gen