Coverage for klayout_pex/rcx25/c/sidewall_and_fringe_extractor.py: 14%
188 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-08 18:54 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-08 18:54 +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/martinjankoehler/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 cap_femto = ((length_um * sidewall_cap_spec.capacitance)
293 / (distance_um + sidewall_cap_spec.offset)
294 / 2.0 # non-bidirectional (half)
295 / 1000.0) # aF -> fF
297 info(f"(Sidewall) layer {layer_name}: Nets {net1} <-> {net2}: {round(cap_femto, 5)} fF")
299 swk = SidewallKey(layer=layer_name, net1=net1, net2=net2)
300 sw_cap = SidewallCap(key=swk,
301 cap_value=cap_femto,
302 distance=distance_um,
303 length=length_um,
304 tech_spec=sidewall_cap_spec)
305 self.results.add_sidewall_cap(sw_cap)
307 self.report.output_sidewall(
308 sidewall_cap=sw_cap,
309 inside_edge=geometry_restorer.restore_edge_interval(edge_interval),
310 outside_edge=geometry_restorer.restore_edge(outside_edge)
311 )
313 def fringe_cap(self,
314 edge_interval_length: float,
315 distance_near: float,
316 distance_far: float,
317 overlap_cap_spec: CapacitanceInfo.OverlapCapacitance,
318 sideoverlap_cap_spec: CapacitanceInfo.SideOverlapCapacitance) -> float:
319 distance_near_um = distance_near * self.dbu
320 distance_far_um = distance_far * self.dbu
321 edge_interval_length_um = edge_interval_length * self.dbu
323 # NOTE: overlap scaling is 1/50 (see MAGIC ExtTech)
324 alpha_scale_factor = 0.02 * 0.01 * 0.5 * 200.0
325 alpha_c = overlap_cap_spec.capacitance * alpha_scale_factor
327 # see Magic ExtCouple.c L1164
328 cnear = (2.0 / math.pi) * math.atan(alpha_c * distance_near_um)
329 cfar = (2.0 / math.pi) * math.atan(alpha_c * distance_far_um)
331 if self.scale_ratio_to_fit_halo:
332 full_halo_ratio = (2.0 / math.pi) * math.atan(alpha_c * self.side_halo)
333 # NOTE: for a large enough halo, full_halo would be 1,
334 # but it is smaller, so we compensate
335 if full_halo_ratio < 1.0:
336 cnear /= full_halo_ratio
337 cfar /= full_halo_ratio
339 # "cfrac" is the fractional portion of the fringe cap seen
340 # by tile tp along its length. This is independent of the
341 # portion of the boundary length that tile tp occupies.
342 cfrac = cfar - cnear
344 cap_femto = (cfrac * edge_interval_length_um *
345 sideoverlap_cap_spec.capacitance / 1000.0)
347 return cap_femto
349 def emit_fringe(self,
350 inside_layer_name: LayerName,
351 outside_layer_name: LayerName,
352 edge: kdb.EdgeWithProperties,
353 edge_interval: EdgeInterval,
354 outside_polygons: List[kdb.PolygonWithProperties],
355 shield: kdb.Region,
356 lateral_shield: kdb.Polygon,
357 geometry_restorer: GeometryRestorer):
358 inside_net_name = self.tech_info.internal_substrate_layer_name \
359 if inside_layer_name == self.tech_info.internal_substrate_layer_name \
360 else edge.property('net')
362 # NOTE: each polygon in outside_polygons
363 # - could have a different net
364 # - could be segmented by a shield into multiple polygons
365 # each with different near/far regions
367 outside_net_names = [
368 self.tech_info.internal_substrate_layer_name \
369 if outside_layer_name == self.tech_info.internal_substrate_layer_name \
370 else p.property('net')
371 for p in outside_polygons
372 ]
374 same_net_markers = [
375 inside_net_name == outside_net_name
376 for outside_net_name in outside_net_names
377 ]
379 # NOTE: overlap_cap_by_layer_names is top/bot (dict is not symmetric)
380 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[inside_layer_name].get(outside_layer_name,
381 None)
382 if not overlap_cap_spec:
383 overlap_cap_spec = self.tech_info.overlap_cap_by_layer_names[outside_layer_name][inside_layer_name]
385 substrate_cap_spec = self.tech_info.substrate_cap_by_layer_name[inside_layer_name]
386 sideoverlap_cap_spec = self.tech_info.side_overlap_cap_by_layer_names[inside_layer_name][
387 outside_layer_name]
389 polygons_by_net: Dict[NetName, List[kdb.PolygonWithProperties]] = defaultdict(list)
391 for idx, p in enumerate(outside_polygons):
392 outside_net = outside_net_names[idx]
393 is_same_net = same_net_markers[idx]
395 if is_same_net:
396 # TODO: log?
397 continue
399 if shield.is_empty():
400 polygons_by_net[outside_net].append(p)
401 else:
402 unshielded_region = kdb.Region(p)
403 unshielded_region.enable_properties()
404 unshielded_region -= shield
405 if unshielded_region.is_empty():
406 # TODO: log?
407 continue
409 for up in unshielded_region.each():
410 up = kdb.PolygonWithProperties(up, {'net': outside_net})
411 polygons_by_net[outside_net].append(up)
412 # if p != up:
413 # print(f"Unshieleded polygon {up}, differs from original polygon {p}")
415 for outside_net_name, polygons in polygons_by_net.items():
416 for p in polygons:
417 bbox = p.bbox()
418 if not p.is_box():
419 warning(f"Side overlap, polygon {p} is not a box. "
420 f"Currently, only boxes are supported, will be using bounding box {bbox}")
422 distance_near = bbox.p1.y # + 1
423 if distance_near < 0:
424 distance_near = 0
425 distance_far = bbox.p2.y # - 2
426 if distance_far < 0:
427 distance_far = 0
428 try:
429 assert distance_near >= 0
430 assert distance_far >= distance_near
431 except AssertionError:
432 print()
433 raise
435 if distance_far == distance_near:
436 return
438 edge_interval_length = edge_interval[1] - edge_interval[0]
439 edge_interval_length_um = edge_interval_length * self.dbu
441 cap_femto = self.fringe_cap(edge_interval_length=edge_interval_length,
442 distance_near=distance_near,
443 distance_far=distance_far,
444 overlap_cap_spec=overlap_cap_spec,
445 sideoverlap_cap_spec=sideoverlap_cap_spec)
447 if cap_femto > 0.0001: # TODO: configurable threshold, but keeping accumulation might also be nice
448 info(f"(Side Overlap) "
449 f"{inside_layer_name}({inside_net_name})-{outside_layer_name}({outside_net_name}): "
450 f"{round(cap_femto, 5)} fF, "
451 f"edge interval length = {round(edge_interval_length_um, 2)} µm")
453 sok = SideOverlapKey(layer_inside=inside_layer_name,
454 net_inside=inside_net_name,
455 layer_outside=outside_layer_name,
456 net_outside=outside_net_name)
457 soc = SideOverlapCap(key=sok, cap_value=cap_femto)
458 self.results.add_sideoverlap_cap(soc)
460 self.report.output_sideoverlap(
461 sideoverlap_cap=soc,
462 inside_edge=geometry_restorer.restore_edge_interval(edge_interval),
463 outside_polygon=geometry_restorer.restore_polygon(p),
464 lateral_shield=geometry_restorer.restore_polygon(lateral_shield) \
465 if lateral_shield is not None else None
466 )