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