Coverage for klayout_pex/rcx25/extractor.py: 39%
219 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-03-31 19:36 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-03-31 19:36 +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 RCExtractor:
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 self.extract_cell(results=cell_extraction_results,
104 report=extraction_report)
105 extraction_results.cell_extraction_results[cell_name] = cell_extraction_results
107 extraction_report.save(self.report_path)
109 return extraction_results
111 def extract_cell(self,
112 results: CellExtractionResults,
113 report: ExtractionReporter):
114 netlist: kdb.Netlist = self.pex_context.lvsdb.netlist()
115 dbu = self.pex_context.dbu
116 # ------------------------------------------------------------------------
118 layer_regions_by_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region)
120 all_region = kdb.Region()
121 all_region.enable_properties()
123 substrate_region = kdb.Region()
124 substrate_region.enable_properties()
126 side_halo_um = self.tech_info.tech.process_parasitics.side_halo
127 substrate_region.insert(self.pex_context.top_cell_bbox().enlarged(side_halo_um / dbu)) # e.g. 8 µm halo
129 layer_regions_by_name[self.tech_info.internal_substrate_layer_name] = substrate_region
131 via_name_below_layer_name: Dict[LayerName, Optional[LayerName]] = {}
132 via_name_above_layer_name: Dict[LayerName, Optional[LayerName]] = {}
133 via_regions_by_via_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region)
135 previous_via_name: Optional[str] = None
137 for metal_layer in self.tech_info.process_metal_layers:
138 layer_name = metal_layer.name
139 gds_pair = self.gds_pair(layer_name)
140 canonical_layer_name = self.tech_info.canonical_layer_name_by_gds_pair[gds_pair]
142 all_layer_shapes = self.shapes_of_layer(layer_name)
143 if all_layer_shapes is not None:
144 all_layer_shapes.enable_properties()
146 layer_regions_by_name[canonical_layer_name] += all_layer_shapes
147 layer_regions_by_name[canonical_layer_name].enable_properties()
148 all_region += all_layer_shapes
150 if metal_layer.metal_layer.HasField('contact_above'):
151 contact = metal_layer.metal_layer.contact_above
153 via_regions = self.shapes_of_layer(contact.name)
154 if via_regions is not None:
155 via_regions.enable_properties()
156 via_regions_by_via_name[contact.name] += via_regions
157 via_name_above_layer_name[canonical_layer_name] = contact.name
158 via_name_below_layer_name[canonical_layer_name] = previous_via_name
160 previous_via_name = contact.name
161 else:
162 previous_via_name = None
164 all_layer_names = list(layer_regions_by_name.keys())
166 # ------------------------------------------------------------------------
167 if self.pex_mode.need_capacitance():
168 overlap_extractor = OverlapExtractor(
169 all_layer_names=all_layer_names,
170 layer_regions_by_name=layer_regions_by_name,
171 dbu=dbu,
172 tech_info=self.tech_info,
173 results=results,
174 report=report
175 )
176 overlap_extractor.extract()
178 sidewall_and_fringe_extractor = SidewallAndFringeExtractor(
179 all_layer_names=all_layer_names,
180 layer_regions_by_name=layer_regions_by_name,
181 dbu=dbu,
182 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo,
183 tech_info=self.tech_info,
184 results=results,
185 report=report
186 )
187 sidewall_and_fringe_extractor.extract()
189 # ------------------------------------------------------------------------
190 if self.pex_mode.need_resistance():
191 rex = ResistorExtraction(b=self.delaunay_b, amax=self.delaunay_amax)
193 c: kdb.Circuit = netlist.top_circuit()
194 info(f"LVSDB: found {c.pin_count()}pins")
196 result_network = MultiLayerResistanceNetwork(
197 resistor_networks_by_layer={},
198 via_resistors=[]
199 )
201 devices_by_name = self.pex_context.devices_by_name
202 report.output_devices(devices_by_name)
204 node_count_by_net: Dict[str, int] = defaultdict(int)
206 for layer_name, region in layer_regions_by_name.items():
207 if layer_name == self.tech_info.internal_substrate_layer_name:
208 continue
210 layer_sheet_resistance = self.tech_info.layer_resistance_by_layer_name.get(layer_name, None)
211 if layer_sheet_resistance is None:
212 continue
214 gds_pair = self.gds_pair(layer_name)
215 pins = self.pex_context.pins_of_layer(gds_pair)
216 labels = self.pex_context.labels_of_layer(gds_pair)
218 nodes = kdb.Region()
219 nodes.enable_properties()
221 pin_labels: kdb.Texts = labels & pins
222 for l in pin_labels:
223 l: kdb.Text
224 # NOTE: because we want more like a point as a junction
225 # and folx create huge pins (covering the whole metal)
226 # we create our own "mini squares"
227 # (ResistorExtractor will subtract the pins from the metal polygons,
228 # so in the extreme case the polygons could become empty)
229 pin_point = l.bbox().enlarge(5)
230 nodes.insert(pin_point)
232 report.output_pin(layer_name=layer_name,
233 pin_point=pin_point,
234 label=l)
236 def create_nodes_for_region(region: kdb.Region):
237 for p in region:
238 p: kdb.PolygonWithProperties
239 cp: kdb.Point = p.bbox().center()
240 b = kdb.Box(w=6, h=6)
241 b.move(cp.x - b.width() / 2,
242 cp.y - b.height() / 2)
243 bwp = kdb.BoxWithProperties(b, p.properties())
245 net = bwp.property('net')
246 if net is None or net == '':
247 error(f"Could not find net for via at {cp}")
248 else:
249 label_text = f"{net}.n{node_count_by_net[net]}"
250 node_count_by_net[net] += 1
251 label = kdb.Text(label_text, cp.x, cp.y)
252 labels.insert(label)
254 nodes.insert(bwp)
256 # create additional nodes for vias
257 via_above = via_name_above_layer_name.get(layer_name, None)
258 if via_above is not None:
259 create_nodes_for_region(via_regions_by_via_name[via_above])
260 via_below = via_name_below_layer_name.get(layer_name, None)
261 if via_below is not None:
262 create_nodes_for_region(via_regions_by_via_name[via_below])
264 extracted_resistor_networks = rex.extract(polygons=region, pins=nodes, labels=labels)
265 resistor_networks = ResistorNetworks(
266 layer_name=layer_name,
267 layer_sheet_resistance=layer_sheet_resistance.resistance,
268 networks=extracted_resistor_networks
269 )
271 result_network.resistor_networks_by_layer[layer_name] = resistor_networks
273 subproc(f"Layer {layer_name} (R_coeff = {layer_sheet_resistance.resistance}):")
274 for rn in resistor_networks.networks:
275 # print(rn.to_string(True))
276 if not rn.node_to_s:
277 continue
279 subproc("\tNodes:")
280 for node_id in rn.node_to_s.keys():
281 loc = rn.locations[node_id]
282 node_name = rn.node_names[node_id]
283 subproc(f"\t\tNode #{node_id} {node_name} at {loc} ({loc.x * dbu} µm, {loc.y * dbu} µm)")
285 subproc("\tResistors:")
286 visited_resistors: Set[Conductance] = set()
287 for node_id, resistors in rn.node_to_s.items():
288 node_name = rn.node_names[node_id]
289 for conductance, other_node_id in resistors:
290 if conductance in visited_resistors:
291 continue # we don't want to add it twice, only once per direction!
292 visited_resistors.add(conductance)
294 other_node_name = rn.node_names[other_node_id]
295 ohm = layer_sheet_resistance.resistance / 1000.0 / conductance.cond
296 # TODO: layer_sheet_resistance.corner_adjustment_fraction not yet used !!!
297 subproc(f"\t\t{node_name} ↔︎ {other_node_name}: {round(ohm, 3)} Ω (internally: {conductance.cond})")
299 # "Stitch" in the VIAs into the graph
300 for layer_idx_bottom, layer_name_bottom in enumerate(all_layer_names):
301 if layer_name_bottom == self.tech_info.internal_substrate_layer_name:
302 continue
303 if (layer_idx_bottom + 1) == len(all_layer_names):
304 break
306 via = self.tech_info.contact_above_metal_layer_name.get(layer_name_bottom, None)
307 if via is None:
308 continue
310 via_gds_pair = self.gds_pair(via.name)
311 canonical_via_name = self.tech_info.canonical_layer_name_by_gds_pair[via_gds_pair]
313 via_region = via_regions_by_via_name.get(canonical_via_name)
314 if via_region is None:
315 continue
317 # NOTE: poly layer stands for poly/nsdm/psdm, this will be in contacts, not in vias
318 via_resistance = self.tech_info.via_resistance_by_layer_name.get(canonical_via_name, None)
319 r_coeff: Optional[float] = None
320 if via_resistance is None:
321 r_coeff = self.tech_info.contact_resistance_by_layer_name[layer_name_bottom].resistance
322 else:
323 r_coeff = via_resistance.resistance
325 layer_name_top = all_layer_names[layer_idx_bottom + 1]
327 networks_bottom = result_network.resistor_networks_by_layer[layer_name_bottom]
328 networks_top = result_network.resistor_networks_by_layer[layer_name_top]
330 for via_polygon in via_region:
331 net_name = via_polygon.property('net')
332 matches_bottom = networks_bottom.find_network_nodes(location=via_polygon)
334 device_terminal: Optional[Tuple[DeviceTerminal, float]] = None
336 if len(matches_bottom) == 0:
337 ignored_device_layers: Set[str] = set()
339 def find_device_terminal(via_region: kdb.Region) -> Optional[Tuple[DeviceTerminal, float]]:
340 for d in devices_by_name.values():
341 for dt in d.terminals.terminals:
342 for ln, r in dt.regions_by_layer_name.items():
343 res = self.tech_info.contact_resistance_by_layer_name.get(ln, None)
344 if res is None:
345 ignored_device_layers.add(ln)
346 continue
347 elif r.overlapping(via_region):
348 return (DeviceTerminal(device=d, device_terminal=dt), res)
349 return None
351 if layer_name_bottom in self.tech_info.contact_resistance_by_layer_name.keys():
352 device_terminal = find_device_terminal(via_region=kdb.Region(via_polygon))
353 if device_terminal is None:
354 warning(f"Couldn't find bottom network node (on {layer_name_bottom}) "
355 f"for location {via_polygon}, "
356 f"but could not find a device terminal either "
357 f"(ignored layers: {ignored_device_layers})")
358 else:
359 r_coeff = device_terminal[1].resistance
361 matches_top = networks_top.find_network_nodes(location=via_polygon)
362 if len(matches_top) == 0:
363 error(f"Could not find top network nodes for location {via_polygon}")
365 # given a drawn via area, we calculate the actual via matrix
366 approx_width = math.sqrt(via_polygon.area()) * dbu
367 n_xy = 1 + math.floor((approx_width - (via.width + 2 * via.border)) / (via.width + via.spacing))
368 if n_xy < 1:
369 n_xy = 1
370 r_via_ohm = r_coeff / n_xy**2 / 1000.0 # mΩ -> Ω
372 info(f"via ({canonical_via_name}) found between "
373 f"metals {layer_name_bottom} ↔ {layer_name_top} at {via_polygon}, "
374 f"{n_xy}x{n_xy} (w={via.width}, sp={via.spacing}, border={via.border}), "
375 f"{r_via_ohm} Ω")
377 report.output_via(via_name=canonical_via_name,
378 bottom_layer=layer_name_bottom,
379 top_layer=layer_name_top,
380 net=net_name,
381 via_width=via.width,
382 via_spacing=via.spacing,
383 via_border=via.border,
384 polygon=via_polygon,
385 ohm=r_via_ohm,
386 comment=f"({len(matches_bottom)} bottom, {len(matches_top)} top)")
388 match_top = matches_top[0] if len(matches_top) == 1 else (None, -1)
390 bottom: ViaJunction | DeviceTerminal
391 if device_terminal is None:
392 match_bottom = matches_bottom[0] if len(matches_bottom) == 1 else (None, -1)
393 bottom = ViaJunction(layer_name=layer_name_bottom,
394 network=match_bottom[0],
395 node_id=match_bottom[1])
396 else:
397 bottom = device_terminal[0]
399 via_resistor = ViaResistor(
400 bottom=bottom,
401 top=ViaJunction(layer_name=layer_name_top,
402 network=match_top[0],
403 node_id=match_top[1]),
404 resistance=r_via_ohm
405 )
406 result_network.via_resistors.append(via_resistor)
408 # import rich.pretty
409 # rich.pretty.pprint(result_network)
410 results.resistor_network = result_network
412 return results