Coverage for klayout_pex/rcx25/c/sidewall_and_fringe_extractor.py: 95%
195 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#
26from functools import cached_property
27import math
29import klayout.db as kdb
31from klayout_pex.log import (
32 debug,
33 info,
34 warning,
35 get_log_level,
36 LogLevel
37)
38from klayout_pex.tech_info import TechInfo
40from klayout_pex.rcx25.c.device_capacitor_plates import DeviceCapacitorPlates
41from klayout_pex.rcx25.c.geometry_restorer import GeometryRestorer
42from klayout_pex.rcx25.extraction_results import *
43from klayout_pex.rcx25.extraction_reporter import ExtractionReporter
44from klayout_pex.rcx25.c.polygon_utils import find_polygon_with_nearest_edge, nearest_edge
45from klayout_pex.rcx25.types import EdgeInterval, EdgeNeighborhood
46from klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 import CapacitanceInfo
49class SidewallAndFringeExtractor:
50 def __init__(self,
51 all_layer_names: List[LayerName],
52 layer_regions_by_name: Dict[LayerName, kdb.Region],
53 dbu: float,
54 scale_ratio_to_fit_halo: bool,
55 tech_info: TechInfo,
56 device_capacitor_plates: DeviceCapacitorPlates,
57 results: CellExtractionResults,
58 report: ExtractionReporter):
59 self.all_layer_names = all_layer_names
60 self.layer_regions_by_name = layer_regions_by_name
61 self.dbu = dbu
62 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo
63 self.tech_info = tech_info
64 self.device_capacitor_plates = device_capacitor_plates
65 self.results = results
66 self.report = report
68 self.all_layer_regions = layer_regions_by_name.values()
70 def extract(self):
71 for idx, (layer_name, layer_region) in enumerate(self.layer_regions_by_name.items()):
72 # NOTE: the substrate (with its wells) and the diffusion are the bottom plates of the capacitances
73 # of the layers above, whose fringe ends at them, while their own edges have none
74 # (like in the magic techs): those between them are the devices' (e.g. the junctions)
75 if layer_name == self.tech_info.internal_substrate_layer_name or \
76 layer_name in self.tech_info.process_diffusion_layer_names:
77 continue
79 other_layer_regions = [
80 r for ln, r in self.layer_regions_by_name.items()
81 if ln != layer_name
82 ]
84 en_visitor = self.PEXEdgeNeighborhoodVisitor(
85 all_layer_names=self.all_layer_names,
86 inside_layer_index=idx,
87 dbu=self.dbu,
88 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo,
89 tech_info=self.tech_info,
90 device_capacitor_plates=self.device_capacitor_plates,
91 results=self.results,
92 report=self.report
93 )
95 en_children = [kdb.CompoundRegionOperationNode.new_secondary(r)
96 for r in self.all_layer_regions]
97 en_children[idx] = kdb.CompoundRegionOperationNode.new_foreign() # sidewall of other nets on the same layer
98 en_children.append(kdb.CompoundRegionOperationNode.new_primary()) # opposing structures of the same polygon
100 side_halo_um = self.tech_info.tech.process_parasitics.side_halo
101 side_halo_dbu = int(side_halo_um / self.dbu) + 1 # add 1 nm to halo
103 en_node = kdb.CompoundRegionOperationNode.new_edge_neighborhood(
104 children=en_children,
105 visitor=en_visitor,
106 bext=-1, # NOTE: -1 dbu, suppresses quasi-empty contributions (will also suppress 90° edges)
107 eext=-1, # NOTE: -1 dbu, suppresses quasi-empty contributions (will also suppress 90° edges)
108 din=-1, # NOTE: -1 dbu, suppresses the edge itself appearing as a pseudo-polygon in new_primary()
109 dout=side_halo_dbu # dout
110 )
112 layer_region.complex_op(en_node)
114 # ------------------------------------------------------------------------
116 class PEXEdgeNeighborhoodVisitor(kdb.EdgeNeighborhoodVisitor):
117 def __init__(self,
118 all_layer_names: List[LayerName],
119 inside_layer_index: int,
120 dbu: float,
121 tech_info: TechInfo,
122 scale_ratio_to_fit_halo: bool,
123 device_capacitor_plates: DeviceCapacitorPlates,
124 results: CellExtractionResults,
125 report: ExtractionReporter):
126 super().__init__()
128 self.all_layer_names = all_layer_names
129 self.inside_layer_index = inside_layer_index
130 self.dbu = dbu
131 self.tech_info = tech_info
132 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo
133 self.device_capacitor_plates = device_capacitor_plates
134 self.results = results
135 self.report = report
137 # NOTE: prepare layers below and layers above the "inside" layer,
138 # each prepared for iteration that allows iterativly growing a shield region
139 self.layer_below_indices = reversed(range(0, inside_layer_index))
140 self.layer_above_indices = range(inside_layer_index,
141 len(all_layer_names) - inside_layer_index)
143 @cached_property
144 def inside_layer_name(self) -> LayerName:
145 return self.all_layer_names[self.inside_layer_index]
147 def begin_polygon(self,
148 layout: kdb.Layout,
149 cell: kdb.Cell,
150 polygon: kdb.Polygon):
151 pass
153 def end_polygon(self):
154 pass
156 @cached_property
157 def side_halo(self) -> float:
158 return self.tech_info.tech.process_parasitics.side_halo
160 def on_edge(self,
161 layout: kdb.Layout,
162 cell: kdb.Cell,
163 edge: kdb.EdgeWithProperties,
164 neighborhood: EdgeNeighborhood):
165 #
166 # NOTE: this complex operation will automatically rotate every edge to be on the x-axis
167 # going from 0 to edge.length
168 # so we only have to consider the y-axis to get the near and far distances
169 #
170 geometry_restorer = GeometryRestorer(self.to_original_trans(edge))
172 if get_log_level() == LogLevel.DEBUG:
173 self.report.output_edge_neighborhood(inside_layer=self.inside_layer_name,
174 all_layer_names=self.all_layer_names,
175 edge=edge,
176 neighborhood=neighborhood,
177 geometry_restorer=geometry_restorer)
179 for edge_interval, polygons_by_child in neighborhood:
180 if not polygons_by_child:
181 continue
183 edge_interval_length = edge_interval[1] - edge_interval[0]
184 if edge_interval_length <= 1:
185 # NOTE: expected, so no warning, which would train to ignore warnings
186 debug(f"Short edge interval {edge_interval} "
187 f"(length {edge_interval_length * self.dbu * 1000} nm), "
188 f"expected to be dropped due to bext/eext parameters, skipping…")
189 continue
191 layer_fringe_shields = [kdb.Region() for _ in self.all_layer_names]
192 for child_index, polygons in polygons_by_child.items():
193 if child_index < len(self.all_layer_names):
194 layer_fringe_shields[child_index].insert(polygons)
196 # NOTE: lateral fringe shielding, can be caused by
197 # - sidewall (other net)
198 # - same net "sidewall" (other polygons)
199 # - even opposing edges of the same polygon of the same net!
200 # fringe to shapes on other layers will be limited by this distance
201 # (i.e., fringe is shielded beyond this distance)
203 nearest_distance: Optional[float] = None
204 nearest_lateral_edge: Optional[kdb.EdgeWithProperties] = None
206 for child_index, polygons in polygons_by_child.items():
207 if child_index == len(self.all_layer_names): # TODO, fix index, same layer, same polygon
208 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons)
210 if nearest_distance is None or \
211 distance < nearest_distance:
212 nearest_distance = distance
213 nearest_lateral_edge = nearest_edge(nearby_polygon)
214 elif self.inside_layer_index == child_index: # SIDEWALL!
215 # NOTE: use only the nearest polygon,
216 # as the others are laterally shielded by the nearer ones
217 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons)
219 if nearest_distance is None or \
220 distance < nearest_distance:
221 nearest_distance = distance
222 nearest_lateral_edge = nearest_edge(nearby_polygon)
224 self.emit_sidewall(
225 layer_name=self.inside_layer_name,
226 edge=edge,
227 edge_interval=edge_interval,
228 polygon=nearby_polygon,
229 geometry_restorer=geometry_restorer
230 )
232 lateral_shield: Optional[kdb.Polygon] = None
233 if nearest_lateral_edge is not None:
234 lateral_shield = kdb.Polygon([
235 nearest_lateral_edge.p2,
236 nearest_lateral_edge.p1,
237 kdb.Point(nearest_lateral_edge.p1.x, (self.side_halo + 10) / self.dbu),
238 kdb.Point(nearest_lateral_edge.p2.x, (self.side_halo + 10) / self.dbu),
239 ])
241 for child_index, polygons in polygons_by_child.items():
242 if self.inside_layer_index == child_index:
243 continue # already handled above
244 elif child_index < len(self.all_layer_names): # FRINGE!
245 fringe_shield = kdb.Region()
246 if lateral_shield is not None:
247 fringe_shield.insert(lateral_shield)
248 if child_index < self.inside_layer_index:
249 r = range(child_index + 1, self.inside_layer_index)
250 for idx in r:
251 fringe_shield += layer_fringe_shields[idx]
252 elif self.inside_layer_index < child_index:
253 r = range(self.inside_layer_index + 1, child_index)
254 for idx in r:
255 fringe_shield += layer_fringe_shields[idx]
257 # NOTE:
258 # polygons can have different nets
259 # polygons can be segmented after shield is applied
261 self.emit_fringe(
262 inside_layer_name=self.inside_layer_name,
263 outside_layer_name=self.all_layer_names[child_index],
264 edge=edge,
265 edge_interval=edge_interval,
266 outside_polygons=polygons,
267 shield=fringe_shield,
268 lateral_shield=lateral_shield,
269 geometry_restorer=geometry_restorer)
271 def emit_sidewall(self,
272 layer_name: LayerName,
273 edge: kdb.EdgeWithProperties,
274 edge_interval: EdgeInterval,
275 polygon: kdb.PolygonWithProperties,
276 geometry_restorer: GeometryRestorer):
277 net1 = edge.property('net')
278 net2 = polygon.property('net')
280 if net1 == net2:
281 return
283 # NOTE: the model of a device has the capacitance between its plates
284 if self.device_capacitor_plates.are_plates_of_a_device(edge, net1, polygon, net2):
285 return
287 sidewall_cap_spec = self.tech_info.sidewall_cap_by_layer_name[layer_name]
289 # TODO!
291 # NOTE: this method is always called for a single nearest edge (line), so the
292 # polygons have 4 points.
293 # Polygons points are sorted clockwise, so the edge
294 # that goes from right-to-left is the nearest edge
295 # nearby_opposing_edge = [e for e in nearest_lateral_shape[1].each_edge() if e.d().x < 0][-1]
296 # nearby_opposing_edge_trans = geometry_restorer.restore_edge(edge) * nearby_opposing_edge
298 # C = Csidewall * l * t / s
299 # C = Csidewall * l / s
301 avg_length = edge_interval[1] - edge_interval[0]
302 avg_distance = min(polygon.bbox().p1.y, polygon.bbox().p2.y)
304 outside_edge = nearest_edge(polygon)
306 length_um = avg_length * self.dbu
307 distance_um = avg_distance * self.dbu
309 # NOTE: dividing by 2 (like MAGIC this not bidirectional),
310 # but we count 2 sidewall contributions (one for each side of the cap).
311 # The second half accounts for the tech file coefficient itself:
312 # it holds the full value of the field solver, while a
313 # per-edge value has to be half of that. MAGIC counted the
314 # full value on every edge until 8.3.679, which redefined the
315 # tech file value rather than changing every PDK — the
316 # coefficients stay as the PDK states them, and the factor
317 # lives here, see
318 # https://github.com/martinjankoehler/magic/issues/6#issuecomment-5371056429
319 cap_femto = ((length_um * sidewall_cap_spec.capacitance)
320 / (distance_um + sidewall_cap_spec.offset)
321 / 2.0 # non-bidirectional (half)
322 / 2.0 # the coefficient is the value for both edges
323 / 1000.0) # aF -> fF
325 # info(f"(Sidewall) layer {layer_name}: Nets {net1} <-> {net2}: {round(cap_femto, 5)} fF")
327 swk = SidewallKey(layer=layer_name, net1=net1, net2=net2)
328 sw_cap = SidewallCap(key=swk,
329 cap_value=cap_femto,
330 distance=distance_um,
331 length=length_um,
332 tech_spec=sidewall_cap_spec)
333 self.results.add_sidewall_cap(sw_cap)
335 self.report.output_sidewall(
336 sidewall_cap=sw_cap,
337 inside_edge=geometry_restorer.restore_edge_interval(edge_interval),
338 outside_edge=geometry_restorer.restore_edge(outside_edge)
339 )
341 def fringe_cap(self,
342 edge_interval_length: float,
343 distance_near: float,
344 distance_far: float,
345 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance,
346 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float:
347 distance_near_um = distance_near * self.dbu
348 distance_far_um = distance_far * self.dbu
349 edge_interval_length_um = edge_interval_length * self.dbu
351 # NOTE: overlap scaling is 1/50 (see MAGIC ExtTech)
352 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0
353 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor
355 # see Magic ExtCouple.c L1164
356 cnear = (2.0 / math.pi) * math.atan(alpha_c * distance_near_um)
357 cfar = (2.0 / math.pi) * math.atan(alpha_c * distance_far_um)
359 if self.scale_ratio_to_fit_halo:
360 full_halo_ratio = (2.0 / math.pi) * math.atan(alpha_c * self.side_halo)
361 # NOTE: for a large enough halo, full_halo would be 1,
362 # but it is smaller, so we compensate
363 if full_halo_ratio < 1.0:
364 cnear /= full_halo_ratio
365 cfar /= full_halo_ratio
367 # "cfrac" is the fractional portion of the fringe cap seen
368 # by tile tp along its length. This is independent of the
369 # portion of the boundary length that tile tp occupies.
370 cfrac = cfar - cnear
372 cap_femto = (cfrac * edge_interval_length_um *
373 sideoverlap_cap_spec.capacitance / 1000.0)
375 return cap_femto
377 def emit_fringe(self,
378 inside_layer_name: LayerName,
379 outside_layer_name: LayerName,
380 edge: kdb.EdgeWithProperties,
381 edge_interval: EdgeInterval,
382 outside_polygons: List[kdb.PolygonWithProperties],
383 shield: kdb.Region,
384 lateral_shield: kdb.Polygon,
385 geometry_restorer: GeometryRestorer):
386 # NOTE: e.g. the fringe of the gate poly to the source/drain, which the transistor model has
387 if self.tech_info.is_device_capacitance(inside_layer_name, outside_layer_name):
388 return
390 inside_net_name = edge.property('net')
392 # NOTE: each polygon in outside_polygons
393 # - could have a different net (e.g. the wells of the substrate and the rest, VSUBS)
394 # - could be segmented by a shield into multiple polygons
395 # each with different near/far regions
397 outside_net_names = [p.property('net') for p in outside_polygons]
399 # NOTE: the model of a device has the capacitance between its plates, so they are like one net
400 same_net_markers = [
401 inside_net_name == outside_net_name or
402 self.device_capacitor_plates.are_plates_of_a_device(edge, inside_net_name, p, outside_net_name)
403 for p, outside_net_name in zip(outside_polygons, outside_net_names)
404 ]
406 # NOTE: overlap_cap_by_layer_names is top/bot (dict is not symmetric)
407 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[inside_layer_name].get(outside_layer_name,
408 None)
409 if not overlap_cap_spec:
410 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[outside_layer_name][inside_layer_name]
412 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name]
413 sideoverlap_cap_spec = self.tech_info.side_overlap_cap_by_layer_names[inside_layer_name][
414 outside_layer_name]
416 polygons_by_net: Dict[NetName, List[kdb.PolygonWithProperties]] = defaultdict(list)
418 for idx, p in enumerate(outside_polygons):
419 outside_net = outside_net_names[idx]
420 is_same_net = same_net_markers[idx]
422 if is_same_net:
423 # TODO: log?
424 continue
426 if shield.is_empty():
427 polygons_by_net[outside_net].append(p)
428 else:
429 unshielded_region = kdb.Region(p)
430 unshielded_region.enable_properties()
431 unshielded_region -= shield
432 if unshielded_region.is_empty():
433 # TODO: log?
434 continue
436 for up in unshielded_region.each():
437 up = kdb.PolygonWithProperties(up, {'net': outside_net})
438 polygons_by_net[outside_net].append(up)
439 # if p != up:
440 # print(f"Unshieleded polygon {up}, differs from original polygon {p}")
442 for outside_net_name, polygons in polygons_by_net.items():
443 for p in polygons:
444 bbox = p.bbox()
445 if not p.is_box():
446 warning(f"Side overlap, polygon {p} is not a box. "
447 f"Currently, only boxes are supported, will be using bounding box {bbox}")
449 distance_near = bbox.p1.y # + 1
450 if distance_near < 0:
451 distance_near = 0
452 distance_far = bbox.p2.y # - 2
453 if distance_far < 0:
454 distance_far = 0
455 try:
456 assert distance_near >= 0
457 assert distance_far >= distance_near
458 except AssertionError:
459 print()
460 raise
462 if distance_far == distance_near:
463 return
465 edge_interval_length = edge_interval[1] - edge_interval[0]
466 edge_interval_length_um = edge_interval_length * self.dbu
468 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length,
469 distance_near=distance_near,
470 distance_far=distance_far,
471 overlap_cap_spec=overlap_cap_spec,
472 sideoverlap_cap_spec=sideoverlap_cap_spec)
474 if cap_femto > 0.0001: # TODO: configurable threshold, but keeping accumulation might also be nice
475 # info(f"(Side Overlap) "
476 # f"{inside_layer_name}({inside_net_name})-{outside_layer_name}({outside_net_name}): "
477 # f"{round(cap_femto, 5)} fF, "
478 # f"edge interval length = {round(edge_interval_length_um, 2)} µm")
480 sok = SideOverlapKey(layer_inside=inside_layer_name,
481 net_inside=inside_net_name,
482 layer_outside=outside_layer_name,
483 net_outside=outside_net_name)
484 soc = SideOverlapCap(key=sok, cap_value=cap_femto)
485 self.results.add_sideoverlap_cap(soc)
487 self.report.output_sideoverlap(
488 sideoverlap_cap=soc,
489 inside_edge=geometry_restorer.restore_edge_interval(edge_interval),
490 outside_polygon=geometry_restorer.restore_polygon(p),
491 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \
492 if lateral_shield is not None else None
493 )