Coverage for klayout_pex/fastercap/fastercap_input_builder.py: 97%
186 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +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 collections import defaultdict
33from typing import *
34from functools import cached_property
35import math
37import klayout.db as kdb
39from ..klayout.lvsdb_extractor import KLayoutExtractionContext
40from .fastercap_model_generator import FasterCapModelBuilder, FasterCapModelGenerator
41from ..log import (
42 console,
43 debug,
44 info,
45 warning,
46 error
47)
48from ..rcx25.exclusion import ConductorPart, Exclusions
49from ..tech_info import TechInfo
50from ..types import GDSPair, LayerName
54class FasterCapInputBuilder:
55 def __init__(self,
56 pex_context: KLayoutExtractionContext,
57 tech_info: TechInfo,
58 substrate_net_name: str,
59 k_void: float = 3.5,
60 delaunay_amax: float = 0.0,
61 delaunay_b: float = 1.0,
62 exclusions: Optional[Exclusions] = None):
63 """
64 :param substrate_net_name: the conductor of the substrate block, the substrate net (e.g. VSS),
65 whose metal is then the same conductor, or a name for a port of its own
66 :param exclusions: what a model has the parasitics of, so the nets have conductors per block
67 (see Exclusions.capacitance_matrix_of_nets)
68 """
69 self.pex_context = pex_context
70 self.tech_info = tech_info
71 self.substrate_net_name = substrate_net_name
72 self.k_void = k_void
73 self.delaunay_amax = delaunay_amax
74 self.delaunay_b = delaunay_b
75 self.exclusions = exclusions
76 self.conductor_parts_by_layer: Dict[str, kdb.Region] = defaultdict(kdb.Region)
78 @cached_property
79 def dbu(self) -> float:
80 return self.pex_context.dbu
82 def gds_pair(self, layer_name) -> Optional[GDSPair]:
83 gds_pair = self.tech_info.gds_pair_for_computed_layer_name.get(layer_name, None)
84 if not gds_pair:
85 gds_pair = self.tech_info.gds_pair_for_layer_name.get(layer_name, None)
86 if not gds_pair:
87 warning(f"Can't find GDS pair for layer {layer_name}")
88 return None
89 return gds_pair
91 # NOTE: layers of the process stack can share a GDS pair (e.g. a metal without and with a MIM cap above),
92 # so each of them has the shapes of its own LVS layers only
93 def shapes_of_net(self, layer_name: str, net: kdb.Net) -> Optional[kdb.Region]:
94 gds_pair = self.gds_pair(layer_name=layer_name)
95 if not gds_pair:
96 return None
98 lvs_layer_names = self.tech_info.lvs_layer_names_by_process_layer_name.get(layer_name, [])
99 shapes = self.pex_context.shapes_of_net(gds_pair=gds_pair, net=net, lvs_layer_names=lvs_layer_names)
100 if not shapes:
101 debug(f"Nothing extracted for layer {layer_name}")
102 return shapes
104 def shapes_of_layer(self, layer_name: str) -> Optional[kdb.Region]:
105 gds_pair = self.gds_pair(layer_name=layer_name)
106 if not gds_pair:
107 return None
109 lvs_layer_names = self.tech_info.lvs_layer_names_by_process_layer_name.get(layer_name, [])
110 shapes = self.pex_context.shapes_of_layer(gds_pair=gds_pair, lvs_layer_names=lvs_layer_names)
111 if not shapes:
112 debug(f"Nothing extracted for layer {layer_name}")
113 return shapes
115 def top_cell_bbox(self) -> kdb.Box:
116 return self.pex_context.top_cell_bbox()
118 def canonical_layer_name(self, layer_name: str) -> LayerName:
119 """
120 :return: the name of the layer of the stack by the one of the process stack, the masks go by these
121 """
122 return self.tech_info.canonical_layer_name_by_gds_pair.get(self.gds_pair(layer_name=layer_name), layer_name)
124 def add_conductor(self,
125 model_builder: FasterCapModelBuilder,
126 net_name: str,
127 layer_name: str,
128 shapes: kdb.Region,
129 z: float,
130 height: float):
131 """
132 :param layer_name: the layer of the process stack
133 """
134 if self.exclusions is None:
135 model_builder.add_conductor(net_name=net_name, layer=shapes, z=z, height=height)
136 return
137 for part, conductor_name in self.exclusions.conductor_parts_of_shapes(shapes, net_name,
138 self.canonical_layer_name(layer_name)):
139 model_builder.add_conductor(net_name=conductor_name, layer=part, z=z, height=height, group_name=net_name)
140 self.conductor_parts_by_layer[layer_name] += part
142 def add_via(self,
143 model_builder: FasterCapModelBuilder,
144 net_name: str,
145 layer_below_name: str,
146 contact: Any,
147 shapes: kdb.Region,
148 z: float):
149 """
150 :param layer_below_name: the layer of the process stack that has the contact above it
151 :param contact: the contact, of the process stack
152 """
153 if self.exclusions is None:
154 model_builder.add_conductor(net_name=net_name, layer=shapes, z=z, height=contact.thickness)
155 return
156 for part, conductor_name in self.exclusions.conductor_parts_of_vias(
157 shapes, net_name,
158 self.canonical_layer_name(layer_below_name),
159 self.canonical_layer_name(contact.metal_above or layer_below_name)):
160 model_builder.add_conductor(net_name=conductor_name, layer=part, z=z, height=contact.thickness,
161 group_name=net_name)
163 def build(self) -> FasterCapModelGenerator:
164 lvsdb = self.pex_context.lvsdb
165 netlist: kdb.Netlist = lvsdb.netlist()
167 def format_terminal(t: kdb.NetTerminalRef) -> str:
168 td = t.terminal_def()
169 d = t.device()
170 return f"{d.expanded_name()}/{td.name}/{td.description}"
172 model_builder = FasterCapModelBuilder(
173 dbu=self.dbu,
174 k_void=self.k_void,
175 delaunay_amax=self.delaunay_amax, # test/compare with smaller, e.g. 0.05 => more triangles
176 delaunay_b=self.delaunay_b # test/compare with 1.0 => more triangles at edges
177 )
179 fox_layer = self.tech_info.field_oxide_layer
181 model_builder.add_material(name=fox_layer.name, k=fox_layer.field_oxide_layer.dielectric_k)
182 for diel_name, diel_k in self.tech_info.dielectric_by_name.items():
183 model_builder.add_material(name=diel_name, k=diel_k)
185 circuit = netlist.circuit_by_name(self.pex_context.annotated_top_cell.name)
186 # https://www.klayout.de/doc-qt5/code/class_Circuit.html
187 if not circuit:
188 circuits = [c.name for c in netlist.each_circuit()]
189 raise Exception(f"Expected circuit called {self.pex_context.annotated_top_cell.name} in extracted netlist, "
190 f"only available circuits are: {circuits}")
192 diffusion_regions: List[kdb.Region] = []
194 for net in circuit.each_net():
195 # https://www.klayout.de/doc-qt5/code/class_Net.html
196 debug(f"Net name={net.name}, expanded_name={net.expanded_name()}, pin_count={net.pin_count()}, "
197 f"is_floating={net.is_floating()}, is_passive={net.is_passive()}, "
198 f"terminals={list(map(lambda t: format_terminal(t), net.each_terminal()))}")
200 net_name = net.expanded_name()
202 for metal_layer in self.tech_info.process_metal_layers:
203 metal_layer_name = metal_layer.name
204 metal_layer = metal_layer.metal_layer
206 metal_z_bottom = metal_layer.z
207 metal_z_top = metal_z_bottom + metal_layer.thickness
209 shapes = self.shapes_of_net(layer_name=metal_layer_name, net=net)
210 if shapes:
211 if shapes.count() >= 1:
212 info(f"Conductor {net_name}, metal {metal_layer_name}, "
213 f"z={metal_layer.z}, height={metal_layer.thickness}")
214 self.add_conductor(model_builder,
215 net_name=net_name,
216 layer_name=metal_layer_name,
217 shapes=shapes,
218 z=metal_layer.z,
219 height=metal_layer.thickness)
221 if metal_layer.HasField('contact_above'):
222 contact = metal_layer.contact_above
223 shapes = self.shapes_of_net(layer_name=contact.name, net=net)
224 if shapes and not shapes.is_empty():
225 info(f"Conductor {net_name}, via {contact.name}, "
226 f"z={metal_z_top}, height={contact.thickness}")
227 self.add_via(model_builder,
228 net_name=net_name,
229 layer_below_name=metal_layer_name,
230 contact=contact,
231 shapes=shapes,
232 z=metal_z_top)
234 # diel_above = self.tech_info.process_stack_layer_by_name.get(metal_layer.reference_above, None)
235 # if diel_above:
236 # #model_builder.add_dielectric(material_name=metal_layer.reference_above,
237 # # layer=kdb.Region().)
238 # pass
239 # TODO: add stuff
241 # DIFF / TAP
242 for diffusion_layer in self.tech_info.process_diffusion_layers:
243 diffusion_layer_name = diffusion_layer.name
244 diffusion_layer = diffusion_layer.diffusion_layer
245 shapes = self.shapes_of_net(layer_name=diffusion_layer_name, net=net)
246 if shapes and not shapes.is_empty():
247 diffusion_regions.append(shapes)
248 info(f"Diffusion {net_name}, layer {diffusion_layer_name}, "
249 f"z={0}, height={0.1}")
250 self.add_conductor(model_builder,
251 net_name=net_name,
252 layer_name=diffusion_layer_name,
253 shapes=shapes,
254 z=0, # TODO
255 height=0.1) # TODO: diffusion_layer.z
257 contact = diffusion_layer.contact_above
258 shapes = self.shapes_of_net(layer_name=contact.name, net=net)
259 if shapes and not shapes.is_empty():
260 info(f"Diffusion {net_name}, contact {contact.name}, "
261 f"z={0}, height={contact.thickness}")
262 self.add_via(model_builder,
263 net_name=net_name,
264 layer_below_name=diffusion_layer_name,
265 contact=contact,
266 shapes=shapes,
267 z=0.0)
269 enlarged_top_cell_bbox = self.top_cell_bbox().enlarged(math.floor(8 / self.dbu)) # 8µm fringe halo
271 #
272 # global substrate block below everything, a conductor of the substrate net
273 #
275 substrate_layer = self.tech_info.process_substrate_layer.substrate_layer
276 substrate_region = kdb.Region()
278 substrate_block = enlarged_top_cell_bbox.dup()
279 substrate_region.insert(substrate_block)
281 diffusion_margin = math.floor(1 / self.dbu) # 1 µm
282 for d in diffusion_regions:
283 substrate_region -= d.sized(diffusion_margin)
284 info(f"Substrate {self.substrate_net_name}, "
285 f"z={0 - substrate_layer.height - substrate_layer.thickness}, height={substrate_layer.thickness}")
286 # NOTE: with exclusions, the substrate is apart from the metal of its net (e.g. VSS),
287 # which can be within a block
288 substrate_conductor_name = self.substrate_net_name
289 if self.exclusions is not None:
290 substrate_conductor_name = self.exclusions.conductor_name(ConductorPart(self.substrate_net_name,
291 is_substrate=True))
292 model_builder.add_conductor(net_name=substrate_conductor_name,
293 layer=substrate_region,
294 z=0 - substrate_layer.height - substrate_layer.thickness,
295 height=substrate_layer.thickness,
296 group_name=self.substrate_net_name)
298 #
299 # add dielectrics
300 #
302 fox_region = kdb.Region()
303 fox_block = enlarged_top_cell_bbox.dup()
304 fox_region.insert(fox_block)
306 # field oxide goes from substrate/diff/well up to below the gate-poly
307 gate_poly_height = self.tech_info.gate_poly_layer.metal_layer.z
308 fox_z = 0
309 fox_height = gate_poly_height - fox_z
310 info(f"Simple dielectric (field oxide) {fox_layer.name}: "
311 f"z={fox_z}, height={fox_height}")
312 model_builder.add_dielectric(material_name=fox_layer.name,
313 layer=fox_region,
314 z=fox_z,
315 height=fox_height)
317 for metal_layer in self.tech_info.process_metal_layers:
318 metal_layer_name = metal_layer.name
319 metal_layer = metal_layer.metal_layer
321 metal_z_bottom = metal_layer.z
323 # NOTE: as the conductors have them with exclusions, any-angle edges have the cuts as vertices then,
324 # which would leave slivers between these conductors and dielectrics of the shapes as they are
325 extracted_shapes = self.shapes_of_layer(layer_name=metal_layer_name) if self.exclusions is None \
326 else self.conductor_parts_by_layer.get(metal_layer_name, None)
328 sidewall_region: Optional[kdb.Region] = None
329 sidewall_height = 0
331 no_metal_region: Optional[kdb.Region] = None
332 no_metal_height = 0
334 #
335 # add the conformal dielectric films anchored on this metal
336 #
337 if extracted_shapes:
338 # Films grow incrementally along the chain: each one is measured from
339 # the surface of the film it wraps, so the metal's own thickness is
340 # added once, by the first link, and not again by the ones above it.
341 sidewall_height = metal_layer.thickness
342 sidewall_region = extracted_shapes
343 wrapped = metal_layer_name
345 while True:
346 film = self.tech_info.conformal_dielectric_wrapping(wrapped)
347 if not film:
348 break
350 conf_diel = film.conformal_dielectric_layer
351 d = math.floor(conf_diel.thickness_sidewall / self.dbu)
352 sidewall_region = sidewall_region.sized(d)
353 sidewall_height += conf_diel.thickness_over_metal
354 info(f"Conformal dielectric (sidewall) {film.name}: "
355 f"z={metal_layer.z}, height={sidewall_height}")
356 model_builder.add_dielectric(material_name=film.name,
357 layer=sidewall_region,
358 z=metal_layer.z,
359 height=sidewall_height)
360 if conf_diel.thickness_where_no_metal > 0.0:
361 no_metal_block = enlarged_top_cell_bbox.dup()
362 no_metal_region = kdb.Region()
363 no_metal_region.insert(no_metal_block)
364 no_metal_region -= sidewall_region
365 no_metal_height = conf_diel.thickness_where_no_metal
366 info(f"Conformal dielectric (where no metal) {film.name}: "
367 f"z={metal_layer.z}, height={no_metal_height}")
368 model_builder.add_dielectric(material_name=film.name,
369 layer=no_metal_region,
370 z=metal_layer.z,
371 height=no_metal_height)
373 wrapped = film.name
375 #
376 # add simple dielectric
377 #
378 simple_dielectric, diel_height = self.tech_info.simple_dielectric_above_metal(metal_layer_name)
379 if simple_dielectric:
380 diel_block = enlarged_top_cell_bbox.dup()
381 diel_region = kdb.Region()
382 diel_region.insert(diel_block)
383 if sidewall_region:
384 assert sidewall_height >= 0.0
385 diel_region -= sidewall_region
386 info(f"Simple dielectric (sidewall) {simple_dielectric.name}: "
387 f"z={metal_z_bottom + sidewall_height}, height={diel_height - sidewall_height}")
388 model_builder.add_dielectric(material_name=simple_dielectric.name,
389 layer=sidewall_region,
390 z=metal_z_bottom + sidewall_height,
391 height=diel_height - sidewall_height)
392 if no_metal_region:
393 info(f"Simple dielectric (no metal) {simple_dielectric.name}: "
394 f"z={metal_z_bottom + no_metal_height}, height={diel_height - no_metal_height}")
395 model_builder.add_dielectric(material_name=simple_dielectric.name,
396 layer=diel_region,
397 z=metal_z_bottom + no_metal_height,
398 height=diel_height - no_metal_height)
399 else:
400 info(f"Simple dielectric {simple_dielectric.name}: "
401 f"z={metal_z_bottom}, height={diel_height}")
402 model_builder.add_dielectric(material_name=simple_dielectric.name,
403 layer=diel_region,
404 z=metal_z_bottom,
405 height=diel_height)
407 gen = model_builder.generate()
408 return gen