Coverage for klayout_pex/kpex_cli.py: 70%

551 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 15:48 +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 argparse 

27from datetime import datetime 

28from enum import StrEnum 

29import logging 

30import os 

31import os.path 

32from pathlib import Path 

33import rich.console 

34import rich.markdown 

35import rich.text 

36from rich_argparse import RichHelpFormatter 

37import shlex 

38import shutil 

39import sys 

40from typing import * 

41 

42import klayout.db as kdb 

43import klayout.rdb as rdb 

44 

45from .common.path_validation import validate_files, FileValidationResult 

46from .env import EnvVar, Env 

47from .extraction_engine import ExtractionEngine 

48from .fastercap.fastercap_input_builder import FasterCapInputBuilder 

49from .fastercap.fastercap_model_generator import FasterCapModelGenerator 

50from .fastercap.fastercap_runner import run_fastercap, fastercap_parse_capacitance_matrix 

51from .fastcap.fastcap_runner import run_fastcap, fastcap_parse_capacitance_matrix 

52from .klayout.lvs_runner import LVSRunner 

53from .klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo 

54from .klayout.netlist_expander import NetlistExpander 

55from .klayout.netlist_csv import NetlistCSVWriter 

56from .klayout.netlist_printer import NetlistPrinter 

57from .klayout.netlist_reducer import NetlistReducer 

58from .klayout.repair_rdb import repair_rdb 

59from .log import ( 

60 LogLevel, 

61 set_log_level, 

62 register_additional_handler, 

63 deregister_additional_handler, 

64 # console, 

65 # debug, 

66 info, 

67 warning, 

68 subproc, 

69 error, 

70 rule 

71) 

72from .magic.magic_ext_file_parser import parse_magic_pex_run 

73from .magic.magic_runner import ( 

74 MagicPEXMode, 

75 MagicShortMode, 

76 MagicMergeMode, 

77 MagicUniqueMode, 

78 run_magic, 

79 prepare_magic_script, 

80) 

81from .magic.magic_log_analyzer import MagicLogAnalyzer 

82from .pdk_config import PDK, PDKConfig 

83from .rcx25.extractor import RCX25Extractor, ExtractionResults 

84from .rcx25.netlist_expander import RCX25NetlistExpander 

85from .rcx25.pex_mode import PEXMode 

86from .tech_info import TechInfo 

87from .util.multiple_choice import MultipleChoicePattern 

88from .util.argparse_helpers import render_enum_help, true_or_false 

89from .version import __version__ 

90 

91 

92# ------------------------------------------------------------------------------------ 

93 

94PROGRAM_NAME = "kpex" 

95 

96 

97class ArgumentValidationError(Exception): 

98 pass 

99 

100 

101class InputMode(StrEnum): 

102 LVSDB = "lvsdb" 

103 GDS = "gds" 

104 

105 

106class KpexCLI: 

107 @staticmethod 

108 def parse_args(arg_list: List[str], 

109 env: Env) -> argparse.Namespace: 

110 # epilog = f"See '{PROGRAM_NAME} <subcommand> -h' for help on subcommand" 

111 epilog = EnvVar.help_epilog_table() 

112 epilog_md = rich.console.Group( 

113 rich.text.Text('Environmental variables:', style='argparse.groups'), 

114 rich.markdown.Markdown(epilog, style='argparse.text') 

115 ) 

116 main_parser = argparse.ArgumentParser(description=f"{PROGRAM_NAME}: " 

117 f"KLayout-integrated Parasitic Extraction Tool", 

118 epilog=epilog_md, 

119 add_help=False, 

120 formatter_class=RichHelpFormatter) 

121 

122 group_special = main_parser.add_argument_group("Special options") 

123 group_special.add_argument("--help", "-h", action='help', help="show this help message and exit") 

124 group_special.add_argument("--version", "-v", action='version', version=f'{PROGRAM_NAME} {__version__}') 

125 group_special.add_argument("--log_level", dest='log_level', default='subprocess', 

126 help=render_enum_help(topic='log_level', enum_cls=LogLevel)) 

127 group_special.add_argument("--threads", dest='num_threads', type=int, 

128 default=os.cpu_count() * 4, 

129 help="number of threads (e.g. for FasterCap) (default is %(default)s)") 

130 

131 group_pex = main_parser.add_argument_group("Parasitic Extraction Setup") 

132 

133 all_pdk_choices = list(PDK) + list(PDK.legacy_aliases().keys()) 

134 

135 default_pdk = env.default_pdk 

136 pdk_help = render_enum_help(topic='pdk', enum_cls=PDK, print_default=False) 

137 if default_pdk: 

138 pdk_help += f" (default is '{default_pdk}')" 

139 

140 group_pex.add_argument("--pdk", dest="pdk", required=default_pdk is None, 

141 type=PDK.from_string, choices=all_pdk_choices, 

142 help=pdk_help, default=default_pdk) 

143 

144 group_pex.add_argument("--out_dir", dest="output_dir_base_path", default="output", 

145 help="Run directory path (default is '%(default)s')") 

146 

147 group_pex.add_argument("--out_spice", "-o", dest="output_spice_path", default=None, 

148 help="Optional additional SPICE output path (default is none)") 

149 

150 group_pex_input = main_parser.add_argument_group("Parasitic Extraction Input", 

151 description="Either LVS is run, or an existing LVSDB is used") 

152 group_pex_input.add_argument("--gds", "-g", dest="gds_path", default=None, 

153 help="GDS path (for LVS)") 

154 group_pex_input.add_argument("--schematic", "-s", dest="schematic_path", 

155 help="Schematic SPICE netlist path (for LVS). " 

156 "If none given, a dummy schematic will be created") 

157 group_pex_input.add_argument("--lvsdb", "-l", dest="lvsdb_path", default=None, 

158 help="KLayout PEX-LVSDB path from a previous run (bypass PEX-LVS)") 

159 group_pex_input.add_argument("--cell", "-c", dest="cell_name", default=None, 

160 help="Cell (default is the top cell)") 

161 

162 group_pex_input.add_argument("--cache-lvs", dest="cache_lvs", 

163 type=true_or_false, default=True, 

164 help="Used cached LVSDB (for given input GDS) (default is %(default)s)") 

165 group_pex_input.add_argument("--cache-dir", dest="cache_dir_path", default=None, 

166 help="Path for cached LVSDB (default is .kpex_cache within --out_dir)") 

167 group_pex_input.add_argument("--lvs-verbose", dest="klayout_lvs_verbose", 

168 type=true_or_false, default=False, 

169 help="Verbose KLayout LVS output (default is %(default)s)") 

170 

171 group_pex_options = main_parser.add_argument_group("Parasitic Extraction Options") 

172 group_pex_options.add_argument("--blackbox", dest="blackbox_devices", 

173 type=true_or_false, default=False, # TODO: in the future this should be True by default 

174 help="Blackbox devices like MIM/MOM caps, as they are handled by SPICE models " 

175 "(default is %(default)s for testing now)") 

176 group_pex_options.add_argument("--fastercap", dest="run_fastercap", 

177 action='store_true', default=False, 

178 help="Run FasterCap engine (default is %(default)s)") 

179 group_pex_options.add_argument("--fastcap", dest="run_fastcap", 

180 action='store_true', default=False, 

181 help="Run FastCap2 engine (default is %(default)s)") 

182 group_pex_options.add_argument("--magic", dest="run_magic", 

183 action='store_true', default=False, 

184 help="Run MAGIC engine (default is %(default)s)") 

185 group_pex_options.add_argument("--2.5D", dest="run_2_5D", 

186 action='store_true', default=False, 

187 help="Run 2.5D analytical engine (default is %(default)s)") 

188 

189 group_fastercap = main_parser.add_argument_group("FasterCap options") 

190 group_fastercap.add_argument("--k_void", "-k", dest="k_void", 

191 type=float, default=3.9, 

192 help="Dielectric constant of void (default is %(default)s)") 

193 

194 # TODO: reflect that these are also now used by KPEX/2.5D engine! 

195 group_fastercap.add_argument("--delaunay_amax", "-a", dest="delaunay_amax", 

196 type=float, default=50, 

197 help="Delaunay triangulation maximum area (default is %(default)s)") 

198 group_fastercap.add_argument("--delaunay_b", "-b", dest="delaunay_b", 

199 type=float, default=0.5, 

200 help="Delaunay triangulation b (default is %(default)s)") 

201 group_fastercap.add_argument("--geo_check", dest="geometry_check", 

202 type=true_or_false, default=False, 

203 help=f"Validate geometries before passing to FasterCap " 

204 f"(default is False)") 

205 group_fastercap.add_argument("--diel", dest="dielectric_filter", 

206 type=str, default="all", 

207 help=f"Comma separated list of dielectric filter patterns. " 

208 f"Allowed patterns are: (none, all, -dielname1, +dielname2) " 

209 f"(default is %(default)s)") 

210 

211 group_fastercap.add_argument("--tolerance", dest="fastercap_tolerance", 

212 type=float, default=0.05, 

213 help="FasterCap -aX error tolerance (default is %(default)s)") 

214 group_fastercap.add_argument("--d_coeff", dest="fastercap_d_coeff", 

215 type=float, default=0.5, 

216 help=f"FasterCap -d direct potential interaction coefficient to mesh refinement " 

217 f"(default is %(default)s)") 

218 group_fastercap.add_argument("--mesh", dest="fastercap_mesh_refinement_value", 

219 type=float, default=0.5, 

220 help="FasterCap -m Mesh relative refinement value (default is %(default)s)") 

221 group_fastercap.add_argument("--ooc", dest="fastercap_ooc_condition", 

222 type=float, default=2, 

223 help="FasterCap -f out-of-core free memory to link memory condition " 

224 "(0 = don't go OOC, default is %(default)s)") 

225 group_fastercap.add_argument("--auto_precond", dest="fastercap_auto_preconditioner", 

226 type=true_or_false, default=True, 

227 help=f"FasterCap -ap Automatic preconditioner usage (default is %(default)s)") 

228 group_fastercap.add_argument("--galerkin", dest="fastercap_galerkin_scheme", 

229 action='store_true', default=False, 

230 help=f"FasterCap -g Use Galerkin scheme (default is %(default)s)") 

231 group_fastercap.add_argument("--jacobi", dest="fastercap_jacobi_preconditioner", 

232 action='store_true', default=False, 

233 help="FasterCap -pj Use Jacobi preconditioner (default is %(default)s)") 

234 

235 group_magic = main_parser.add_argument_group("MAGIC options") 

236 

237 default_magicrc_path = env.default_magicrc_path 

238 if default_magicrc_path: 

239 magicrc_help = f"Path to magicrc configuration file (default is '{default_magicrc_path}')" 

240 else: 

241 magicrc_help = "Path to magicrc configuration file "\ 

242 "(default not available, PDK and PDK_ROOT must be set!)" 

243 

244 group_magic.add_argument('--magicrc', dest='magicrc_path', default=default_magicrc_path, 

245 help=magicrc_help) 

246 group_magic.add_argument("--magic_mode", dest='magic_pex_mode', 

247 default=MagicPEXMode.DEFAULT, type=MagicPEXMode, choices=list(MagicPEXMode), 

248 help=render_enum_help(topic='magic_mode', enum_cls=MagicPEXMode)) 

249 group_magic.add_argument("--magic_cthresh", dest="magic_cthresh", 

250 type=float, default=0.01, 

251 help="Threshold (in fF) for ignored parasitic capacitances (default is %(default)s). " 

252 "(MAGIC command: ext2spice cthresh <value>)") 

253 group_magic.add_argument("--magic_rthresh", dest="magic_rthresh", 

254 type=int, default=100, 

255 help="Threshold (in Ω) for ignored parasitic resistances (default is %(default)s). " 

256 "(MAGIC command: ext2spice rthresh <value>)") 

257 group_magic.add_argument("--magic_thresh", dest="magic_thresh", 

258 type=int, default=None, 

259 help="Lumped resistance threshold (in mΩ) to trigger network extraction. " 

260 "If not supplied, MAGIC's default is implicitly used. " 

261 "(MAGIC command: extresist threshold <value>)") 

262 group_magic.add_argument("--magic_minres", dest="magic_minres", 

263 type=int, default=None, 

264 help="Threshold (in mΩ) for removing individual resistors during network simplification. " 

265 "If not supplied, MAGIC's default is implicitly used. " 

266 "(MAGIC command: extresist minres <value>)") 

267 group_magic.add_argument("--magic_mindel", dest="magic_mindel", 

268 type=int, default=None, 

269 help="Minimum delay threshold (in ps) to trigger network extraction. " 

270 "If not supplied, MAGIC's default is implicitly used. " 

271 "(MAGIC command: extresist mindelay <value>)") 

272 group_magic.add_argument("--magic_halo", dest="magic_halo", 

273 type=float, default=None, 

274 help="Custom sidewall halo distance (in µm) " 

275 "(MAGIC command: extract halo <value>) (default is no custom halo)") 

276 group_magic.add_argument("--magic_short", dest='magic_short_mode', 

277 default=MagicShortMode.DEFAULT, type=MagicShortMode, choices=list(MagicShortMode), 

278 help=render_enum_help(topic='magic_short', enum_cls=MagicShortMode)) 

279 group_magic.add_argument("--magic_merge", dest='magic_merge_mode', 

280 default=MagicMergeMode.DEFAULT, type=MagicMergeMode, choices=list(MagicMergeMode), 

281 help=render_enum_help(topic='magic_merge', enum_cls=MagicMergeMode)) 

282 group_magic.add_argument("--magic_pre_flatten", dest='magic_pre_flatten', 

283 action='store_true', default=False, 

284 help="Flatten GDS before reading it with Magic (default is %(default)s).") 

285 group_magic.add_argument("--magic_unique", dest='magic_unique_mode', 

286 default=MagicUniqueMode.DEFAULT, type=MagicUniqueMode, choices=list(MagicUniqueMode), 

287 help="Control net uniqueness during extraction " 

288 " ('implicit' omits the command, uses MAGIC's default value). " 

289 + render_enum_help(topic='magic_unique', enum_cls=MagicUniqueMode)) 

290 

291 group_25d = main_parser.add_argument_group("2.5D options") 

292 group_25d.add_argument("--mode", dest='pex_mode', 

293 default=PEXMode.DEFAULT, type=PEXMode, choices=list(PEXMode), 

294 help=render_enum_help(topic='mode', enum_cls=PEXMode)) 

295 group_25d.add_argument("--halo", dest="halo", 

296 type=float, default=None, 

297 help="Custom sidewall halo distance (in µm) to override tech info " 

298 "(default is no custom halo)") 

299 group_25d.add_argument("--scale", dest="scale_ratio_to_fit_halo", 

300 type=true_or_false, default=True, 

301 help=f"Scale fringe ratios, so that halo distance is 100%% (default is %(default)s)") 

302 

303 if arg_list is None: 

304 arg_list = sys.argv[1:] 

305 args = main_parser.parse_args(arg_list) 

306 

307 # environmental variables and their defaults 

308 args.fastcap_exe_path = env[EnvVar.FASTCAP_EXE] 

309 args.fastercap_exe_path = env[EnvVar.FASTERCAP_EXE] 

310 args.klayout_exe_path = env[EnvVar.KLAYOUT_EXE] 

311 args.magic_exe_path = env[EnvVar.MAGIC_EXE] 

312 

313 return args 

314 

315 @staticmethod 

316 def validate_args(args: argparse.Namespace): 

317 found_errors = False 

318 

319 pdk_config: PDKConfig = args.pdk.config 

320 args.tech_pbjson_path = pdk_config.tech_pb_json_path 

321 args.lvs_script_path = pdk_config.pex_lvs_script_path 

322 

323 def input_file_stem(path: str): 

324 # could be *.gds, or *.gds.gz, so remove all extensions 

325 return os.path.basename(path).split(sep='.')[0] 

326 

327 if not os.path.isfile(args.klayout_exe_path): 

328 path = shutil.which(args.klayout_exe_path) 

329 if not path: 

330 error(f"Can't locate KLayout executable at {args.klayout_exe_path}") 

331 found_errors = True 

332 

333 if not os.path.isfile(args.tech_pbjson_path): 

334 error(f"Can't read technology file at path {args.tech_pbjson_path}") 

335 found_errors = True 

336 

337 if not os.path.isfile(args.lvs_script_path): 

338 error(f"Can't locate LVS script path at {args.lvs_script_path}") 

339 found_errors = True 

340 

341 rule('Input Layout') 

342 

343 # check engines VS input possiblities 

344 match (args.run_magic, args.run_fastcap, args.run_fastercap, args.run_2_5D, 

345 args.gds_path, args.lvsdb_path): 

346 case (True, _, _, _, None, _): 

347 error(f"Running PEX engine MAGIC requires --gds (--lvsdb not possible)") 

348 found_errors = True 

349 case (False, False, False, False, _, _): # at least one engine must be activated 

350 error("No PEX engines activated") 

351 engine_help = """ 

352 | Argument | Description | 

353 | ------------ | ------------------------------- | 

354 | --2.5D | Run KPEX/2.5D analytical engine | 

355 | --fastercap | Run KPEX/FastCap 3D engine | 

356 | --fastercap | Run KPEX/FasterCap 3D engine | 

357 | --magic | Run MAGIC wrapper engine | 

358 """ 

359 subproc(f"\n\nPlease activate one or more engines using the arguments:") 

360 rich.print(rich.markdown.Markdown(engine_help, style='argparse.text')) 

361 found_errors = True 

362 case (_, _, _, _, None, None): 

363 error(f"Neither GDS nor LVSDB was provided") 

364 found_errors = True 

365 

366 # check if we find magicrc 

367 if args.run_magic: 

368 if args.magicrc_path is None: 

369 error(f"magicrc not available, requires any those:\n" 

370 f"\t• set environmental variables PDK_ROOT / PDK\n" 

371 f"\t• pass argument --magicrc") 

372 found_errors = True 

373 else: 

374 result = validate_files([args.magicrc_path]) 

375 for f in result.failures: 

376 error(f"Invalid magicrc: {f.reason} at {str(f.path)}") 

377 found_errors = True 

378 

379 # input mode: LVS or existing LVSDB? 

380 if args.gds_path: 

381 info(f"GDS input file passed, running in LVS mode") 

382 args.input_mode = InputMode.GDS 

383 if not os.path.isfile(args.gds_path): 

384 error(f"Can't read GDS file (LVS input) at path {args.gds_path}") 

385 found_errors = True 

386 else: 

387 args.layout = kdb.Layout() 

388 args.layout.read(args.gds_path) 

389 

390 top_cells = args.layout.top_cells() 

391 

392 if args.cell_name: # explicit user-specified cell name 

393 args.effective_cell_name = args.cell_name 

394 

395 found_cell: Optional[kdb.Cell] = None 

396 for cell in args.layout.cells('*'): 

397 if cell.name == args.effective_cell_name: 

398 found_cell = cell 

399 break 

400 if not found_cell: 

401 error(f"Could not find cell {args.cell_name} in GDS {args.gds_path}") 

402 found_errors = True 

403 

404 is_only_top_cell = len(top_cells) == 1 and top_cells[0].name == args.cell_name 

405 if is_only_top_cell: 

406 info(f"Found cell {args.cell_name} in GDS {args.gds_path} (only top cell)") 

407 else: # there are other cells => extract the top cell to a tmp layout 

408 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}" 

409 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id) 

410 os.makedirs(args.output_dir_path, exist_ok=True) 

411 args.effective_gds_path = os.path.join(args.output_dir_path, 

412 f"{args.cell_name}_exported.gds.gz") 

413 info(f"Found cell {args.cell_name} in GDS {args.gds_path}, " 

414 f"but it is not the only top cell, " 

415 f"so layout is exported to: {args.effective_gds_path}") 

416 

417 found_cell.write(args.effective_gds_path) 

418 else: # find top cell 

419 if len(top_cells) == 1: 

420 args.effective_cell_name = top_cells[0].name 

421 info(f"No explicit top cell specified, using top cell '{args.effective_cell_name}'") 

422 else: 

423 args.effective_cell_name = 'TOP' 

424 error(f"Could not determine the default top cell in GDS {args.gds_path}, " 

425 f"there are multiple: {', '.join([c.name for c in top_cells])}. " 

426 f"Use --cell to specify the cell") 

427 found_errors = True 

428 

429 if not hasattr(args, 'effective_gds_path'): 

430 args.effective_gds_path = args.gds_path 

431 elif args.lvsdb_path is not None: 

432 info(f"LVSDB input file passed, bypassing LVS") 

433 args.input_mode = InputMode.LVSDB 

434 if not os.path.isfile(args.lvsdb_path): 

435 error(f"Can't read KLayout LVSDB file at path {args.lvsdb_path}") 

436 found_errors = True 

437 else: 

438 lvsdb = kdb.LayoutVsSchematic() 

439 lvsdb.read(args.lvsdb_path) 

440 top_cell: kdb.Cell = lvsdb.internal_top_cell() 

441 args.effective_cell_name = top_cell.name 

442 

443 if hasattr(args, 'effective_cell_name'): 

444 run_dir_id: str 

445 match args.input_mode: 

446 case InputMode.GDS: 

447 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}" 

448 case InputMode.LVSDB: 

449 run_dir_id = f"{input_file_stem(args.lvsdb_path)}__{args.effective_cell_name}" 

450 case _: 

451 raise NotImplementedError(f"Unknown input mode {args.input_mode}") 

452 

453 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id) 

454 os.makedirs(args.output_dir_path, exist_ok=True) 

455 if args.input_mode == InputMode.GDS: 

456 if args.schematic_path: 

457 args.effective_schematic_path = args.schematic_path 

458 if not os.path.isfile(args.schematic_path): 

459 error(f"Can't read schematic (LVS input) at path {args.schematic_path}") 

460 found_errors = True 

461 else: 

462 info(f"LVS input schematic not specified (argument --schematic), using dummy schematic") 

463 args.effective_schematic_path = os.path.join(args.output_dir_path, 

464 f"{args.effective_cell_name}_dummy_schematic.spice") 

465 with open(args.effective_schematic_path, 'w', encoding='utf-8') as f: 

466 f.writelines([ 

467 f".subckt {args.effective_cell_name} VDD VSS\n", 

468 '.ends\n', 

469 '.end\n' 

470 ]) 

471 

472 try: 

473 args.log_level = LogLevel[args.log_level.upper()] 

474 except KeyError: 

475 error(f"Requested log level {args.log_level.lower()} does not exist, " 

476 f"{render_enum_help(topic='log_level', enum_cls=LogLevel, print_default=False)}") 

477 found_errors = True 

478 

479 try: 

480 pattern_string: str = args.dielectric_filter 

481 args.dielectric_filter = MultipleChoicePattern(pattern=pattern_string) 

482 except ValueError as e: 

483 error("Failed to parse --diel arg", e) 

484 found_errors = True 

485 

486 if args.cache_dir_path is None: 

487 args.cache_dir_path = os.path.join(args.output_dir_base_path, '.kpex_cache') 

488 

489 if found_errors: 

490 raise ArgumentValidationError("Argument validation failed") 

491 

492 def create_netlist_printer(self, 

493 args: argparse.Namespace, 

494 extraction_engine: ExtractionEngine): 

495 printer = NetlistPrinter(extraction_engine=extraction_engine, 

496 pdk=args.pdk) 

497 return printer 

498 

499 def build_fastercap_input(self, 

500 args: argparse.Namespace, 

501 pex_context: KLayoutExtractionContext, 

502 tech_info: TechInfo) -> str: 

503 rule('Process stackup') 

504 fastercap_input_builder = FasterCapInputBuilder(pex_context=pex_context, 

505 tech_info=tech_info, 

506 k_void=args.k_void, 

507 delaunay_amax=args.delaunay_amax, 

508 delaunay_b=args.delaunay_b) 

509 gen: FasterCapModelGenerator = fastercap_input_builder.build() 

510 

511 rule('FasterCap Input File Generation') 

512 faster_cap_input_dir_path = os.path.join(args.output_dir_path, 'FasterCap_Input_Files') 

513 os.makedirs(faster_cap_input_dir_path, exist_ok=True) 

514 

515 lst_file = gen.write_fastcap(output_dir_path=faster_cap_input_dir_path, prefix='FasterCap_Input_') 

516 

517 rule('STL File Generation') 

518 geometry_dir_path = os.path.join(args.output_dir_path, 'Geometries') 

519 os.makedirs(geometry_dir_path, exist_ok=True) 

520 gen.dump_stl(output_dir_path=geometry_dir_path, prefix='') 

521 

522 if args.geometry_check: 

523 rule('Geometry Validation') 

524 gen.check() 

525 

526 return lst_file 

527 

528 

529 def run_fastercap_extraction(self, 

530 args: argparse.Namespace, 

531 pex_context: KLayoutExtractionContext, 

532 lst_file: str): 

533 rule('FasterCap Execution') 

534 info(f"Configure number of OpenMP threads (environmental variable OMP_NUM_THREADS) as {args.num_threads}") 

535 os.environ['OMP_NUM_THREADS'] = f"{args.num_threads}" 

536 

537 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Output.txt") 

538 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Raw.csv") 

539 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Avg.csv") 

540 expanded_netlist_path = os.path.join(args.output_dir_path, 

541 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.cir") 

542 expanded_netlist_csv_path = os.path.join(args.output_dir_path, 

543 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.csv") 

544 reduced_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Reduced_Netlist.cir") 

545 

546 run_fastercap(exe_path=args.fastercap_exe_path, 

547 lst_file_path=lst_file, 

548 log_path=log_path, 

549 tolerance=args.fastercap_tolerance, 

550 d_coeff=args.fastercap_d_coeff, 

551 mesh_refinement_value=args.fastercap_mesh_refinement_value, 

552 ooc_condition=args.fastercap_ooc_condition, 

553 auto_preconditioner=args.fastercap_auto_preconditioner, 

554 galerkin_scheme=args.fastercap_galerkin_scheme, 

555 jacobi_preconditioner=args.fastercap_jacobi_preconditioner) 

556 

557 cap_matrix = fastercap_parse_capacitance_matrix(log_path) 

558 cap_matrix.write_csv(raw_csv_path) 

559 

560 cap_matrix = cap_matrix.averaged_off_diagonals() 

561 cap_matrix.write_csv(avg_csv_path) 

562 

563 netlist_expander = NetlistExpander() 

564 expanded_netlist = netlist_expander.expand( 

565 extracted_netlist=pex_context.lvsdb.netlist(), 

566 top_cell_name=pex_context.annotated_top_cell.name, 

567 cap_matrix=cap_matrix, 

568 blackbox_devices=args.blackbox_devices 

569 ) 

570 

571 # create a nice CSV for reports, useful for spreadsheets 

572 netlist_csv_writer = NetlistCSVWriter() 

573 netlist_csv_writer.write_csv(netlist=expanded_netlist, 

574 top_cell_name=pex_context.annotated_top_cell.name, 

575 output_path=expanded_netlist_csv_path) 

576 

577 rule("Extended netlist (CSV format):") 

578 with open(expanded_netlist_csv_path, 'r') as f: 

579 for line in f.readlines(): 

580 subproc(line[:-1]) # abusing subproc, simply want verbatim 

581 rule() 

582 

583 info(f"Wrote expanded netlist CSV to: {expanded_netlist_csv_path}") 

584 

585 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTERCAP) 

586 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

587 info(f"Wrote expanded netlist to: {expanded_netlist_path}") 

588 

589 # FIXME: should this be already reduced? 

590 if args.output_spice_path: 

591 netlist_printer.write(expanded_netlist, args.output_spice_path) 

592 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}") 

593 

594 netlist_reducer = NetlistReducer() 

595 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist, 

596 top_cell_name=pex_context.annotated_top_cell.name) 

597 netlist_printer.write(reduced_netlist, reduced_netlist_path) 

598 info(f"Wrote reduced netlist to: {reduced_netlist_path}") 

599 

600 self._fastercap_extracted_csv_path = expanded_netlist_csv_path 

601 

602 def run_magic_extraction(self, 

603 args: argparse.Namespace): 

604 if args.input_mode != InputMode.GDS: 

605 error(f"MAGIC engine only works with GDS input mode" 

606 f" (currently {args.input_mode})") 

607 return 

608 

609 magic_run_dir = os.path.join(args.output_dir_path, f"magic_{args.magic_pex_mode}") 

610 magic_log_path = os.path.join(magic_run_dir, 

611 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Output.txt") 

612 magic_script_path = os.path.join(magic_run_dir, 

613 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Script.tcl") 

614 

615 output_netlist_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}.pex.spice") 

616 report_db_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}_MAGIC_report.rdb.gz") 

617 

618 os.makedirs(magic_run_dir, exist_ok=True) 

619 

620 layout = kdb.Layout() 

621 layout.read(args.effective_gds_path) 

622 

623 if args.magic_pre_flatten: 

624 tops = list(layout.top_cells()) 

625 if not tops: 

626 raise Exception("No top cells found in the input layout.") 

627 for top in tops: 

628 top.flatten(True) 

629 flattened_gds_path=os.path.join(magic_run_dir, f"{args.effective_cell_name}.gds") 

630 layout.write(flattened_gds_path) 

631 gds_path = flattened_gds_path 

632 else: 

633 gds_path = args.effective_gds_path 

634 

635 prepare_magic_script(gds_path=gds_path, 

636 cell_name=args.effective_cell_name, 

637 run_dir_path=magic_run_dir, 

638 script_path=magic_script_path, 

639 output_netlist_path=output_netlist_path, 

640 pex_mode=args.magic_pex_mode, 

641 c_threshold=args.magic_cthresh, 

642 r_threshold=args.magic_rthresh, 

643 threshold=args.magic_thresh, 

644 min_res=args.magic_minres, 

645 min_delay=args.magic_mindel, 

646 halo=args.magic_halo, 

647 short_mode=args.magic_short_mode, 

648 merge_mode=args.magic_merge_mode, 

649 unique_mode=args.magic_unique_mode) 

650 

651 run_magic(exe_path=args.magic_exe_path, 

652 magicrc_path=args.magicrc_path, 

653 script_path=magic_script_path, 

654 log_path=magic_log_path) 

655 

656 magic_pex_run = parse_magic_pex_run(Path(magic_run_dir)) 

657 

658 report = rdb.ReportDatabase('') 

659 magic_log_analyzer = MagicLogAnalyzer(magic_pex_run=magic_pex_run, 

660 report=report, 

661 dbu=layout.dbu) 

662 magic_log_analyzer.analyze() 

663 report.save(report_db_path) 

664 

665 rule("Paths") 

666 subproc(f"Report DB saved at: {report_db_path}") 

667 subproc(f"SPICE netlist saved at: {output_netlist_path}") 

668 

669 if os.path.exists(output_netlist_path): 

670 if args.output_spice_path and os.path.exists(output_netlist_path): 

671 shutil.copy(output_netlist_path, args.output_spice_path) 

672 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}") 

673 

674 rule("MAGIC PEX SPICE netlist") 

675 with open(output_netlist_path, 'r') as f: 

676 subproc(f.read()) 

677 rule() 

678 

679 def run_fastcap_extraction(self, 

680 args: argparse.Namespace, 

681 pex_context: KLayoutExtractionContext, 

682 lst_file: str): 

683 rule('FastCap2 Execution') 

684 

685 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Output.txt") 

686 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Raw.csv") 

687 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Avg.csv") 

688 expanded_netlist_path = os.path.join(args.output_dir_path, 

689 f"{args.effective_cell_name}_FastCap2_Expanded_Netlist.cir") 

690 reduced_netlist_path = os.path.join(args.output_dir_path, 

691 f"{args.effective_cell_name}_FastCap2_Reduced_Netlist.cir") 

692 

693 run_fastcap(exe_path=args.fastcap_exe_path, 

694 lst_file_path=lst_file, 

695 log_path=log_path) 

696 

697 cap_matrix = fastcap_parse_capacitance_matrix(log_path) 

698 cap_matrix.write_csv(raw_csv_path) 

699 

700 cap_matrix = cap_matrix.averaged_off_diagonals() 

701 cap_matrix.write_csv(avg_csv_path) 

702 

703 netlist_expander = NetlistExpander() 

704 expanded_netlist = netlist_expander.expand( 

705 extracted_netlist=pex_context.lvsdb.netlist(), 

706 top_cell_name=pex_context.annotated_top_cell.name, 

707 cap_matrix=cap_matrix, 

708 blackbox_devices=args.blackbox_devices 

709 ) 

710 

711 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTCAP2) 

712 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

713 info(f"Wrote expanded netlist to: {expanded_netlist_path}") 

714 

715 # FIXME: should this be already reduced? 

716 if args.output_spice_path: 

717 netlist_printer.write(expanded_netlist, args.output_spice_path) 

718 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}") 

719 

720 netlist_reducer = NetlistReducer() 

721 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist, 

722 top_cell_name=pex_context.annotated_top_cell.name) 

723 netlist_printer.write(reduced_netlist, reduced_netlist_path) 

724 

725 info(f"Wrote reduced netlist to: {reduced_netlist_path}") 

726 

727 def run_kpex_2_5d_engine(self, 

728 args: argparse.Namespace, 

729 pex_context: KLayoutExtractionContext, 

730 tech_info: TechInfo, 

731 report_path: str, 

732 netlist_csv_path: Optional[str], 

733 expanded_netlist_path: Optional[str]): 

734 # TODO: make this separatly configurable 

735 # for now we use 0 

736 args.rcx25d_delaunay_amax = 0 

737 args.rcx25d_delaunay_b = 0.5 

738 

739 extractor = RCX25Extractor(pex_context=pex_context, 

740 pex_mode=args.pex_mode, 

741 delaunay_amax=args.rcx25d_delaunay_amax, 

742 delaunay_b=args.rcx25d_delaunay_b, 

743 scale_ratio_to_fit_halo=args.scale_ratio_to_fit_halo, 

744 tech_info=tech_info, 

745 report_path=report_path) 

746 extraction_results = extractor.extract() 

747 

748 if netlist_csv_path is not None: 

749 # TODO: merge this with klayout_pex/klayout/netlist_csv.py 

750 

751 with open(netlist_csv_path, 'w', encoding='utf-8') as f: 

752 summary = extraction_results.summarize() 

753 

754 f.write('Device;Net1;Net2;Capacitance [fF];Resistance [Ω]\n') 

755 for idx, (key, cap_value) in enumerate(sorted(summary.capacitances.items())): 

756 f.write(f"C{idx + 1};{key.net1};{key.net2};{round(cap_value, 3)};\n") 

757 for idx, (key, res_value) in enumerate(sorted(summary.resistances.items())): 

758 f.write(f"R{idx + 1};{key.net1};{key.net2};;{round(res_value, 3)}\n") 

759 

760 rule('kpex/2.5D extracted netlist (CSV format)') 

761 with open(netlist_csv_path, 'r') as f: 

762 for line in f.readlines(): 

763 subproc(line[:-1]) # abusing subproc, simply want verbatim 

764 

765 rule('Extracted netlist CSV') 

766 subproc(f"{netlist_csv_path}") 

767 

768 if expanded_netlist_path is not None: 

769 rule('kpex/2.5D extracted netlist (SPICE format)') 

770 netlist_expander = RCX25NetlistExpander() 

771 expanded_netlist = netlist_expander.expand( 

772 extracted_netlist=pex_context.lvsdb.netlist(), 

773 top_cell_name=pex_context.annotated_top_cell.name, 

774 extraction_results=extraction_results, 

775 blackbox_devices=args.blackbox_devices 

776 ) 

777 

778 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.K25D) 

779 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

780 subproc(f"Wrote expanded netlist to: {expanded_netlist_path}") 

781 

782 # FIXME: should this be already reduced? 

783 if args.output_spice_path: 

784 netlist_printer.write(expanded_netlist, args.output_spice_path) 

785 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}") 

786 

787 # NOTE: there was a KLayout bug that some of the categories were lost, 

788 # so that the marker browser could not load the report file 

789 try: 

790 report = rdb.ReportDatabase('') 

791 report.load(report_path) # try loading rdb 

792 except Exception as e: 

793 rule("Repair broken marker DB") 

794 warning(f"Detected KLayout bug: RDB can't be loaded due to exception {e}") 

795 repair_rdb(report_path) 

796 

797 return extraction_results 

798 

799 def setup_logging(self, args: argparse.Namespace): 

800 def register_log_file_handler(log_path: str, 

801 formatter: Optional[logging.Formatter]) -> logging.Handler: 

802 handler = logging.FileHandler(log_path) 

803 handler.setLevel(LogLevel.SUBPROCESS) 

804 if formatter: 

805 handler.setFormatter(formatter) 

806 register_additional_handler(handler) 

807 return handler 

808 

809 def reregister_log_file_handler(handler: logging.Handler, 

810 log_path: str, 

811 formatter: Optional[logging.Formatter]): 

812 deregister_additional_handler(handler) 

813 handler.flush() 

814 handler.close() 

815 os.makedirs(args.output_dir_path, exist_ok=True) 

816 new_path = os.path.join(args.output_dir_path, os.path.basename(log_path)) 

817 if os.path.exists(new_path): 

818 ctime = os.path.getctime(new_path) 

819 dt = datetime.fromtimestamp(ctime) 

820 timestamp = dt.strftime('%Y-%m-%d_%H-%M-%S') 

821 backup_path = f"{new_path[:-4]}_{timestamp}.bak.log" 

822 shutil.move(new_path, backup_path) 

823 log_path = shutil.move(log_path, new_path) 

824 register_log_file_handler(log_path, formatter) 

825 

826 # setup preliminary logger 

827 cli_log_path_plain = os.path.join(args.output_dir_base_path, f"kpex_plain.log") 

828 cli_log_path_formatted = os.path.join(args.output_dir_base_path, f"kpex.log") 

829 formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') 

830 file_handler_plain = register_log_file_handler(cli_log_path_plain, None) 

831 file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter) 

832 try: 

833 self.validate_args(args) 

834 except ArgumentValidationError: 

835 if hasattr(args, 'output_dir_path'): 

836 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) 

837 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) 

838 sys.exit(1) 

839 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) 

840 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) 

841 

842 set_log_level(args.log_level) 

843 

844 @staticmethod 

845 def modification_date(filename: str) -> datetime: 

846 t = os.path.getmtime(filename) 

847 return datetime.fromtimestamp(t) 

848 

849 def create_lvsdb(self, args: argparse.Namespace) -> kdb.LayoutVsSchematic: 

850 lvsdb = kdb.LayoutVsSchematic() 

851 

852 match args.input_mode: 

853 case InputMode.LVSDB: 

854 lvsdb.read(args.lvsdb_path) 

855 case InputMode.GDS: 

856 lvs_log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_lvs.log") 

857 lvsdb_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}.lvsdb.gz") 

858 lvsdb_cache_path = os.path.join(args.cache_dir_path, args.pdk, 

859 os.path.splitroot(os.path.abspath(args.gds_path))[-1], 

860 f"{args.effective_cell_name}.lvsdb.gz") 

861 

862 lvs_needed = True 

863 

864 if args.cache_lvs: 

865 if not os.path.exists(lvsdb_cache_path): 

866 info(f"Cache miss: extracted LVSDB does not exist") 

867 subproc(lvsdb_cache_path) 

868 elif self.modification_date(lvsdb_cache_path) <= self.modification_date(args.gds_path): 

869 info(f"Cache miss: extracted LVSDB is older than the input GDS") 

870 subproc(lvsdb_cache_path) 

871 else: 

872 warning(f"Cache hit: Reusing cached LVSDB") 

873 subproc(lvsdb_cache_path) 

874 lvs_needed = False 

875 

876 if lvs_needed: 

877 lvs_runner = LVSRunner() 

878 lvs_runner.run_klayout_lvs(exe_path=args.klayout_exe_path, 

879 lvs_script=args.lvs_script_path, 

880 gds_path=args.effective_gds_path, 

881 schematic_path=args.effective_schematic_path, 

882 log_path=lvs_log_path, 

883 lvsdb_path=lvsdb_path, 

884 verbose=args.klayout_lvs_verbose) 

885 if args.cache_lvs: 

886 cache_dir_path = os.path.dirname(lvsdb_cache_path) 

887 if not os.path.exists(cache_dir_path): 

888 os.makedirs(cache_dir_path, exist_ok=True) 

889 shutil.copy(lvsdb_path, lvsdb_cache_path) 

890 

891 lvsdb.read(lvsdb_path) 

892 return lvsdb 

893 

894 def main(self, argv: List[str]): 

895 if '-v' not in argv and \ 

896 '--version' not in argv and \ 

897 '-h' not in argv and \ 

898 '--help' not in argv: 

899 rule('Command line arguments') 

900 subproc(' '.join(map(shlex.quote, sys.argv))) 

901 

902 env = Env.from_os_environ() 

903 args = self.parse_args(arg_list=argv[1:], env=env) 

904 

905 os.makedirs(args.output_dir_base_path, exist_ok=True) 

906 self.setup_logging(args) 

907 

908 tech_info = TechInfo.from_json(args.tech_pbjson_path, 

909 dielectric_filter=args.dielectric_filter) 

910 

911 if args.halo is not None: 

912 tech_info.tech.process_parasitics.side_halo = args.halo 

913 

914 if args.run_magic: 

915 rule('MAGIC') 

916 self.run_magic_extraction(args) 

917 

918 # no need to run LVS etc if only running magic engine 

919 if not (args.run_fastcap or args.run_fastercap or args.run_2_5D): 

920 return 

921 

922 rule('Prepare LVSDB') 

923 lvsdb = self.create_lvsdb(args) 

924 

925 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=args.effective_cell_name, 

926 lvsdb=lvsdb, 

927 tech=tech_info, 

928 blackbox_devices=args.blackbox_devices) 

929 rule('Non-empty layers in LVS database') 

930 for gds_pair, layer_info in pex_context.extracted_layers.items(): 

931 names = [l.lvs_layer_name for l in layer_info.source_layers] 

932 info(f"{gds_pair} -> ({' '.join(names)})") 

933 

934 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_extracted.oas") 

935 pex_context.annotated_layout.write(gds_path) 

936 

937 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_internal.oas") 

938 pex_context.lvsdb.internal_layout().write(gds_path) 

939 

940 def dump_layers(cell: str, 

941 layers: List[KLayoutExtractedLayerInfo], 

942 layout_dump_path: str): 

943 layout = kdb.Layout() 

944 layout.dbu = lvsdb.internal_layout().dbu 

945 

946 top_cell = layout.create_cell(cell) 

947 for ulyr in layers: 

948 li = kdb.LayerInfo(*ulyr.gds_pair) 

949 li.name = ulyr.lvs_layer_name 

950 layer = layout.insert_layer(li) 

951 layout.insert(top_cell.cell_index(), layer, ulyr.region.dup()) 

952 

953 layout.write(layout_dump_path) 

954 

955 if len(pex_context.unnamed_layers) >= 1: 

956 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_unnamed_LVS_layers.gds.gz") 

957 dump_layers(cell=args.effective_cell_name, 

958 layers=pex_context.unnamed_layers, 

959 layout_dump_path=layout_dump_path) 

960 

961 if len(pex_context.extracted_layers) >= 1: 

962 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_nonempty_LVS_layers.gds.gz") 

963 nonempty_layers = [l \ 

964 for layers in pex_context.extracted_layers.values() \ 

965 for l in layers.source_layers] 

966 dump_layers(cell=args.effective_cell_name, 

967 layers=nonempty_layers, 

968 layout_dump_path=layout_dump_path) 

969 else: 

970 error("No extracted layers found") 

971 sys.exit(1) 

972 

973 if args.run_fastcap or args.run_fastercap: 

974 lst_file = self.build_fastercap_input(args=args, 

975 pex_context=pex_context, 

976 tech_info=tech_info) 

977 if args.run_fastercap: 

978 self.run_fastercap_extraction(args=args, 

979 pex_context=pex_context, 

980 lst_file=lst_file) 

981 if args.run_fastcap: 

982 self.run_fastcap_extraction(args=args, 

983 pex_context=pex_context, 

984 lst_file=lst_file) 

985 

986 if args.run_2_5D: 

987 rule("kpex/2.5D PEX Engine") 

988 report_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_k25d_pex_report.rdb.gz") 

989 netlist_csv_path = os.path.abspath(os.path.join(args.output_dir_path, 

990 f"{args.effective_cell_name}_k25d_pex_netlist.csv")) 

991 netlist_spice_path = os.path.abspath(os.path.join(args.output_dir_path, 

992 f"{args.effective_cell_name}_k25d_pex_netlist.spice")) 

993 

994 self._rcx25_extraction_results = self.run_kpex_2_5d_engine( # NOTE: store for test case 

995 args=args, 

996 pex_context=pex_context, 

997 tech_info=tech_info, 

998 report_path=report_path, 

999 netlist_csv_path=netlist_csv_path, 

1000 expanded_netlist_path=netlist_spice_path 

1001 ) 

1002 

1003 self._rcx25_extracted_csv_path = netlist_csv_path 

1004 

1005 @property 

1006 def rcx25_extraction_results(self) -> ExtractionResults: 

1007 if not hasattr(self, '_rcx25_extraction_results'): 

1008 raise Exception('rcx25_extraction_results is not initialized, was run_kpex_2_5d_engine called?') 

1009 return self._rcx25_extraction_results 

1010 

1011 @property 

1012 def rcx25_extracted_csv_path(self) -> str: 

1013 if not hasattr(self, '_rcx25_extracted_csv_path'): 

1014 raise Exception('rcx25_extracted_csv_path is not initialized, was run_kpex_2_5d_engine called?') 

1015 return self._rcx25_extracted_csv_path 

1016 

1017 @property 

1018 def fastercap_extracted_csv_path(self) -> str: 

1019 if not hasattr(self, '_fastercap_extracted_csv_path'): 

1020 raise Exception('fastercap_extracted_csv_path is not initialized, was run_fastercap_extraction called?') 

1021 return self._fastercap_extracted_csv_path 

1022 

1023 

1024if __name__ == "__main__": 

1025 cli = KpexCLI() 

1026 cli.main(sys.argv)