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

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# 

25 

26import klayout.db as kdb 

27 

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 

45 

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 

52 

53 

54class CExtractionTechError(Exception): 

55 """ 

56 The tech info can't model the capacitances of the layout 

57 """ 

58 pass 

59 

60 

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 

77 

78 if "PolygonWithProperties" not in kdb.__all__: 

79 raise Exception("KLayout version does not support properties (needs 0.30 at least)") 

80 

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) 

84 

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 

89 

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}") 

94 

95 return shapes 

96 

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 

105 

106 substrate_region = kdb.Region() 

107 substrate_region.enable_properties() 

108 

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})) 

112 

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')) 

122 

123 insert(rest, self.tech_info.internal_substrate_layer_name) 

124 return substrate_region 

125 

126 def extract(self) -> ExtractionResults: 

127 extraction_results = ExtractionResults() 

128 

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) 

134 

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 

146 

147 extraction_results.cell_extraction_results[cell_name] = cell_extraction_results 

148 

149 extraction_report.save(self.report_path) 

150 

151 return extraction_results 

152 

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 # ------------------------------------------------------------------------ 

159 

160 layer_regions_by_name: Dict[LayerName, kdb.Region] = defaultdict(kdb.Region) 

161 

162 all_region = kdb.Region() 

163 all_region.enable_properties() 

164 

165 layer_regions_by_name[self.tech_info.internal_substrate_layer_name] = self.substrate_region() 

166 

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() 

177 

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) 

181 

182 previous_via_name: Optional[str] = None 

183 

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] 

188 

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() 

194 

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 

198 

199 if metal_layer.metal_layer.HasField('contact_above'): 

200 contact = metal_layer.metal_layer.contact_above 

201 

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 

208 

209 previous_via_name = contact.name 

210 else: 

211 previous_via_name = None 

212 

213 all_layer_names = list(layer_regions_by_name.keys()) 

214 

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 ) 

233 

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() 

240 

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() 

251 

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() 

263 

264 # ------------------------------------------------------------------------ 

265 if self.pex_mode.need_resistance(): 

266 c: kdb.Circuit = netlist.top_circuit() 

267 info(f"LVSDB: found {c.pin_count()}pins") 

268 

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) 

282 

283 rex_result = r_extractor.extract(rex_request) 

284 report.output_rex_result(result=rex_result) 

285 

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)} Ω") 

315 

316 results.r_extraction_result = rex_result 

317 

318 return results