Coverage for klayout_pex/rcx25/extractor.py: 95%
133 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +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#
26import klayout.db as kdb
28from ..klayout.lvsdb_extractor import KLayoutExtractionContext, GDSPair
29from ..log import (
30 debug,
31 warning,
32 error,
33 info,
34 subproc,
35 rule
36)
37from ..tech_info import TechInfo
38from .extraction_results import *
39from .extraction_reporter import ExtractionReporter
40from .pex_mode import PEXMode
41from klayout_pex.rcx25.c.device_capacitor_plates import DeviceCapacitorPlates
42from klayout_pex.rcx25.c.overlap_extractor import OverlapExtractor
43from klayout_pex.rcx25.c.sidewall_and_fringe_extractor import SidewallAndFringeExtractor
44from klayout_pex.rcx25.r.r_extractor import RExtractor
46import klayout_pex_protobuf.kpex.geometry.shapes_pb2 as shapes_pb2
47import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2
48import klayout_pex_protobuf.kpex.request.pex_request_pb2 as pex_request_pb2
49import klayout_pex_protobuf.kpex.result.pex_result_pb2 as pex_result_pb2
50import klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 as rex_tech_pb2
51from klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 import RExtractorTech as pb_RExtractorTech
54class CExtractionTechError(Exception):
55 """
56 The tech info can't model the capacitances of the layout
57 """
58 pass
61class RCX25Extractor:
62 def __init__(self,
63 pex_context: KLayoutExtractionContext,
64 pex_mode: PEXMode,
65 scale_ratio_to_fit_halo: bool,
66 delaunay_amax: float,
67 delaunay_b: float,
68 tech_info: TechInfo,
69 report_path: str):
70 self.pex_context = pex_context
71 self.pex_mode = pex_mode
72 self.scale_ratio_to_fit_halo = scale_ratio_to_fit_halo
73 self.delaunay_amax = delaunay_amax
74 self.delaunay_b = delaunay_b
75 self.tech_info = tech_info
76 self.report_path = report_path
78 if "PolygonWithProperties" not in kdb.__all__:
79 raise Exception("KLayout version does not support properties (needs 0.30 at least)")
81 # TODO: remove this function by inlining
82 def gds_pair(self, layer_name) -> Optional[GDSPair]:
83 return self.tech_info.gds_pair(layer_name)
85 def shapes_of_layer(self, layer_name: str, mark_device_capacitor_plates: bool = False) -> Optional[kdb.Region]:
86 gds_pair = self.gds_pair(layer_name=layer_name)
87 if not gds_pair:
88 return None
90 shapes = self.pex_context.shapes_of_layer(gds_pair=gds_pair,
91 mark_device_capacitor_plates=mark_device_capacitor_plates)
92 if not shapes:
93 debug(f"Nothing extracted for layer {layer_name}")
95 return shapes
97 def substrate_region(self) -> kdb.Region:
98 """
99 The substrate below the layout (and its halo), the bottom plate of the capacitances to the substrate,
100 split into its wells, of the nets of the wells (e.g. VDD), and the rest, of the substrate (VSUBS)
101 """
102 dbu = self.pex_context.dbu
103 side_halo_um = self.tech_info.tech.process_parasitics.side_halo
104 rest = kdb.Region(self.pex_context.top_cell_bbox().enlarged(side_halo_um / dbu)) # e.g. 8 µm halo
106 substrate_region = kdb.Region()
107 substrate_region.enable_properties()
109 def insert(region: kdb.Region, net_name: NetName):
110 for polygon in region.each():
111 substrate_region.insert(kdb.PolygonWithProperties(polygon.downcast(), {'net': net_name}))
113 # NOTE: where wells overlap (e.g. an nwell within a deep nwell), the first one is at the surface
114 for well_layer_name in self.tech_info.tech.substrate.well_lvs_layer_names:
115 wells = self.shapes_of_layer(well_layer_name)
116 if wells is None:
117 continue
118 for well in wells.merged().each(): # NOTE: merged per net
119 well_region = kdb.Region(well.downcast()) & rest
120 rest -= well_region
121 insert(well_region, well.property('net'))
123 insert(rest, self.tech_info.internal_substrate_layer_name)
124 return substrate_region
126 def extract(self) -> ExtractionResults:
127 extraction_results = ExtractionResults()
129 # TODO: for now, we always flatten and have only 1 cell
130 cell_name = self.pex_context.annotated_top_cell.name
131 extraction_report = ExtractionReporter(cell_name=cell_name,
132 dbu=self.pex_context.dbu)
133 cell_extraction_results = CellExtractionResults(cell_name=cell_name)
135 # Explicitly log the stacktrace here, because otherwise Exceptions
136 # raised in the callbacks of *NeighborhoodVisitors can cause RuntimeErrors
137 # that are not traceable beyond the Region.complex_op() calls
138 try:
139 self.extract_cell(results=cell_extraction_results,
140 report=extraction_report)
141 except RuntimeError as e:
142 import traceback
143 print(f"Caught a RuntimeError: {e}")
144 traceback.print_exc()
145 raise
147 extraction_results.cell_extraction_results[cell_name] = cell_extraction_results
149 extraction_report.save(self.report_path)
151 return extraction_results
153 def extract_cell(self,
154 results: CellExtractionResults,
155 report: ExtractionReporter):
156 netlist: kdb.Netlist = self.pex_context.lvsdb.netlist()
157 dbu = self.pex_context.dbu
158 # ------------------------------------------------------------------------
160 layer_regions_by_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region)
162 all_region = kdb.Region()
163 all_region.enable_properties()
165 layer_regions_by_name[self.tech_info.internal_substrate_layer_name] = self.substrate_region()
167 # NOTE: the diffusion (the source/drain of the transistors) is above the substrate, which it shields,
168 # and below the metal layers, of whose capacitances it's the bottom plate
169 for diffusion_layer in self.tech_info.process_diffusion_layers:
170 diffusion_shapes = self.shapes_of_layer(diffusion_layer.name)
171 if diffusion_shapes is not None:
172 diffusion_shapes.enable_properties()
173 gds_pair = self.gds_pair(diffusion_layer.name)
174 canonical_layer_name = self.tech_info.canonical_layer_name_by_gds_pair[gds_pair]
175 layer_regions_by_name[canonical_layer_name] += diffusion_shapes
176 layer_regions_by_name[canonical_layer_name].enable_properties()
178 via_name_below_layer_name: Dict[LayerName, Optional[LayerName]] = {}
179 via_name_above_layer_name: Dict[LayerName, Optional[LayerName]] = {}
180 via_regions_by_via_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region)
182 previous_via_name: Optional[str] = None
184 for metal_layer in self.tech_info.process_metal_layers:
185 layer_name = metal_layer.name
186 gds_pair = self.gds_pair(layer_name)
187 canonical_layer_name = self.tech_info.canonical_layer_name_by_gds_pair[gds_pair]
189 # NOTE: with --blackbox, the plates of the capacitor devices are marked (see DeviceCapacitorPlates)
190 all_layer_shapes = self.shapes_of_layer(layer_name,
191 mark_device_capacitor_plates=self.pex_context.blackbox_devices)
192 if all_layer_shapes is not None:
193 all_layer_shapes.enable_properties()
195 layer_regions_by_name[canonical_layer_name] += all_layer_shapes
196 layer_regions_by_name[canonical_layer_name].enable_properties()
197 all_region += all_layer_shapes
199 if metal_layer.metal_layer.HasField('contact_above'):
200 contact = metal_layer.metal_layer.contact_above
202 via_regions = self.shapes_of_layer(contact.name)
203 if via_regions is not None:
204 via_regions.enable_properties()
205 via_regions_by_via_name[contact.name] += via_regions
206 via_name_above_layer_name[canonical_layer_name] = contact.name
207 via_name_below_layer_name[canonical_layer_name] = previous_via_name
209 previous_via_name = contact.name
210 else:
211 previous_via_name = None
213 all_layer_names = list(layer_regions_by_name.keys())
215 # ------------------------------------------------------------------------
216 if self.pex_mode.need_capacitance():
217 # NOTE: a layer pair without an overlap capacitance would miss its capacitances,
218 # which is never intended (#217), e.g. the plates of a MIM cap in white-box mode,
219 # but for the capacitances of the devices (e.g. of the diffusion to the substrate)
220 missing_overlap_caps = [f"{top_layer_name} over {bottom_layer_name}"
221 for idx, bottom_layer_name in enumerate(all_layer_names)
222 for top_layer_name in all_layer_names[idx + 1:]
223 if not self.tech_info.overlap_cap_by_layer_names
224 .get(top_layer_name, {}).get(bottom_layer_name, None)
225 and not self.tech_info.is_device_capacitance(top_layer_name, bottom_layer_name)]
226 if missing_overlap_caps:
227 raise CExtractionTechError(
228 "The tech info has no overlap capacitance for these layer pairs of the layout, "
229 "so their capacitances would be missing (e.g. the plates of a MIM cap, "
230 "which --blackbox leaves to the device model):\n" +
231 '\n'.join(f" - {pair}" for pair in missing_overlap_caps)
232 )
234 # NOTE: with --blackbox, the capacitances between the plates of a capacitor device
235 # (e.g. the fingers of a MOM cap) are left to its model
236 device_capacitor_plates = DeviceCapacitorPlates.from_circuit(
237 circuit=self.pex_context.top_circuit,
238 device_class_names=self.tech_info.device_models.metal_capacitor_class_names
239 ) if self.pex_context.blackbox_devices else DeviceCapacitorPlates()
241 overlap_extractor = OverlapExtractor(
242 all_layer_names=all_layer_names,
243 layer_regions_by_name=layer_regions_by_name,
244 dbu=dbu,
245 tech_info=self.tech_info,
246 device_capacitor_plates=device_capacitor_plates,
247 results=results,
248 report=report
249 )
250 overlap_extractor.extract()
252 sidewall_and_fringe_extractor = SidewallAndFringeExtractor(
253 all_layer_names=all_layer_names,
254 layer_regions_by_name=layer_regions_by_name,
255 dbu=dbu,
256 scale_ratio_to_fit_halo=self.scale_ratio_to_fit_halo,
257 tech_info=self.tech_info,
258 device_capacitor_plates=device_capacitor_plates,
259 results=results,
260 report=report
261 )
262 sidewall_and_fringe_extractor.extract()
264 # ------------------------------------------------------------------------
265 if self.pex_mode.need_resistance():
266 c: kdb.Circuit = netlist.top_circuit()
267 info(f"LVSDB: found {c.pin_count()}pins")
269 # FIXME:
270 # currenly, tesselation does not work:
271 # https://github.com/KLayout/klayout/issues/2100
272 r_extractor = RExtractor(pex_context=self.pex_context,
273 substrate_algorithm=pb_RExtractorTech.Algorithm.ALGORITHM_SQUARE_COUNTING,
274 #substrate_algorithm = pb_RExtractorTech.Algorithm.ALGORITHM_TESSELATION,
275 wire_algorithm = pb_RExtractorTech.Algorithm.ALGORITHM_SQUARE_COUNTING,
276 delaunay_b = self.delaunay_b,
277 delaunay_amax = self.delaunay_amax,
278 via_merge_distance = 0,
279 skip_simplify = True)
280 rex_request = r_extractor.prepare_request()
281 report.output_rex_request(request=rex_request)
283 rex_result = r_extractor.extract(rex_request)
284 report.output_rex_result(result=rex_result)
286 #
287 # node_by_id: Dict[int, r_network_pb2.RNode] = {}
288 # subproc("\tNodes:")
289 # for node in rex_result.nodes:
290 # node_by_id[node.node_id] = node
291 #
292 # msg = f"\t\tNode #{hex(node.node_id)} '{node.node_name}' " \
293 # f"of net '{node.net_name}' " \
294 # f"on layer '{node.layer_name}' "
295 # match node.location.kind:
296 # case location_pb2.Location.Kind.LOCATION_KIND_POINT:
297 # p = node.location.point
298 # msg += f"at {p.x},{p.y} ({p.x * dbu} µm, {p.y * dbu} µm)"
299 # case location_pb2.Location.Kind.LOCATION_KIND_BOX:
300 # b = node.location.box
301 # msg += f"at {b.lower_left.x},{b.lower_left.y};{b.upper_right.x},{b.upper_right.y} (" \
302 # f"B/L {round(b.lower_left.x * dbu, 3)},"\
303 # f"{round(b.lower_left.y * dbu, 3)} µm, " \
304 # f"T/R {round(b.upper_right.x * dbu, 3)},"\
305 # f"{round(b.upper_right.y * dbu)} µm)"
306 # subproc(msg)
307 #
308 # subproc("\tElements:")
309 # for element in rex_result.elements:
310 # node_a = node_by_id[element.node_a.node_id]
311 # node_b = node_by_id[element.node_b.node_id]
312 # subproc(f"\t\t{node_a.node_name} (port net '{node_a.net_name}') "
313 # f"↔︎ {node_b.node_name} (port net '{node_b.net_name}') "
314 # f"{round(element.resistance, 3)} Ω")
316 results.r_extraction_result = rex_result
318 return results