Coverage for klayout_pex/rcx25/extractor.py: 39%
225 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-12 13:45 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-12 13:45 +0000
1#! /usr/bin/env python3
2#
3# --------------------------------------------------------------------------------
4# SPDX-FileCopyrightText: 2024 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#
25import math
27import klayout.db as kdb
29from .r.conductance import Conductance
30from ..klayout.lvsdb_extractor import KLayoutExtractionContext, GDSPair
31from ..log import (
32 debug,
33 warning,
34 error,
35 info,
36 subproc
37)
38from ..tech_info import TechInfo
39from .extraction_results import *
40from .extraction_reporter import ExtractionReporter
41from .pex_mode import PEXMode
42from klayout_pex.rcx25.c.overlap_extractor import OverlapExtractor
43from klayout_pex.rcx25.c.sidewall_and_fringe_extractor import SidewallAndFringeExtractor
44from .r.resistor_extraction import ResistorExtraction
45from .r.resistor_network import (
46 ResistorNetworks,
47 ViaResistor,
48 ViaJunction,
49 DeviceTerminal,
50 MultiLayerResistanceNetwork
51)
54class RCX25Extractor:
55 def __init__(self,
56 pex_context: KLayoutExtractionContext,
57 pex_mode: PEXMode,
58 scale_ratio_to_fit_halo: bool,
59 delaunay_amax: float,
60 delaunay_b: float,
61 tech_info: TechInfo,
62 report_path: str):
63 self.pex_context = pex_context
64 self.pex_mode = pex_mode
65 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo
66 self.delaunay_amax = delaunay_amax
67 self.delaunay_b = delaunay_b
68 self.tech_info = tech_info
69 self.report_path = report_path
71 if "PolygonWithProperties" not in kdb.__all__:
72 raise Exception("KLayout version does not support properties (needs 0.30 at least)")
74 def gds_pair(self, layer_name) -> Optional[GDSPair]:
75 gds_pair = self.tech_info.gds_pair_for_computed_layer_name.get(layer_name, None)
76 if not gds_pair:
77 gds_pair = self.tech_info.gds_pair_for_layer_name.get(layer_name, None)
78 if not gds_pair:
79 warning(f"Can't find GDS pair for layer {layer_name}")
80 return None
81 return gds_pair
83 def shapes_of_layer(self, layer_name: str) -> Optional[kdb.Region]:
84 gds_pair = self.gds_pair(layer_name=layer_name)
85 if not gds_pair:
86 return None
88 shapes = self.pex_context.shapes_of_layer(gds_pair=gds_pair)
89 if not shapes:
90 debug(f"Nothing extracted for layer {layer_name}")
92 return shapes
94 def extract(self) -> ExtractionResults:
95 extraction_results = ExtractionResults()
97 # TODO: for now, we always flatten and have only 1 cell
98 cell_name = self.pex_context.annotated_top_cell.name
99 extraction_report = ExtractionReporter(cell_name=cell_name,
100 dbu=self.pex_context.dbu)
101 cell_extraction_results = CellExtractionResults(cell_name=cell_name)
103 # Explicitly log the stacktrace here, because otherwise Exceptions
104 # raised in the callbacks of *NeighborhoodVisitors can cause RuntimeErrors
105 # that are not traceable beyond the Region.complex_op() calls
106 try:
107 self.extract_cell(results=cell_extraction_results,
108 report=extraction_report)
109 except RuntimeError as e:
110 import traceback
111 print(f"Caught a RuntimeError: {e}")
112 traceback.print_exc()
113 raise
115 extraction_results.cell_extraction_results[cell_name] = cell_extraction_results
117 extraction_report.save(self.report_path)
119 return extraction_results
121 def extract_cell(self,
122 results: CellExtractionResults,
123 report: ExtractionReporter):
124 netlist: kdb.Netlist = self.pex_context.lvsdb.netlist()
125 dbu = self.pex_context.dbu
126 # ------------------------------------------------------------------------
128 layer_regions_by_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region)
130 all_region = kdb.Region()
131 all_region.enable_properties()
133 substrate_region = kdb.Region()
134 substrate_region.enable_properties()
136 side_halo_um = self.tech_info.tech.process_parasitics.side_halo
137 substrate_region.insert(self.pex_context.top_cell_bbox().enlarged(side_halo_um / dbu)) # e.g. 8 µm halo
139 layer_regions_by_name[self.tech_info.internal_substrate_layer_name] = substrate_region
141 via_name_below_layer_name: Dict[LayerName, Optional[LayerName]] = {}
142 via_name_above_layer_name: Dict[LayerName, Optional[LayerName]] = {}
143 via_regions_by_via_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region)
145 previous_via_name: Optional[str] = None
147 for metal_layer in self.tech_info.process_metal_layers:
148 layer_name = metal_layer.name
149 gds_pair = self.gds_pair(layer_name)
150 canonical_layer_name = self.tech_info.canonical_layer_name_by_gds_pair[gds_pair]
152 all_layer_shapes = self.shapes_of_layer(layer_name)
153 if all_layer_shapes is not None:
154 all_layer_shapes.enable_properties()
156 layer_regions_by_name[canonical_layer_name] += all_layer_shapes
157 layer_regions_by_name[canonical_layer_name].enable_properties()
158 all_region += all_layer_shapes
160 if metal_layer.metal_layer.HasField('contact_above'):
161 contact = metal_layer.metal_layer.contact_above
163 via_regions = self.shapes_of_layer(contact.name)
164 if via_regions is not None:
165 via_regions.enable_properties()
166 via_regions_by_via_name[contact.name] += via_regions
167 via_name_above_layer_name[canonical_layer_name] = contact.name
168 via_name_below_layer_name[canonical_layer_name] = previous_via_name
170 previous_via_name = contact.name
171 else:
172 previous_via_name = None
174 all_layer_names = list(layer_regions_by_name.keys())
176 # ------------------------------------------------------------------------
177 if self.pex_mode.need_capacitance():
178 overlap_extractor = OverlapExtractor(
179 all_layer_names=all_layer_names,
180 layer_regions_by_name=layer_regions_by_name,
181 dbu=dbu,
182 tech_info=self.tech_info,
183 results=results,
184 report=report
185 )
186 overlap_extractor.extract()
188 sidewall_and_fringe_extractor = SidewallAndFringeExtractor(
189 all_layer_names=all_layer_names,
190 layer_regions_by_name=layer_regions_by_name,
191 dbu=dbu,
192 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo,
193 tech_info=self.tech_info,
194 results=results,
195 report=report
196 )
197 sidewall_and_fringe_extractor.extract()
199 # ------------------------------------------------------------------------
200 if self.pex_mode.need_resistance():
201 rex = ResistorExtraction(b=self.delaunay_b, amax=self.delaunay_amax)
203 c: kdb.Circuit = netlist.top_circuit()
204 info(f"LVSDB: found {c.pin_count()}pins")
206 result_network = MultiLayerResistanceNetwork(
207 resistor_networks_by_layer={},
208 via_resistors=[]
209 )
211 devices_by_name = self.pex_context.devices_by_name
212 report.output_devices(devices_by_name)
214 node_count_by_net: Dict[str, int] = defaultdict(int)
216 for layer_name, region in layer_regions_by_name.items():
217 if layer_name == self.tech_info.internal_substrate_layer_name:
218 continue
220 layer_sheet_resistance = self.tech_info.layer_resistance_by_layer_name.get(layer_name, None)
221 if layer_sheet_resistance is None:
222 continue
224 gds_pair = self.gds_pair(layer_name)
225 pins = self.pex_context.pins_of_layer(gds_pair)
226 labels = self.pex_context.labels_of_layer(gds_pair)
228 nodes = kdb.Region()
229 nodes.enable_properties()
231 pin_labels: kdb.Texts = labels & pins
232 for l in pin_labels:
233 l: kdb.Text
234 # NOTE: because we want more like a point as a junction
235 # and folx create huge pins (covering the whole metal)
236 # we create our own "mini squares"
237 # (ResistorExtractor will subtract the pins from the metal polygons,
238 # so in the extreme case the polygons could become empty)
239 pin_point = l.bbox().enlarge(5)
240 nodes.insert(pin_point)
242 report.output_pin(layer_name=layer_name,
243 pin_point=pin_point,
244 label=l)
246 def create_nodes_for_region(region: kdb.Region):
247 for p in region:
248 p: kdb.PolygonWithProperties
249 cp: kdb.Point = p.bbox().center()
250 b = kdb.Box(w=6, h=6)
251 b.move(cp.x - b.width() / 2,
252 cp.y - b.height() / 2)
253 bwp = kdb.BoxWithProperties(b, p.properties())
255 net = bwp.property('net')
256 if net is None or net == '':
257 error(f"Could not find net for via at {cp}")
258 else:
259 label_text = f"{net}.n{node_count_by_net[net]}"
260 node_count_by_net[net] += 1
261 label = kdb.Text(label_text, cp.x, cp.y)
262 labels.insert(label)
264 nodes.insert(bwp)
266 # create additional nodes for vias
267 via_above = via_name_above_layer_name.get(layer_name, None)
268 if via_above is not None:
269 create_nodes_for_region(via_regions_by_via_name[via_above])
270 via_below = via_name_below_layer_name.get(layer_name, None)
271 if via_below is not None:
272 create_nodes_for_region(via_regions_by_via_name[via_below])
274 extracted_resistor_networks = rex.extract(polygons=region, pins=nodes, labels=labels)
275 resistor_networks = ResistorNetworks(
276 layer_name=layer_name,
277 layer_sheet_resistance=layer_sheet_resistance.resistance,
278 networks=extracted_resistor_networks
279 )
281 result_network.resistor_networks_by_layer[layer_name] = resistor_networks
283 subproc(f"Layer {layer_name} (R_coeff = {layer_sheet_resistance.resistance}):")
284 for rn in resistor_networks.networks:
285 # print(rn.to_string(True))
286 if not rn.node_to_s:
287 continue
289 subproc("\tNodes:")
290 for node_id in rn.node_to_s.keys():
291 loc = rn.locations[node_id]
292 node_name = rn.node_names[node_id]
293 subproc(f"\t\tNode #{node_id} {node_name} at {loc} ({loc.x * dbu} µm, {loc.y * dbu} µm)")
295 subproc("\tResistors:")
296 visited_resistors: Set[Conductance] = set()
297 for node_id, resistors in rn.node_to_s.items():
298 node_name = rn.node_names[node_id]
299 for conductance, other_node_id in resistors:
300 if conductance in visited_resistors:
301 continue # we don't want to add it twice, only once per direction!
302 visited_resistors.add(conductance)
304 other_node_name = rn.node_names[other_node_id]
305 ohm = layer_sheet_resistance.resistance / 1000.0 / conductance.cond
306 # TODO: layer_sheet_resistance.corner_adjustment_fraction not yet used !!!
307 subproc(f"\t\t{node_name} ↔︎ {other_node_name}: {round(ohm, 3)} Ω (internally: {conductance.cond})")
309 # "Stitch" in the VIAs into the graph
310 for layer_idx_bottom, layer_name_bottom in enumerate(all_layer_names):
311 if layer_name_bottom == self.tech_info.internal_substrate_layer_name:
312 continue
313 if (layer_idx_bottom + 1) == len(all_layer_names):
314 break
316 via = self.tech_info.contact_above_metal_layer_name.get(layer_name_bottom, None)
317 if via is None:
318 continue
320 via_gds_pair = self.gds_pair(via.name)
321 canonical_via_name = self.tech_info.canonical_layer_name_by_gds_pair[via_gds_pair]
323 via_region = via_regions_by_via_name.get(canonical_via_name)
324 if via_region is None:
325 continue
327 # NOTE: poly layer stands for poly/nsdm/psdm, this will be in contacts, not in vias
328 via_resistance = self.tech_info.via_resistance_by_layer_name.get(canonical_via_name, None)
329 r_coeff: Optional[float] = None
330 if via_resistance is None:
331 r_coeff = self.tech_info.contact_resistance_by_layer_name[layer_name_bottom].resistance
332 else:
333 r_coeff = via_resistance.resistance
335 layer_name_top = all_layer_names[layer_idx_bottom + 1]
337 networks_bottom = result_network.resistor_networks_by_layer[layer_name_bottom]
338 networks_top = result_network.resistor_networks_by_layer[layer_name_top]
340 for via_polygon in via_region:
341 net_name = via_polygon.property('net')
342 matches_bottom = networks_bottom.find_network_nodes(location=via_polygon)
344 device_terminal: Optional[Tuple[DeviceTerminal, float]] = None
346 if len(matches_bottom) == 0:
347 ignored_device_layers: Set[str] = set()
349 def find_device_terminal(via_region: kdb.Region) -> Optional[Tuple[DeviceTerminal, float]]:
350 for d in devices_by_name.values():
351 for dt in d.terminals.terminals:
352 for ln, r in dt.regions_by_layer_name.items():
353 res = self.tech_info.contact_resistance_by_layer_name.get(ln, None)
354 if res is None:
355 ignored_device_layers.add(ln)
356 continue
357 elif r.overlapping(via_region):
358 return (DeviceTerminal(device=d, device_terminal=dt), res)
359 return None
361 if layer_name_bottom in self.tech_info.contact_resistance_by_layer_name.keys():
362 device_terminal = find_device_terminal(via_region=kdb.Region(via_polygon))
363 if device_terminal is None:
364 warning(f"Couldn't find bottom network node (on {layer_name_bottom}) "
365 f"for location {via_polygon}, "
366 f"but could not find a device terminal either "
367 f"(ignored layers: {ignored_device_layers})")
368 else:
369 r_coeff = device_terminal[1].resistance
371 matches_top = networks_top.find_network_nodes(location=via_polygon)
372 if len(matches_top) == 0:
373 error(f"Could not find top network nodes for location {via_polygon}")
375 # given a drawn via area, we calculate the actual via matrix
376 approx_width = math.sqrt(via_polygon.area()) * dbu
377 n_xy = 1 + math.floor((approx_width - (via.width + 2 * via.border)) / (via.width + via.spacing))
378 if n_xy < 1:
379 n_xy = 1
380 r_via_ohm = r_coeff / n_xy**2 / 1000.0 # mΩ -> Ω
382 info(f"via ({canonical_via_name}) found between "
383 f"metals {layer_name_bottom} ↔ {layer_name_top} at {via_polygon}, "
384 f"{n_xy}x{n_xy} (w={via.width}, sp={via.spacing}, border={via.border}), "
385 f"{r_via_ohm} Ω")
387 report.output_via(via_name=canonical_via_name,
388 bottom_layer=layer_name_bottom,
389 top_layer=layer_name_top,
390 net=net_name,
391 via_width=via.width,
392 via_spacing=via.spacing,
393 via_border=via.border,
394 polygon=via_polygon,
395 ohm=r_via_ohm,
396 comment=f"({len(matches_bottom)} bottom, {len(matches_top)} top)")
398 match_top = matches_top[0] if len(matches_top) >= 1 else (None, -1)
400 bottom: ViaJunction | DeviceTerminal
401 if device_terminal is None:
402 match_bottom = matches_bottom[0] if len(matches_bottom) >= 1 else (None, -1)
403 bottom = ViaJunction(layer_name=layer_name_bottom,
404 network=match_bottom[0],
405 node_id=match_bottom[1])
406 else:
407 bottom = device_terminal[0]
409 via_resistor = ViaResistor(
410 bottom=bottom,
411 top=ViaJunction(layer_name=layer_name_top,
412 network=match_top[0],
413 node_id=match_top[1]),
414 resistance=r_via_ohm
415 )
416 result_network.via_resistors.append(via_resistor)
418 # import rich.pretty
419 # rich.pretty.pprint(result_network)
420 results.resistor_network = result_network
422 return results