Coverage for klayout_pex/rcx25/c/sidewall_and_fringe_extractor.py: 96%
213 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#
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 mos_channels: kdb.Region,
58 results: CellExtractionResults,
59 report: ExtractionReporter):
60 """
61 :param mos_channels: the channels of the transistors, whose gate edges along them have no fringe
62 to the substrate and wells (see emit_fringe)
63 """
64 self.all_layer_names = all_layer_names
65 self.layer_regions_by_name = layer_regions_by_name
66 self.dbu = dbu
67 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo
68 self.tech_info = tech_info
69 self.device_capacitor_plates = device_capacitor_plates
70 self.results = results
71 self.report = report
73 # NOTE: found by their boxes, as each gate edge looks them up
74 self.mos_channels = kdb.Shapes()
75 for channel in mos_channels.each():
76 self.mos_channels.insert(channel)
78 self.all_layer_regions = layer_regions_by_name.values()
80 def extract(self):
81 for idx, (layer_name, layer_region) in enumerate(self.layer_regions_by_name.items()):
82 # NOTE: the substrate (with its wells) and the diffusion are the bottom plates of the capacitances
83 # of the layers above, whose fringe ends at them, while their own edges have none
84 # (like in the magic techs): those between them are the devices' (e.g. the junctions)
85 if layer_name == self.tech_info.internal_substrate_layer_name or \
86 layer_name in self.tech_info.process_diffusion_layer_names:
87 continue
89 other_layer_regions = [
90 r for ln, r in self.layer_regions_by_name.items()
91 if ln != layer_name
92 ]
94 en_visitor = self.PEXEdgeNeighborhoodVisitor(
95 all_layer_names=self.all_layer_names,
96 inside_layer_index=idx,
97 dbu=self.dbu,
98 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo,
99 tech_info=self.tech_info,
100 device_capacitor_plates=self.device_capacitor_plates,
101 mos_channels=self.mos_channels,
102 results=self.results,
103 report=self.report
104 )
106 en_children = [kdb.CompoundRegionOperationNode.new_secondary(r)
107 for r in self.all_layer_regions]
108 en_children[idx] = kdb.CompoundRegionOperationNode.new_foreign() # sidewall of other nets on the same layer
109 en_children.append(kdb.CompoundRegionOperationNode.new_primary()) # opposing structures of the same polygon
111 side_halo_um = self.tech_info.tech.process_parasitics.side_halo
112 side_halo_dbu = int(side_halo_um / self.dbu) + 1 # add 1 nm to halo
114 en_node = kdb.CompoundRegionOperationNode.new_edge_neighborhood(
115 children=en_children,
116 visitor=en_visitor,
117 bext=-1, # NOTE: -1 dbu, suppresses quasi-empty contributions (will also suppress 90° edges)
118 eext=-1, # NOTE: -1 dbu, suppresses quasi-empty contributions (will also suppress 90° edges)
119 din=-1, # NOTE: -1 dbu, suppresses the edge itself appearing as a pseudo-polygon in new_primary()
120 dout=side_halo_dbu # dout
121 )
123 layer_region.complex_op(en_node)
125 # ------------------------------------------------------------------------
127 class PEXEdgeNeighborhoodVisitor(kdb.EdgeNeighborhoodVisitor):
128 def __init__(self,
129 all_layer_names: List[LayerName],
130 inside_layer_index: int,
131 dbu: float,
132 tech_info: TechInfo,
133 scale_ratio_to_fit_halo: bool,
134 device_capacitor_plates: DeviceCapacitorPlates,
135 mos_channels: kdb.Shapes,
136 results: CellExtractionResults,
137 report: ExtractionReporter):
138 super().__init__()
140 self.all_layer_names = all_layer_names
141 self.inside_layer_index = inside_layer_index
142 self.dbu = dbu
143 self.tech_info = tech_info
144 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo
145 self.device_capacitor_plates = device_capacitor_plates
146 self.mos_channels = mos_channels
147 self.results = results
148 self.report = report
150 # NOTE: prepare layers below and layers above the "inside" layer,
151 # each prepared for iteration that allows iterativly growing a shield region
152 self.layer_below_indices = reversed(range(0, inside_layer_index))
153 self.layer_above_indices = range(inside_layer_index,
154 len(all_layer_names) - inside_layer_index)
156 @cached_property
157 def inside_layer_name(self) -> LayerName:
158 return self.all_layer_names[self.inside_layer_index]
160 def begin_polygon(self,
161 layout: kdb.Layout,
162 cell: kdb.Cell,
163 polygon: kdb.Polygon):
164 pass
166 def end_polygon(self):
167 pass
169 @cached_property
170 def side_halo(self) -> float:
171 return self.tech_info.tech.process_parasitics.side_halo
173 def on_edge(self,
174 layout: kdb.Layout,
175 cell: kdb.Cell,
176 edge: kdb.EdgeWithProperties,
177 neighborhood: EdgeNeighborhood):
178 #
179 # NOTE: this complex operation will automatically rotate every edge to be on the x-axis
180 # going from 0 to edge.length
181 # so we only have to consider the y-axis to get the near and far distances
182 #
183 geometry_restorer = GeometryRestorer(self.to_original_trans(edge))
185 if get_log_level() == LogLevel.DEBUG:
186 self.report.output_edge_neighborhood(inside_layer=self.inside_layer_name,
187 all_layer_names=self.all_layer_names,
188 edge=edge,
189 neighborhood=neighborhood,
190 geometry_restorer=geometry_restorer)
192 for edge_interval, polygons_by_child in neighborhood:
193 if not polygons_by_child:
194 continue
196 edge_interval_length = edge_interval[1] - edge_interval[0]
197 if edge_interval_length <= 1:
198 # NOTE: expected, so no warning, which would train to ignore warnings
199 debug(f"Short edge interval {edge_interval} "
200 f"(length {edge_interval_length * self.dbu * 1000} nm), "
201 f"expected to be dropped due to bext/eext parameters, skipping…")
202 continue
204 layer_fringe_shields = [kdb.Region() for _ in self.all_layer_names]
205 for child_index, polygons in polygons_by_child.items():
206 if child_index < len(self.all_layer_names):
207 layer_fringe_shields[child_index].insert(polygons)
209 # NOTE: lateral fringe shielding, can be caused by
210 # - sidewall (other net)
211 # - same net "sidewall" (other polygons)
212 # - even opposing edges of the same polygon of the same net!
213 # fringe to shapes on other layers will be limited by this distance
214 # (i.e., fringe is shielded beyond this distance)
216 nearest_distance: Optional[float] = None
217 nearest_lateral_edge: Optional[kdb.EdgeWithProperties] = None
218 nearest_of_same_polygon: Optional[Tuple[float, kdb.PolygonWithProperties]] = None
219 nearest_of_other_polygons: Optional[float] = None
221 for child_index, polygons in polygons_by_child.items():
222 if child_index == len(self.all_layer_names): # TODO, fix index, same layer, same polygon
223 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons)
225 if nearest_distance is None or \
226 distance < nearest_distance:
227 nearest_distance = distance
228 nearest_lateral_edge = nearest_edge(nearby_polygon)
230 nearest_of_same_polygon = distance, nearby_polygon
231 elif self.inside_layer_index == child_index: # SIDEWALL!
232 # NOTE: use only the nearest polygon,
233 # as the others are laterally shielded by the nearer ones
234 distance, nearby_polygon = find_polygon_with_nearest_edge(polygons_on_same_layer=polygons)
236 if nearest_distance is None or \
237 distance < nearest_distance:
238 nearest_distance = distance
239 nearest_lateral_edge = nearest_edge(nearby_polygon)
240 nearest_of_other_polygons = distance
242 self.emit_sidewall(
243 layer_name=self.inside_layer_name,
244 edge=edge,
245 edge_interval=edge_interval,
246 polygon=nearby_polygon,
247 geometry_restorer=geometry_restorer
248 )
250 # NOTE: parts of a polygon can be apart in the netlist, e.g. the leads of a coil,
251 # as far as no nearer polygon shields them from each other
252 if nearest_of_same_polygon is not None and self.results.has_net_pieces(edge.property('net')) and \
253 (nearest_of_other_polygons is None or nearest_of_same_polygon[0] < nearest_of_other_polygons):
254 self.emit_sidewall(
255 layer_name=self.inside_layer_name,
256 edge=edge,
257 edge_interval=edge_interval,
258 polygon=nearest_of_same_polygon[1],
259 geometry_restorer=geometry_restorer
260 )
262 lateral_shield: Optional[kdb.Polygon] = None
263 if nearest_lateral_edge is not None:
264 lateral_shield = kdb.Polygon([
265 nearest_lateral_edge.p2,
266 nearest_lateral_edge.p1,
267 kdb.Point(nearest_lateral_edge.p1.x, (self.side_halo + 10) / self.dbu),
268 kdb.Point(nearest_lateral_edge.p2.x, (self.side_halo + 10) / self.dbu),
269 ])
271 for child_index, polygons in polygons_by_child.items():
272 if self.inside_layer_index == child_index:
273 continue # already handled above
274 elif child_index < len(self.all_layer_names): # FRINGE!
275 fringe_shield = kdb.Region()
276 if lateral_shield is not None:
277 fringe_shield.insert(lateral_shield)
278 if child_index < self.inside_layer_index:
279 r = range(child_index + 1, self.inside_layer_index)
280 for idx in r:
281 fringe_shield += layer_fringe_shields[idx]
282 elif self.inside_layer_index < child_index:
283 r = range(self.inside_layer_index + 1, child_index)
284 for idx in r:
285 fringe_shield += layer_fringe_shields[idx]
287 # NOTE:
288 # polygons can have different nets
289 # polygons can be segmented after shield is applied
291 self.emit_fringe(
292 inside_layer_name=self.inside_layer_name,
293 outside_layer_name=self.all_layer_names[child_index],
294 edge=edge,
295 edge_interval=edge_interval,
296 outside_polygons=polygons,
297 shield=fringe_shield,
298 lateral_shield=lateral_shield,
299 geometry_restorer=geometry_restorer)
301 def emit_sidewall(self,
302 layer_name: LayerName,
303 edge: kdb.EdgeWithProperties,
304 edge_interval: EdgeInterval,
305 polygon: kdb.PolygonWithProperties,
306 geometry_restorer: GeometryRestorer):
307 net1 = edge.property('net')
308 net2 = polygon.property('net')
310 # NOTE: but for the parts of a net that are apart in the netlist
311 if net1 == net2 and not self.results.has_net_pieces(net1):
312 return
314 # NOTE: the model of a device has the capacitance between its plates
315 if self.device_capacitor_plates.are_plates_of_a_device(edge, net1, polygon, net2):
316 return
318 sidewall_cap_spec = self.tech_info.sidewall_cap_by_layer_name[layer_name]
320 # TODO!
322 # NOTE: this method is always called for a single nearest edge (line), so the
323 # polygons have 4 points.
324 # Polygons points are sorted clockwise, so the edge
325 # that goes from right-to-left is the nearest edge
326 # nearby_opposing_edge = [e for e in nearest_lateral_shape[1].each_edge() if e.d().x < 0][-1]
327 # nearby_opposing_edge_trans = geometry_restorer.restore_edge(edge) * nearby_opposing_edge
329 # C = Csidewall * l * t / s
330 # C = Csidewall * l / s
332 avg_length = edge_interval[1] - edge_interval[0]
333 avg_distance = min(polygon.bbox().p1.y, polygon.bbox().p2.y)
335 outside_edge = nearest_edge(polygon)
337 length_um = avg_length * self.dbu
338 distance_um = avg_distance * self.dbu
340 # NOTE: dividing by 2 (like MAGIC this not bidirectional),
341 # but we count 2 sidewall contributions (one for each side of the cap).
342 # The second half accounts for the tech file coefficient itself:
343 # it holds the full value of the field solver, while a
344 # per-edge value has to be half of that. MAGIC counted the
345 # full value on every edge until 8.3.679, which redefined the
346 # tech file value rather than changing every PDK — the
347 # coefficients stay as the PDK states them, and the factor
348 # lives here, see
349 # https://github.com/martinjankoehler/magic/issues/6#issuecomment-5371056429
350 cap_femto = ((length_um * sidewall_cap_spec.capacitance)
351 / (distance_um + sidewall_cap_spec.offset)
352 / 2.0 # non-bidirectional (half)
353 / 2.0 # the coefficient is the value for both edges
354 / 1000.0) # aF -> fF
356 # info(f"(Sidewall) layer {layer_name}: Nets {net1} <-> {net2}: {round(cap_femto, 5)} fF")
358 inside_edge = geometry_restorer.restore_edge_interval(edge_interval)
360 swk = SidewallKey(layer=layer_name, net1=net1, net2=net2)
361 sw_cap = SidewallCap(key=swk,
362 cap_value=cap_femto,
363 distance=distance_um,
364 length=length_um,
365 tech_spec=sidewall_cap_spec,
366 inside_edge=inside_edge,
367 # NOTE: the part of the other net facing the edge interval
368 outside_edge=geometry_restorer.restore_edge(
369 kdb.Edge(kdb.Point(edge_interval[0], avg_distance),
370 kdb.Point(edge_interval[1], avg_distance))
371 ))
372 self.results.add_sidewall_cap(sw_cap)
374 self.report.output_sidewall(
375 sidewall_cap=sw_cap,
376 inside_edge=inside_edge,
377 outside_edge=geometry_restorer.restore_edge(outside_edge)
378 )
380 def fringe_cap(self,
381 edge_interval_length: float,
382 distance_near: float,
383 distance_far: float,
384 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance,
385 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float:
386 distance_near_um = distance_near * self.dbu
387 distance_far_um = distance_far * self.dbu
388 edge_interval_length_um = edge_interval_length * self.dbu
390 # NOTE: overlap scaling is 1/50 (see MAGIC ExtTech)
391 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0
392 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor
394 # see Magic ExtCouple.c L1164
395 cnear = (2.0 / math.pi) * math.atan(alpha_c * distance_near_um)
396 cfar = (2.0 / math.pi) * math.atan(alpha_c * distance_far_um)
398 if self.scale_ratio_to_fit_halo:
399 full_halo_ratio = (2.0 / math.pi) * math.atan(alpha_c * self.side_halo)
400 # NOTE: for a large enough halo, full_halo would be 1,
401 # but it is smaller, so we compensate
402 if full_halo_ratio < 1.0:
403 cnear /= full_halo_ratio
404 cfar /= full_halo_ratio
406 # "cfrac" is the fractional portion of the fringe cap seen
407 # by tile tp along its length. This is independent of the
408 # portion of the boundary length that tile tp occupies.
409 cfrac = cfar - cnear
411 cap_femto = (cfrac * edge_interval_length_um *
412 sideoverlap_cap_spec.capacitance / 1000.0)
414 return cap_femto
416 def is_gate_edge(self, edge: kdb.Edge) -> bool:
417 """
418 Whether the edge runs along the channel of a transistor (its middle is on the channel's boundary)
419 """
420 middle = edge.bbox().center()
421 return any(shape.polygon.inside(middle)
422 for shape in self.mos_channels.each_touching(kdb.Shapes.SAll, kdb.Box(middle, middle)))
424 def emit_fringe(self,
425 inside_layer_name: LayerName,
426 outside_layer_name: LayerName,
427 edge: kdb.EdgeWithProperties,
428 edge_interval: EdgeInterval,
429 outside_polygons: List[kdb.PolygonWithProperties],
430 shield: kdb.Region,
431 lateral_shield: kdb.Polygon,
432 geometry_restorer: GeometryRestorer):
433 # NOTE: e.g. the fringe of the gate poly to the source/drain, which the transistor model has
434 if self.tech_info.is_device_capacitance(inside_layer_name, outside_layer_name):
435 return
437 # NOTE: and the fringe of a gate edge along the channel, over the source/drain,
438 # to the substrate or well beyond (#290)
439 if outside_layer_name == self.tech_info.internal_substrate_layer_name and \
440 self.is_gate_edge(geometry_restorer.restore_edge_interval(edge_interval)):
441 return
443 inside_net_name = edge.property('net')
445 # NOTE: each polygon in outside_polygons
446 # - could have a different net (e.g. the wells of the substrate and the rest, VSUBS)
447 # - could be segmented by a shield into multiple polygons
448 # each with different near/far regions
450 outside_net_names = [p.property('net') for p in outside_polygons]
452 # NOTE: the model of a device has the capacitance between its plates, so they are like one net,
453 # with the fringe from the edge of a plate to the other plate beyond it
454 # NOTE: and the parts of a net that are apart in the netlist are like nets of their own
455 same_net_markers = [
456 (inside_net_name == outside_net_name and not self.results.has_net_pieces(inside_net_name)) or
457 self.device_capacitor_plates.is_plate_edge_of_a_device(edge, inside_net_name, outside_net_name)
458 for outside_net_name in outside_net_names
459 ]
461 # NOTE: overlap_cap_by_layer_names is top/bot (dict is not symmetric)
462 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[inside_layer_name].get(outside_layer_name,
463 None)
464 if not overlap_cap_spec:
465 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[outside_layer_name][inside_layer_name]
467 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name]
468 sideoverlap_cap_spec = self.tech_info.side_overlap_cap_by_layer_names[inside_layer_name][
469 outside_layer_name]
471 polygons_by_net: Dict[NetName, List[kdb.PolygonWithProperties]] = defaultdict(list)
473 for idx, p in enumerate(outside_polygons):
474 outside_net = outside_net_names[idx]
475 is_same_net = same_net_markers[idx]
477 if is_same_net:
478 # TODO: log?
479 continue
481 if shield.is_empty():
482 polygons_by_net[outside_net].append(p)
483 else:
484 unshielded_region = kdb.Region(p)
485 unshielded_region.enable_properties()
486 unshielded_region -= shield
487 if unshielded_region.is_empty():
488 # TODO: log?
489 continue
491 for up in unshielded_region.each():
492 up = kdb.PolygonWithProperties(up, {'net': outside_net})
493 polygons_by_net[outside_net].append(up)
494 # if p != up:
495 # print(f"Unshieleded polygon {up}, differs from original polygon {p}")
497 for outside_net_name, polygons in polygons_by_net.items():
498 for p in polygons:
499 bbox = p.bbox()
500 if not p.is_box():
501 warning(f"Side overlap, polygon {p} is not a box. "
502 f"Currently, only boxes are supported, will be using bounding box {bbox}")
504 distance_near = bbox.p1.y # + 1
505 if distance_near < 0:
506 distance_near = 0
507 distance_far = bbox.p2.y # - 2
508 if distance_far < 0:
509 distance_far = 0
510 try:
511 assert distance_near >= 0
512 assert distance_far >= distance_near
513 except AssertionError:
514 print()
515 raise
517 if distance_far == distance_near:
518 return
520 edge_interval_length = edge_interval[1] - edge_interval[0]
521 edge_interval_length_um = edge_interval_length * self.dbu
523 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length,
524 distance_near=distance_near,
525 distance_far=distance_far,
526 overlap_cap_spec=overlap_cap_spec,
527 sideoverlap_cap_spec=sideoverlap_cap_spec)
529 if cap_femto > 0.0001: # TODO: configurable threshold, but keeping accumulation might also be nice
530 # info(f"(Side Overlap) "
531 # f"{inside_layer_name}({inside_net_name})-{outside_layer_name}({outside_net_name}): "
532 # f"{round(cap_femto, 5)} fF, "
533 # f"edge interval length = {round(edge_interval_length_um, 2)} µm")
535 inside_edge = geometry_restorer.restore_edge_interval(edge_interval)
536 # NOTE: the fringe ends on the polygon from its near to its far side,
537 # so the middle of it along the edge interval stands for it
538 distance_middle = (distance_near + distance_far) // 2
540 sok = SideOverlapKey(layer_inside=inside_layer_name,
541 net_inside=inside_net_name,
542 layer_outside=outside_layer_name,
543 net_outside=outside_net_name)
544 soc = SideOverlapCap(key=sok,
545 cap_value=cap_femto,
546 inside_edge=inside_edge,
547 outside_edge=geometry_restorer.restore_edge(
548 kdb.Edge(kdb.Point(edge_interval[0], distance_middle),
549 kdb.Point(edge_interval[1], distance_middle))
550 ),
551 near_edge=geometry_restorer.restore_edge(
552 kdb.Edge(kdb.Point(edge_interval[0], distance_near),
553 kdb.Point(edge_interval[1], distance_near))
554 ))
555 self.results.add_sideoverlap_cap(soc)
557 self.report.output_sideoverlap(
558 sideoverlap_cap=soc,
559 inside_edge=inside_edge,
560 outside_polygon=geometry_restorer.restore_polygon(p),
561 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \
562 if lateral_shield is not None else None
563 )