Coverage for klayout_pex/kpex_cli.py: 83%

950 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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 

27import contextlib 

28from datetime import datetime 

29from enum import StrEnum 

30import functools 

31import logging 

32import os 

33import os.path 

34from pathlib import Path 

35import google.protobuf.json_format 

36import rich.console 

37import rich.markdown 

38import rich.text 

39from rich_argparse import RichHelpFormatter 

40import shlex 

41import shutil 

42import sys 

43from typing import * 

44 

45import klayout.db as kdb 

46import klayout.rdb as rdb 

47 

48from .klayout import capacitance_matrix_interpreter 

49from .common.path_validation import validate_files, FileValidationResult 

50from .env import EnvVar, Env 

51from .extraction_engine import ExtractionEngine 

52from .fastercap.fastercap_input_builder import FasterCapInputBuilder 

53from .fastercap.fastercap_model_generator import FasterCapModelGenerator 

54from .fastercap.fastercap_error import FasterCapError 

55from .fastercap.fastercap_runner import run_fastercap, fastercap_parse_capacitance_matrix 

56from .fastercap.output_interpreter import FasterCapOutputInterpreter 

57from .fastcap.fastcap_runner import run_fastcap, fastcap_parse_capacitance_matrix 

58from .fastcap.output_interpreter import FastCapOutputInterpreter 

59from .klayout.lvs_cache import LVSCacheEntry, LVSInputFingerprint, lvs_input_fingerprint 

60from .klayout.lvs_runner import LVSError, LVSRunner 

61from .klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo, LVSDBError 

62from .klayout.netlist_expander import NetlistExpander 

63from .klayout.netlist_csv import NetlistCSVWriter 

64from .klayout.netlist_printer import NetlistHeader, NetlistPrinter 

65from .klayout.netlist_reducer import NetlistReducer 

66from .klayout.output_files import write_output_file 

67from .klayout.repair_rdb import repair_rdb 

68from .log import ( 

69 LogLevel, 

70 set_log_level, 

71 register_additional_handler, 

72 deregister_additional_handler, 

73 # console, 

74 # debug, 

75 info, 

76 warning, 

77 subproc, 

78 error, 

79 rule 

80) 

81from .magic.magic_ext_file_parser import parse_magic_pex_run 

82from .magic.magic_runner import ( 

83 MagicPEXMode, 

84 MagicShortMode, 

85 MagicMergeMode, 

86 MagicUniqueMode, 

87 run_magic, 

88 prepare_magic_script, 

89) 

90from .magic.magic_log_analyzer import MagicLogAnalyzer 

91from .netlist.netlist_convert import convert_to_vacask 

92from .netlist.netlist_format import NetlistFormat 

93from .netlist.spice_hierarchy import SpiceEvaluationError 

94from .netlist.spice_reader import SpiceReader, SpiceReaderError 

95from .netlist.vacask_writer import VacaskWriterError 

96from .pex25d.artifact import ( 

97 CANONICAL_SUFFIX, 

98 ArtifactFormat, 

99 ArtifactKind, 

100 ArtifactNamingError, 

101 infer_artifact_spec, 

102 named_kind_and_format, 

103) 

104from .klayout.pex25d_builder import DEFAULT_GRID_UM 

105from .pex25d.diagnostics import DiagnosticsReport, ExitCode, diagnostics_stream 

106from .pex25d.pex25d_cli import Pex25DCLI 

107from .netlist.netlist_reduction import ReductionMode, reduce_spice_netlist, reduced_netlist_path 

108from .netlist.rc_reduction import ReductionOptions 

109from .pdk_config import PDK, PDKConfig 

110from .rcx25.exclusion import ( 

111 BlockKind, 

112 CutLayers, 

113 ExclusionError, 

114 ExclusionMarkerMode, 

115 Exclusions, 

116 cells_called_by, 

117 check_instances_of_cut_cells, 

118 matches_name, 

119 parsed_cut_requests, 

120 selected_marker_layers, 

121) 

122from .rcx25.extractor import ( 

123 CExtractionTechError, 

124 ExtractionResults, 

125 RCX25Extractor, 

126 prepare_exclusions, 

127 warn_of_ports_without_nodes, 

128) 

129from .rcx25.netlist_checks import ( 

130 check_rc_netlist, 

131 check_schematic_device_classes, 

132 check_schematic_device_totals, 

133 first_names, 

134 RCNetlistCheckError, 

135) 

136from .rcx25.netlist_expander import RCX25NetlistExpander 

137from .rcx25.pex_mode import PEXMode 

138from .rcx25.r.r_extractor import RExtractionTechError 

139from .schematic_devices import schematic_devices 

140from .device_models import DeviceModelError, DeviceModels 

141from .tech_info import TechDefError, TechInfo 

142from .types import NetName 

143from .tool_version_constraints import ( 

144 Tool, 

145 VersionCheckMode, 

146 check_tool_versions 

147) 

148from .util.multiple_choice import MultipleChoicePattern 

149from .util.peak_memory import log_peak_memory 

150from .util.argparse_helpers import ( 

151 SectionedHelpFormatter, 

152 add_argument_with_former_name, 

153 add_help_subcommand, 

154 add_log_level_argument, 

155 add_subcommand_sections, 

156 handle_help_subcommand, 

157 positive_spice_seconds, 

158 render_enum_help, 

159 true_or_false, 

160 warn_deprecated_options, 

161) 

162from .version import __version__ 

163 

164 

165# ------------------------------------------------------------------------------------ 

166 

167PROGRAM_NAME = "kpex" 

168 

169 

170class ArgumentValidationError(Exception): 

171 pass 

172 

173 

174class InputMode(StrEnum): 

175 LVSDB = "lvsdb" 

176 GDS = "gds" 

177 

178 

179@functools.cache 

180def side_halo_by_pdk() -> Dict[PDK, float]: 

181 """ 

182 :return: the side halo (in µm) of the tech info of each PDK, but of those without a readable one, 

183 which fail their own runs (see validate_args), rather than the help of all 

184 """ 

185 side_halos: Dict[PDK, float] = {} 

186 for pdk in PDK: 

187 try: 

188 side_halos[pdk] = TechInfo.parse_tech_def(pdk.config.tech_pb_json_path).process_parasitics.side_halo 

189 except (OSError, TechDefError, google.protobuf.json_format.ParseError): 

190 continue 

191 return side_halos 

192 

193 

194def halo_default_help(halo_by_pdk: Mapping[str, float]) -> str: 

195 """ 

196 :param halo_by_pdk: the side halo (in µm) of each PDK (see side_halo_by_pdk) 

197 :return: the default of --halo for its help, the side halo of the tech info, 

198 e.g. "the tech info's, 8 µm in all PDKs" or "the tech info's, 8 µm in gf180mcuD, 10 µm in sky130A" 

199 """ 

200 halos = [f"{halo:g} µm in {pdk}" for pdk, halo in sorted(halo_by_pdk.items())] 

201 if len(set(halo_by_pdk.values())) == 1: 

202 halos = [f"{next(iter(halo_by_pdk.values())):g} µm in all PDKs"] 

203 return ', '.join(["the tech info's"] + halos) 

204 

205 

206class KpexCLI: 

207 # ---------------------------------------------------------- argument groups 

208 # 

209 # Each group is attached to a *given* parser rather than to one flat parser, 

210 # so that a subcommand declares exactly the options it can act on. That is 

211 # the point of the verb layer: `kpex pex25d` cannot be handed --magic-halo, 

212 # and the validation below no longer has to hand-check which combinations 

213 # of flags make sense together. 

214 

215 @staticmethod 

216 def _add_special_options(parser: argparse.ArgumentParser, 

217 include_threads: bool = False) -> None: 

218 group_special = parser.add_argument_group("Special Options") 

219 group_special.add_argument("--help", "-h", action='help', 

220 help="show this help message and exit") 

221 add_log_level_argument(group_special) 

222 add_argument_with_former_name(group_special, "--tool-version-checks", "--tool_version_checks", 

223 dest='version_check_mode', 

224 default=VersionCheckMode.DEFAULT, type=VersionCheckMode, 

225 choices=list(VersionCheckMode), 

226 help=render_enum_help(topic='tool_version_checks', 

227 enum_cls=VersionCheckMode)) 

228 if include_threads: 

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

230 default=os.cpu_count() * 4, 

231 help="number of threads (e.g. for FasterCap) " 

232 "(default is %(default)s)") 

233 

234 @staticmethod 

235 def _add_pex_setup_arguments(parser: argparse.ArgumentParser, 

236 env: Env, 

237 include_spice_output: bool = True) -> None: 

238 group_pex = parser.add_argument_group("Parasitic Extraction Setup") 

239 

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

241 

242 default_pdk = env.default_pdk 

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

244 if default_pdk: 

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

246 

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

248 type=PDK.from_string, choices=all_pdk_choices, 

249 help=pdk_help, default=default_pdk) 

250 

251 add_argument_with_former_name(group_pex, "--out-dir", "--out_dir", 

252 dest="output_dir_base_path", default="output", 

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

254 

255 if include_spice_output: 

256 add_argument_with_former_name(group_pex, "--out-netlist", "--out_netlist", "-o", 

257 dest="output_netlist_path", default=None, 

258 help="Optional additional output path of the netlist, " 

259 "in the --netlist-format (default is none)") 

260 # NOTE: deprecated, the name of --out-netlist before --netlist-format 

261 group_pex.add_argument("--out_spice", dest="output_spice_path", default=None, help=argparse.SUPPRESS) 

262 add_argument_with_former_name(group_pex, "--netlist-format", "--netlist_format", dest="netlist_format", 

263 default=NetlistFormat.SPICE.value, choices=[f.value for f in NetlistFormat], 

264 help="Format of the netlist the 2.5D engine writes to --out-netlist: " 

265 "'spice' or 'vacask' (for the VACASK simulator, converted from the " 

266 "SPICE netlist, as 'netlist convert' does, both are written into " 

267 "the run directory) (default is %(default)s)") 

268 

269 

270 @staticmethod 

271 def _add_pex_input_arguments(parser: argparse.ArgumentParser) -> None: 

272 group_pex_input = parser.add_argument_group("Parasitic Extraction Input", 

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

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

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

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

277 help="Schematic SPICE netlist path (--check checks its device classes " 

278 "and their totals only, use regular LVS to compare layout against schematic)") 

279 # Developer shortcut, not a supported entry point. It exists so that a 

280 # PEX25D or engine run can be repeated without paying for LVS again while 

281 # working on the code downstream of it. An LVSDB is tied to the kpex 

282 # version and PDK that produced it and is not an interchange format — 

283 # PEX25D is. Anyone who wants to hand a scene to somebody else, or keep 

284 # one, should be writing PEX25D, not passing LVSDBs around. 

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

286 help="KLayout PEX-LVSDB path from a previous run " 

287 "(bypass PEX-LVS, also with --gds, which then gives what the LVSDB " 

288 "lacks, e.g. the shapes of --exclusion-markers). Developer shortcut for " 

289 "re-running without repeating LVS; not an " 

290 "interchange format — use PEX25D for that") 

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

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

293 

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

295 type=true_or_false, default=True, 

296 help="Reuse the cached LVSDB while the LVS inputs are unchanged " 

297 "(layout, LVS deck, KLayout version) (default is %(default)s)") 

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

299 help="Path for cached LVSDB (default is .kpex_cache within --out-dir)") 

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

301 type=true_or_false, default=False, 

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

303 

304 

305 @staticmethod 

306 def _add_engine_arguments(parser: argparse.ArgumentParser) -> None: 

307 group_pex_options = parser.add_argument_group("Extraction Engines") 

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

309 action='store_true', default=False, 

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

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

312 action='store_true', default=False, 

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

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

315 action='store_true', default=False, 

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

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

318 action='store_true', default=False, 

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

320 

321 

322 @staticmethod 

323 def _add_extraction_options(parser: argparse.ArgumentParser) -> None: 

324 group_pex_options = parser.add_argument_group("Parasitic Extraction Options") 

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

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

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

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

329 

330 @staticmethod 

331 def _add_tech_arguments(parser: argparse.ArgumentParser) -> None: 

332 group_tech = parser.add_argument_group("Technology Options") 

333 group_tech.add_argument("--diel", dest="dielectric_filter", 

334 type=str, default="all", 

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

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

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

338 

339 @staticmethod 

340 def _add_fastercap_arguments(parser: argparse.ArgumentParser) -> None: 

341 group_fastercap = parser.add_argument_group("FasterCap Options") 

342 add_argument_with_former_name(group_fastercap, "--k-void", "--k_void", "-k", dest="k_void", 

343 type=float, default=3.9, 

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

345 

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

347 add_argument_with_former_name(group_fastercap, "--delaunay-amax", "--delaunay_amax", "-a", dest="delaunay_amax", 

348 type=float, default=50, 

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

350 add_argument_with_former_name(group_fastercap, "--delaunay-b", "--delaunay_b", "-b", dest="delaunay_b", 

351 type=float, default=0.5, 

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

353 add_argument_with_former_name(group_fastercap, "--geo-check", "--geo_check", dest="geometry_check", 

354 type=true_or_false, default=False, 

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

356 f"(default is False)") 

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

358 type=float, default=0.05, 

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

360 add_argument_with_former_name(group_fastercap, "--d-coeff", "--d_coeff", dest="fastercap_d_coeff", 

361 type=float, default=0.5, 

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

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

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

365 type=float, default=0.5, 

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

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

368 type=float, default=2, 

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

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

371 add_argument_with_former_name(group_fastercap, "--auto-precond", "--auto_precond", 

372 dest="fastercap_auto_preconditioner", 

373 type=true_or_false, default=True, 

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

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

376 action='store_true', default=False, 

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

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

379 action='store_true', default=False, 

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

381 

382 

383 @staticmethod 

384 def _add_magic_arguments(parser: argparse.ArgumentParser, env: Env) -> None: 

385 group_magic = parser.add_argument_group("MAGIC Options") 

386 

387 default_magicrc_path = env.default_magicrc_path 

388 if default_magicrc_path: 

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

390 else: 

391 magicrc_help = "Path to magicrc configuration file "\ 

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

393 

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

395 help=magicrc_help) 

396 add_argument_with_former_name(group_magic, "--magic-mode", "--magic_mode", dest='magic_pex_mode', 

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

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

399 add_argument_with_former_name(group_magic, "--magic-cthresh", "--magic_cthresh", dest="magic_cthresh", 

400 type=float, default=0.01, 

401 help="Threshold (in fF) for ignored parasitic capacitances " 

402 "(default is %(default)s). (MAGIC command: ext2spice cthresh <value>)") 

403 add_argument_with_former_name(group_magic, "--magic-rthresh", "--magic_rthresh", dest="magic_rthresh", 

404 type=float, default=100, 

405 help="Threshold (in Ω) for ignored parasitic resistances " 

406 "(default is %(default)s). (MAGIC command: ext2spice rthresh <value>)") 

407 add_argument_with_former_name(group_magic, "--magic-thresh", "--magic_thresh", dest="magic_thresh", 

408 type=float, default=None, 

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

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

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

412 add_argument_with_former_name(group_magic, "--magic-minres", "--magic_minres", dest="magic_minres", 

413 type=float, default=None, 

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

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

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

417 add_argument_with_former_name(group_magic, "--magic-mindel", "--magic_mindel", dest="magic_mindel", 

418 type=float, default=None, 

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

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

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

422 add_argument_with_former_name(group_magic, "--magic-halo", "--magic_halo", dest="magic_halo", 

423 type=float, default=None, 

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

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

426 add_argument_with_former_name(group_magic, "--magic-short", "--magic_short", dest='magic_short_mode', 

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

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

429 add_argument_with_former_name(group_magic, "--magic-merge", "--magic_merge", dest='magic_merge_mode', 

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

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

432 add_argument_with_former_name(group_magic, "--magic-pre-flatten", "--magic_pre_flatten", 

433 dest='magic_pre_flatten', 

434 action='store_true', default=False, 

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

436 add_argument_with_former_name(group_magic, "--magic-unique", "--magic_unique", dest='magic_unique_mode', 

437 default=MagicUniqueMode.DEFAULT, type=MagicUniqueMode, 

438 choices=list(MagicUniqueMode), 

439 help="Control net uniqueness during extraction " 

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

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

442 add_argument_with_former_name(group_magic, "--magic-analyze", "--magic_analyze", dest='magic_analyze', 

443 type=true_or_false, default=False, 

444 help="Analyze MAGIC extraction files (report as KLayout RDB)") 

445 

446 @staticmethod 

447 def _add_analytical_25d_arguments(parser: argparse.ArgumentParser) -> None: 

448 group_25d = parser.add_argument_group("2.5D Options") 

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

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

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

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

453 type=float, default=None, 

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

455 f"(default is {halo_default_help(side_halo_by_pdk())})") 

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

457 type=true_or_false, default=True, 

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

459 group_25d.add_argument("--check", dest="check_rc_netlist", 

460 type=true_or_false, default=None, 

461 help="Fail the run (after writing the netlist), if the extracted netlist is " 

462 "inconsistent with the LVS netlist, e.g. a device terminal off the resistor " 

463 "network of its net, a port that touches nothing, or a lost capacitance, " 

464 "or if the LVS netlist lacks a device class of --schematic " 

465 "or differs in its totals (e.g. Σ W·L of a MOS) " 

466 "(default is True for --mode RC, otherwise these are warnings)") 

467 group_25d.add_argument("--reduce", dest="reduce_mode", 

468 default=ReductionMode.DEFAULT.value, choices=[m.value for m in ReductionMode], 

469 help="Reduce the netlist of --mode R or RC, with the passes of 'netlist reduce': " 

470 "'floating' merges the floating islands and eliminates the capacitor-only nodes " 

471 "exactly, 'ticer' also eliminates the nodes whose time constant is at most " 

472 "--reduce-tau (the error grows with the frequency, and losses are not kept). " 

473 "Written next to the netlist (*_k25d_pex_netlist_reduced.spice), " 

474 "and to --out-netlist. " 

475 + render_enum_help(topic='reduce', enum_cls=ReductionMode)) 

476 add_argument_with_former_name(group_25d, "--reduce-tau", "--reduce_tau", dest="reduce_tau", metavar="TAU", 

477 type=positive_spice_seconds, default=None, 

478 help=f"TICER's time constant threshold in s for --reduce ticer, a SPICE number " 

479 f"like '0.1p' (default is {ReductionOptions().ticer_tau * 1e12:g}p)") 

480 add_argument_with_former_name(group_25d, "--report-caps", "--report_caps", dest="report_caps", 

481 type=true_or_false, default=False, 

482 help="Write each capacitance contribution with its shapes into the report " 

483 "(*_k25d_pex_report.rdb.gz), which takes much memory and time " 

484 "on large layouts (default is %(default)s)") 

485 

486 

487 @staticmethod 

488 def _add_exclusion_arguments(parser: argparse.ArgumentParser): 

489 group = parser.add_argument_group("Exclusion Options", 

490 "Leave parts of the layout to models or ideal sources, with the 2.5D engine " 

491 "or the field solvers. The netlists of the field solvers, and of --mode CC, " 

492 "have one node per net, so no ports for models") 

493 group.add_argument("--exclusion-markers", dest='exclusion_marker_mode', 

494 default=ExclusionMarkerMode.DEFAULT, type=ExclusionMarkerMode, 

495 choices=list(ExclusionMarkerMode), 

496 help="What to do with the exclusion markers of the PDK (e.g. IHP NoRCX.drawing), which " 

497 "mark what a model has the parasitics of: 'ignore' extracts what is within them, " 

498 "'tie' leaves out the capacitances within them and to the substrate, but not " 

499 "the shapes, which still shield the rest of the layout, nor their capacitances " 

500 "to it, and has no resistance within them, so that each net stays connected " 

501 "across them, 'ports' does the same, but each cut of a net at a marker and each " 

502 "device terminal within it is a port of the netlist, for a model to connect, " 

503 "named after the pin of the net within the marker nearest to it, otherwise " 

504 "<net>__m<marker>_<number>. Needs --gds, which has the markers. " 

505 + render_enum_help(topic='exclusion_markers', enum_cls=ExclusionMarkerMode)) 

506 group.add_argument("--exclusion-marker-layer", dest='exclusion_marker_layers', action='append', 

507 default=None, metavar='LAYER', 

508 help="An exclusion marker of the PDK by its name (e.g. IHP Metal3.noqrc), or any layer " 

509 "of the layout, as L/D for all layers of the stack, or as L/D:layer,layer for " 

510 "these (e.g. 235/5:met2,met3), which also is how a PDK without exclusion markers " 

511 "(sky130A, gf180mcuD) gets them. Repeatable, it replaces the markers " 

512 "the PDK has on by default, e.g. NoRCX.drawing") 

513 group.add_argument("--cut", dest='cut_specifications', action='append', default=None, 

514 metavar='CELL[=PREFIX]', 

515 help="Leave the instances of the cell (a name, or a glob pattern) to a model, " 

516 "like what an exclusion marker marks, but only on the shapes of the cell, so that " 

517 "routing over it stays extracted, and leave out its devices. Where a net enters " 

518 "it is a port of the netlist: PREFIX__PIN by a label of the cell on the net, " 

519 "by the terminal of a removed device, otherwise PREFIX__NET_NUMBER, the prefix " 

520 "by default the name of the cell and a number. Needs --gds. Repeatable") 

521 group.add_argument("--cut-keep-net", dest='cut_kept_net_patterns', action='append', default=None, 

522 metavar='NET', 

523 help="Extract the shapes of the net (a name, or a glob pattern) within the cells " 

524 "of --cut like the rest of the layout, e.g. a guard ring. Repeatable") 

525 group.add_argument("--exclude-net", dest='excluded_net_patterns', action='append', default=None, 

526 metavar='NET', 

527 help="Leave the net (a name, or a glob pattern) to a model or an ideal source, " 

528 "e.g. the power grid of a block, which the top level extracts: it has no " 

529 "resistance, and no capacitance to the substrate, while the capacitances " 

530 "between it and other nets stay. Repeatable") 

531 

532 @staticmethod 

533 def _add_pex25d_output_arguments(parser: argparse.ArgumentParser) -> None: 

534 group_out = parser.add_argument_group( 

535 "PEX25D Output", 

536 description="At least one of --out-file / --out-scene must be given. " 

537 "They are two options rather than one --emit switch because the " 

538 "two artifacts are different things with different consumers, and " 

539 "a single switch would leave it unclear where each one lands." 

540 ) 

541 add_argument_with_former_name(group_out, "--out-file", "--output_file", dest='pex25d_file_path', 

542 default=None, metavar='PATH', 

543 help="Write the unresolved PEX25DFile here ('-' for stdout). " 

544 "The literal form: one message per PEX25D record, " 

545 "references still plain strings, nothing derived. This is " 

546 "what a reader produces and what a validator checks.") 

547 add_argument_with_former_name(group_out, "--out-scene", "--output_scene", dest='pex25d_scene_path', 

548 default=None, metavar='PATH', 

549 help="Write the resolved PEX25DScene here ('-' for stdout). " 

550 "The adapter-ready form: absolute z extents, CONNECTS / " 

551 "BETWEEN / WRAPS resolved, wrap depth flattened, terminal " 

552 "intersections computed.") 

553 group_out.add_argument("--format", dest='pex25d_format', 

554 default=ArtifactFormat.AUTO, type=ArtifactFormat, 

555 choices=list(ArtifactFormat), 

556 help="Override the encoding otherwise inferred from the output " 

557 "file names (default is '%(default)s'). Naming convention: " 

558 "NAME.pex25d is text, NAME.pex25d.pb is binary protobuf, " 

559 "NAME.pex25d.textpb is protobuf text format, and a .scene " 

560 "infix marks a resolved scene. A trailing .gz is honoured.") 

561 group_out.add_argument("--grid", dest='pex25d_grid', 

562 default=DEFAULT_GRID_UM, metavar='UM', 

563 help="Coordinate grid in µm (default is %(default)s). " 

564 "Every z offset, thickness and coordinate must be " 

565 "an integer multiple of it, and it must divide the " 

566 "layout DBU exactly.") 

567 add_argument_with_former_name(group_out, "--domain-margin", "--domain_margin", dest='pex25d_domain_margin', 

568 type=float, default=None, metavar='UM', 

569 help="Emit DOMAIN_MARGIN with this clearance in µm " 

570 "(default is none, leaving the domain unset for " 

571 "the solver adapter to choose)") 

572 group_out.add_argument("--validate", dest='pex25d_validate', 

573 type=true_or_false, default=True, 

574 help="Check the generated artifacts against the reference " 

575 "validator before reporting (default is %(default)s). " 

576 "A defect in the technology data — a profile declared " 

577 "twice, a film anchored on the wrong link — otherwise " 

578 "shows up only when somebody runs 'pex25d validate' on " 

579 "the result. The artifacts are written either way; the " 

580 "diagnostics decide the exit code.") 

581 group_out.add_argument("--comments", dest='pex25d_comments', 

582 type=true_or_false, default=False, 

583 help="Include the syntax hints from the specification as " 

584 "comments (default is %(default)s). Text format only — " 

585 "the protobuf encodings have no comments.") 

586 add_argument_with_former_name(group_out, "--with-source-refs", "--with_source_refs", 

587 dest='pex25d_with_source_refs', 

588 type=true_or_false, default=False, 

589 help="Populate every SourceRef (default is %(default)s). " 

590 "Protobuf output formats only — the text format has no " 

591 "spelling for a source reference. Roughly doubles the size " 

592 "of a layout-scale file, and means little for geometry " 

593 "built in memory rather than parsed from text; useful " 

594 "mainly when diffing generated output against a " 

595 "hand-written file.") 

596 

597 # ------------------------------------------------------------------ parsing 

598 

599 @staticmethod 

600 def parse_args(arg_list: List[str], 

601 env: Env) -> argparse.Namespace: 

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

603 epilog = EnvVar.help_epilog_table() 

604 epilog_md = rich.console.Group( 

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

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

607 ) 

608 main_parser = argparse.ArgumentParser(prog=PROGRAM_NAME, 

609 description=f"{PROGRAM_NAME}: " 

610 f"KLayout-integrated Parasitic Extraction Tool", 

611 epilog=epilog_md, 

612 add_help=False, 

613 formatter_class=SectionedHelpFormatter) 

614 

615 group_special = main_parser.add_argument_group("Special Options") 

616 group_special.add_argument("--help", "-h", action='help', 

617 help="show this help message and exit") 

618 group_special.add_argument("--version", "-v", action='version', 

619 version=f'{PROGRAM_NAME} {__version__}') 

620 

621 subparsers = main_parser.add_subparsers(dest="command", metavar='<subcommand>') 

622 

623 parser_extract = subparsers.add_parser( 

624 "extract", 

625 help="Run one or more parasitic extraction engines", 

626 description="Run PEX end to end: LVS for connectivity, then the selected " 

627 "engine(s), then netlist expansion and reduction.", 

628 add_help=False, formatter_class=RichHelpFormatter, epilog=epilog_md) 

629 KpexCLI._add_special_options(parser_extract, include_threads=True) 

630 KpexCLI._add_pex_setup_arguments(parser_extract, env) 

631 KpexCLI._add_pex_input_arguments(parser_extract) 

632 KpexCLI._add_engine_arguments(parser_extract) 

633 KpexCLI._add_extraction_options(parser_extract) 

634 KpexCLI._add_tech_arguments(parser_extract) 

635 KpexCLI._add_fastercap_arguments(parser_extract) 

636 KpexCLI._add_magic_arguments(parser_extract, env) 

637 KpexCLI._add_analytical_25d_arguments(parser_extract) 

638 KpexCLI._add_exclusion_arguments(parser_extract) 

639 

640 parser_pex25d = subparsers.add_parser( 

641 "pex25d", 

642 help="Generate PEX25D solver input from a layout, without running a solver", 

643 description="Run LVS for connectivity and write the scene as PEX25D, stopping " 

644 "before any engine is started. This is the cut point in the " 

645 "pipeline that lets the geometry be inspected, archived, diffed, " 

646 "or handed to somebody else's solver. Format-only work on the " 

647 "result — validating, re-encoding, exporting to an engine's " 

648 "native input — is the standalone 'pex25d' tool's job.", 

649 add_help=False, formatter_class=RichHelpFormatter, epilog=epilog_md) 

650 KpexCLI._add_special_options(parser_pex25d) 

651 KpexCLI._add_pex_setup_arguments(parser_pex25d, env, include_spice_output=False) 

652 KpexCLI._add_pex_input_arguments(parser_pex25d) 

653 KpexCLI._add_extraction_options(parser_pex25d) 

654 KpexCLI._add_tech_arguments(parser_pex25d) 

655 KpexCLI._add_pex25d_output_arguments(parser_pex25d) 

656 Pex25DCLI._add_diagnostics_arguments(parser_pex25d) 

657 

658 add_help_subcommand(subparsers) 

659 add_subcommand_sections(main_parser, subparsers, KpexCLI.SUBCOMMAND_SECTIONS) 

660 

661 if arg_list is None: 

662 arg_list = sys.argv[1:] 

663 args = main_parser.parse_args(arg_list) 

664 handle_help_subcommand(main_parser, args) 

665 if args.command is None: 

666 main_parser.print_help() 

667 sys.exit(ExitCode.USAGE) 

668 

669 # The engine switches only exist on 'extract'. Defaulting them here keeps 

670 # the shared validation and logging paths free of subcommand checks. 

671 for engine_attr in ('run_magic', 'run_fastcap', 'run_fastercap', 'run_2_5D'): 

672 if not hasattr(args, engine_attr): 

673 setattr(args, engine_attr, False) 

674 

675 # environmental variables and their defaults 

676 args.fastcap_exe_path = env[EnvVar.FASTCAP_EXE] 

677 args.fastercap_exe_path = env[EnvVar.FASTERCAP_EXE] 

678 args.klayout_exe_path = env[EnvVar.KLAYOUT_EXE] 

679 args.magic_exe_path = env[EnvVar.MAGIC_EXE] 

680 

681 return args 

682 

683 @staticmethod 

684 def validate_args(args: argparse.Namespace): 

685 found_errors = False 

686 

687 pdk_config: PDKConfig = args.pdk.config 

688 args.tech_pbjson_path = pdk_config.tech_pb_json_path 

689 args.lvs_script_path = pdk_config.pex_lvs_script_path 

690 

691 def input_file_stem(path: str): 

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

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

694 

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

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

697 found_errors = True 

698 

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

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

701 found_errors = True 

702 

703 rule('Input Layout') 

704 

705 # Which engines may be combined with which inputs is a question only 

706 # 'extract' has; a dump run has no engine to disagree with. 

707 if args.command == 'extract': 

708 # check engines VS input possiblities 

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

710 args.gds_path, args.lvsdb_path): 

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

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

713 found_errors = True 

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

715 error("No PEX engines activated") 

716 engine_help = """ 

717 | Argument | Description | 

718 | ------------ | ------------------------------- | 

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

720 | --fastcap | Run KPEX/FastCap2 3D engine | 

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

722 | --magic | Run MAGIC wrapper engine | 

723 """ 

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

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

726 found_errors = True 

727 case (_, _, _, _, None, None): 

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

729 found_errors = True 

730 

731 args.reduce_mode = ReductionMode(args.reduce_mode) 

732 if args.reduce_mode != ReductionMode.NONE: 

733 if not args.run_2_5D: 

734 error(f"--reduce {args.reduce_mode} reduces the netlist of the 2.5D engine, it needs --2.5D") 

735 found_errors = True 

736 elif not args.pex_mode.need_resistance(): 

737 error(f"--reduce {args.reduce_mode} needs --mode R or RC, " 

738 f"a {args.pex_mode} netlist has no nodes to reduce") 

739 found_errors = True 

740 if args.reduce_tau is not None and args.reduce_mode != ReductionMode.TICER: 

741 error(f"--reduce-tau applies to --reduce ticer only, not to --reduce {args.reduce_mode}") 

742 found_errors = True 

743 

744 if args.exclusion_marker_layers and args.exclusion_marker_mode == ExclusionMarkerMode.IGNORE: 

745 error(f"--exclusion-marker-layer {args.exclusion_marker_layers[0]} needs --exclusion-markers " 

746 f"{ExclusionMarkerMode.TIE} or {ExclusionMarkerMode.PORTS}") 

747 found_errors = True 

748 

749 if args.cut_kept_net_patterns and not args.cut_specifications: 

750 error(f"--cut-keep-net {args.cut_kept_net_patterns[0]} needs --cut") 

751 found_errors = True 

752 

753 layout_options = [f"--exclusion-markers {args.exclusion_marker_mode}" 

754 if args.exclusion_marker_mode != ExclusionMarkerMode.IGNORE else None, 

755 f"--cut {args.cut_specifications[0]}" if args.cut_specifications else None] 

756 net_options = [f"--exclude-net {args.excluded_net_patterns[0]}" if args.excluded_net_patterns else None] 

757 for exclusion_option in filter(None, layout_options + net_options): 

758 # NOTE: MAGIC would extract what is left out 

759 if args.run_magic: 

760 error(f"{exclusion_option} applies to the 2.5D engine, FasterCap and FastCap2, not to MAGIC") 

761 found_errors = True 

762 for exclusion_option in filter(None, layout_options): 

763 # NOTE: the LVS database only has the shapes of nets 

764 if args.gds_path is None: 

765 error(f"{exclusion_option} reads its shapes from the layout, so it needs --gds, also with --lvsdb") 

766 found_errors = True 

767 

768 args.netlist_format = NetlistFormat(args.netlist_format) 

769 if args.netlist_format != NetlistFormat.SPICE: 

770 # NOTE: MAGIC writes its SPICE netlist itself, FasterCap and FastCap2 have no other format (yet) 

771 other_engines = [name for name, is_run in (('MAGIC', args.run_magic), 

772 ('FastCap2', args.run_fastcap), 

773 ('FasterCap', args.run_fastercap)) if is_run] 

774 if other_engines: 

775 error(f"Only the 2.5D engine writes {args.netlist_format} netlists " 

776 f"(--netlist-format {args.netlist_format}), not {', '.join(other_engines)}") 

777 found_errors = True 

778 

779 if args.output_spice_path is not None: 

780 if args.output_netlist_path is not None: 

781 error("--out_spice is the deprecated name of --out-netlist, give --out-netlist only") 

782 found_errors = True 

783 elif args.netlist_format != NetlistFormat.SPICE: 

784 error(f"--out_spice writes a SPICE netlist, not --netlist-format {args.netlist_format}, " 

785 f"use --out-netlist") 

786 found_errors = True 

787 else: 

788 warning("--out_spice is deprecated, use --out-netlist") 

789 args.output_netlist_path = args.output_spice_path 

790 

791 # check if we find magicrc 

792 if args.run_magic: 

793 if args.magicrc_path is None: 

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

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

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

797 found_errors = True 

798 else: 

799 result = validate_files([args.magicrc_path]) 

800 for f in result.failures: 

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

802 found_errors = True 

803 

804 args.cut_requests = [] 

805 if args.command == 'extract': 

806 try: 

807 args.cut_requests = parsed_cut_requests(args.cut_specifications or []) 

808 except ExclusionError as e: 

809 error(str(e)) 

810 found_errors = True 

811 

812 # input mode: LVS or existing LVSDB? 

813 if args.gds_path: 

814 if args.lvsdb_path is None: 

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

816 args.input_mode = InputMode.GDS 

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

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

819 found_errors = True 

820 else: 

821 args.layout = kdb.Layout() 

822 args.layout.read(args.gds_path) 

823 

824 top_cells = args.layout.top_cells() 

825 

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

827 args.effective_cell_name = args.cell_name 

828 

829 found_cell: Optional[kdb.Cell] = None 

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

831 if cell.name == args.effective_cell_name: 

832 found_cell = cell 

833 break 

834 if not found_cell: 

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

836 found_errors = True 

837 

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

839 if is_only_top_cell: 

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

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

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

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

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

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

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

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

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

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

850 

851 write_output_file(found_cell.write, args.effective_gds_path) 

852 else: # find top cell 

853 if len(top_cells) == 1: 

854 args.effective_cell_name = top_cells[0].name 

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

856 else: 

857 args.effective_cell_name = 'TOP' 

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

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

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

861 found_errors = True 

862 

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

864 args.effective_gds_path = args.gds_path 

865 if args.lvsdb_path is not None: 

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

867 args.input_mode = InputMode.LVSDB 

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

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

870 found_errors = True 

871 else: 

872 lvsdb = kdb.LayoutVsSchematic() 

873 lvsdb.read(args.lvsdb_path) 

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

875 if args.gds_path and not found_errors and args.effective_cell_name != top_cell.name: 

876 error(f"The LVS database {args.lvsdb_path} is of the cell {top_cell.name}, " 

877 f"not of the cell {args.effective_cell_name} of the GDS {args.gds_path}") 

878 found_errors = True 

879 args.effective_cell_name = top_cell.name 

880 

881 if args.cut_requests and getattr(args, 'layout', None) is not None and hasattr(args, 'effective_cell_name') \ 

882 and args.layout.cell(args.effective_cell_name) is not None: 

883 cell_names = cells_called_by(args.layout, args.layout.cell(args.effective_cell_name)) 

884 for cut_request in args.cut_requests: 

885 if not any(matches_name(name, cut_request.cell_pattern) for name in cell_names): 

886 error(f"--cut {cut_request.cell_pattern}: no such cell below {args.effective_cell_name}, " 

887 f"which has: {', '.join(cell_names) or 'none'}") 

888 found_errors = True 

889 if not found_errors: 

890 try: 

891 check_instances_of_cut_cells(args.layout, args.layout.cell(args.effective_cell_name), 

892 args.cut_requests) 

893 except ExclusionError as e: 

894 error(str(e)) 

895 found_errors = True 

896 

897 if hasattr(args, 'effective_cell_name'): 

898 run_dir_id: str 

899 match args.input_mode: 

900 case InputMode.GDS: 

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

902 case InputMode.LVSDB: 

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

904 case _: 

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

906 

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

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

909 if args.input_mode == InputMode.GDS: 

910 if args.schematic_path: 

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

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

913 found_errors = True 

914 warning("--schematic is checked for its device classes and their totals only, " 

915 "use regular LVS to compare layout against schematic") 

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

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

918 LVSRunner.write_dummy_schematic(args.effective_schematic_path, args.effective_cell_name) 

919 

920 try: 

921 pattern_string: str = args.dielectric_filter 

922 args.dielectric_filter = MultipleChoicePattern(pattern=pattern_string) 

923 except ValueError as e: 

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

925 found_errors = True 

926 

927 if args.cache_dir_path is None: 

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

929 

930 if args.command == 'pex25d': 

931 if args.pex25d_file_path is None and args.pex25d_scene_path is None: 

932 error("Nothing to write. Give --out-file (the unresolved PEX25DFile), " 

933 "--out-scene (the resolved PEX25DScene), or both.") 

934 found_errors = True 

935 

936 if args.pex25d_file_path is not None \ 

937 and args.pex25d_file_path == args.pex25d_scene_path: 

938 error("--out-file and --out-scene point at the same destination") 

939 found_errors = True 

940 

941 # Resolved here rather than at write time so that a mistyped output 

942 # name fails now, and not after a multi-minute LVS run. 

943 args.pex25d_file_spec = None 

944 args.pex25d_scene_spec = None 

945 try: 

946 if args.pex25d_file_path is not None: 

947 args.pex25d_file_spec = infer_artifact_spec( 

948 args.pex25d_file_path, 

949 kind=ArtifactKind.FILE, 

950 format=args.pex25d_format, 

951 default_format=ArtifactFormat.TEXT) 

952 if args.pex25d_scene_path is not None: 

953 args.pex25d_scene_spec = infer_artifact_spec( 

954 args.pex25d_scene_path, 

955 kind=ArtifactKind.SCENE, 

956 format=args.pex25d_format, 

957 default_format=ArtifactFormat.PB) 

958 except ArtifactNamingError as e: 

959 error(str(e)) 

960 found_errors = True 

961 

962 # NOTE: the name says the kind too, which the readers go by (binary protobuf doesn't record it) 

963 for option, path, kind in (('--out-file', args.pex25d_file_path, ArtifactKind.FILE), 

964 ('--out-scene', args.pex25d_scene_path, ArtifactKind.SCENE)): 

965 named = named_kind_and_format(path) if path is not None else None 

966 if named is not None and named[0] != kind: 

967 suffixes = [suffix for (k, _), suffix in CANONICAL_SUFFIX.items() if k == kind] 

968 error(f"{option} {path} is named as a PEX25D {named[0].value} ({CANONICAL_SUFFIX[named]}), " 

969 f"which pex25d would read it as; name a {kind.value} with one of: {', '.join(suffixes)}") 

970 found_errors = True 

971 

972 # KLayout runs the LVS script, which the MAGIC wrapper does not need 

973 # ("no need to run LVS etc if only running magic engine") and which an 

974 # LVSDB input has already been through. 

975 needs_lvs = getattr(args, 'input_mode', None) == InputMode.GDS and \ 

976 (args.command != 'extract' or 

977 args.run_fastcap or args.run_fastercap or args.run_2_5D) 

978 

979 exe_path_by_tool: Dict[Tool, str] = {} 

980 if needs_lvs: 

981 exe_path_by_tool[Tool.KLAYOUT] = args.klayout_exe_path 

982 if args.run_magic: 

983 exe_path_by_tool[Tool.MAGIC] = args.magic_exe_path 

984 if args.run_fastercap: 

985 exe_path_by_tool[Tool.FASTERCAP] = args.fastercap_exe_path 

986 if args.run_fastcap: 

987 exe_path_by_tool[Tool.FASTCAP] = args.fastcap_exe_path 

988 

989 # Only the tools this run will actually start have to be there. 

990 for tool, exe_path in list(exe_path_by_tool.items()): 

991 if not (os.path.isfile(exe_path) or shutil.which(exe_path)): 

992 error(f"Can't locate {tool.value} executable at {exe_path} " 

993 f"(see {tool.env_var.value})") 

994 found_errors = True 

995 del exe_path_by_tool[tool] 

996 

997 if not check_tool_versions(exe_path_by_tool=exe_path_by_tool, 

998 pdk=args.pdk, 

999 mode=args.version_check_mode): 

1000 found_errors = True 

1001 

1002 if found_errors: 

1003 raise ArgumentValidationError("Argument validation failed") 

1004 

1005 def create_netlist_printer(self, 

1006 args: argparse.Namespace, 

1007 extraction_engine: ExtractionEngine, 

1008 tech_info: TechInfo) -> NetlistPrinter: 

1009 header = NetlistHeader(kpex_version=__version__, 

1010 extraction_engine=str(extraction_engine), 

1011 tech=args.pdk.name.lower(), 

1012 date=datetime.now()) 

1013 return NetlistPrinter(header=header, 

1014 device_models=tech_info.device_models) 

1015 

1016 def build_fastercap_input(self, 

1017 args: argparse.Namespace, 

1018 pex_context: KLayoutExtractionContext, 

1019 tech_info: TechInfo, 

1020 exclusions: Optional[Exclusions]) -> str: 

1021 rule('Process stackup') 

1022 fastercap_input_builder = FasterCapInputBuilder(pex_context=pex_context, 

1023 tech_info=tech_info, 

1024 substrate_net_name=pex_context.substrate_net_name, 

1025 k_void=args.k_void, 

1026 delaunay_amax=args.delaunay_amax, 

1027 delaunay_b=args.delaunay_b, 

1028 exclusions=exclusions) 

1029 gen: FasterCapModelGenerator = fastercap_input_builder.build() 

1030 

1031 rule('FasterCap Input File Generation') 

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

1033 os.makedirs(faster_cap_input_dir_path, exist_ok=True) 

1034 

1035 written = gen.write_fastcap(output_dir_path=faster_cap_input_dir_path, prefix='FasterCap_Input_') 

1036 

1037 rule('STL File Generation') 

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

1039 os.makedirs(geometry_dir_path, exist_ok=True) 

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

1041 

1042 if args.geometry_check: 

1043 rule('Geometry Validation') 

1044 gen.check() 

1045 

1046 return written[0] 

1047 

1048 

1049 def run_fastercap_extraction(self, 

1050 args: argparse.Namespace, 

1051 pex_context: KLayoutExtractionContext, 

1052 lst_file: str, 

1053 exclusions: Optional[Exclusions]): 

1054 rule('FasterCap Execution') 

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

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

1057 

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

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

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

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

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

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

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

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

1066 

1067 run_fastercap(exe_path=args.fastercap_exe_path, 

1068 lst_file_path=lst_file, 

1069 log_path=log_path, 

1070 tolerance=args.fastercap_tolerance, 

1071 d_coeff=args.fastercap_d_coeff, 

1072 mesh_refinement_value=args.fastercap_mesh_refinement_value, 

1073 ooc_condition=args.fastercap_ooc_condition, 

1074 auto_preconditioner=args.fastercap_auto_preconditioner, 

1075 galerkin_scheme=args.fastercap_galerkin_scheme, 

1076 jacobi_preconditioner=args.fastercap_jacobi_preconditioner) 

1077 

1078 cap_matrix = fastercap_parse_capacitance_matrix(log_path) 

1079 cap_matrix.write_csv(raw_csv_path) 

1080 

1081 cap_matrix = cap_matrix.averaged_off_diagonals() 

1082 cap_matrix.write_csv(avg_csv_path) 

1083 

1084 cap_matrix_interpreter = FasterCapOutputInterpreter() 

1085 

1086 netlist_expander = NetlistExpander() 

1087 expanded_netlist = netlist_expander.expand( 

1088 extracted_netlist=pex_context.lvsdb.netlist(), 

1089 top_cell_name=pex_context.annotated_top_cell.name, 

1090 cap_matrix=cap_matrix, 

1091 cap_matrix_interpreter=cap_matrix_interpreter, 

1092 blackbox_devices=args.blackbox_devices, 

1093 device_models=pex_context.tech.device_models, 

1094 substrate_net_name=pex_context.substrate_net_name, 

1095 exclusions=exclusions 

1096 ) 

1097 

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

1099 netlist_csv_writer = NetlistCSVWriter() 

1100 netlist_csv_writer.write_csv(netlist=expanded_netlist, 

1101 top_cell_name=pex_context.annotated_top_cell.name, 

1102 output_path=expanded_netlist_csv_path) 

1103 

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

1105 with open(expanded_netlist_csv_path, 'r', encoding='utf-8') as f: 

1106 for line in f.readlines(): 

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

1108 rule() 

1109 

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

1111 

1112 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTERCAP, pex_context.tech) 

1113 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

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

1115 

1116 # FIXME: should this be already reduced? 

1117 if args.output_netlist_path: 

1118 netlist_printer.write(expanded_netlist, args.output_netlist_path) 

1119 info(f"Copied expanded SPICE netlist to: {args.output_netlist_path}") 

1120 

1121 netlist_reducer = NetlistReducer() 

1122 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist, 

1123 top_cell_name=pex_context.annotated_top_cell.name) 

1124 netlist_printer.write(reduced_netlist, reduced_netlist_path) 

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

1126 

1127 self._fastercap_extracted_csv_path = expanded_netlist_csv_path 

1128 

1129 def run_magic_extraction(self, 

1130 args: argparse.Namespace): 

1131 if args.input_mode != InputMode.GDS: 

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

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

1134 return 

1135 

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

1137 magic_log_path = os.path.join(magic_run_dir, 

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

1139 magic_script_path = os.path.join(magic_run_dir, 

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

1141 

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

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

1144 

1145 os.makedirs(magic_run_dir, exist_ok=True) 

1146 

1147 layout = kdb.Layout() 

1148 layout.read(args.effective_gds_path) 

1149 

1150 if args.magic_pre_flatten: 

1151 tops = list(layout.top_cells()) 

1152 if not tops: 

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

1154 for top in tops: 

1155 top.flatten(True) 

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

1157 layout.write(flattened_gds_path) 

1158 gds_path = flattened_gds_path 

1159 else: 

1160 gds_path = args.effective_gds_path 

1161 

1162 prepare_magic_script(gds_path=gds_path, 

1163 cell_name=args.effective_cell_name, 

1164 run_dir_path=magic_run_dir, 

1165 script_path=magic_script_path, 

1166 output_netlist_path=output_netlist_path, 

1167 pex_mode=args.magic_pex_mode, 

1168 c_threshold=args.magic_cthresh, 

1169 r_threshold=args.magic_rthresh, 

1170 threshold=args.magic_thresh, 

1171 min_res=args.magic_minres, 

1172 min_delay=args.magic_mindel, 

1173 halo=args.magic_halo, 

1174 short_mode=args.magic_short_mode, 

1175 merge_mode=args.magic_merge_mode, 

1176 unique_mode=args.magic_unique_mode) 

1177 

1178 run_magic(exe_path=args.magic_exe_path, 

1179 magicrc_path=args.magicrc_path, 

1180 script_path=magic_script_path, 

1181 log_path=magic_log_path) 

1182 

1183 magic_pex_run = parse_magic_pex_run(Path(magic_run_dir)) 

1184 

1185 if args.magic_analyze: 

1186 report = rdb.ReportDatabase('') 

1187 magic_log_analyzer = MagicLogAnalyzer(magic_pex_run=magic_pex_run, 

1188 report=report, 

1189 dbu=layout.dbu) 

1190 magic_log_analyzer.analyze() 

1191 write_output_file(report.save, report_db_path) 

1192 

1193 rule("Paths") 

1194 if args.magic_analyze: 

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

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

1197 

1198 if os.path.exists(output_netlist_path): 

1199 if args.output_netlist_path and os.path.exists(output_netlist_path): 

1200 shutil.copy(output_netlist_path, args.output_netlist_path) 

1201 info(f"Copied expanded SPICE netlist to: {args.output_netlist_path}") 

1202 

1203 rule("MAGIC PEX SPICE netlist") 

1204 with open(output_netlist_path, 'r', encoding='utf-8', errors='replace') as f: 

1205 subproc(f.read()) 

1206 rule() 

1207 

1208 def run_fastcap_extraction(self, 

1209 args: argparse.Namespace, 

1210 pex_context: KLayoutExtractionContext, 

1211 lst_file: str, 

1212 exclusions: Optional[Exclusions]): 

1213 rule('FastCap2 Execution') 

1214 

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

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

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

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

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

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

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

1222 

1223 run_fastcap(exe_path=args.fastcap_exe_path, 

1224 lst_file_path=lst_file, 

1225 log_path=log_path) 

1226 

1227 cap_matrix = fastcap_parse_capacitance_matrix(log_path) 

1228 cap_matrix.write_csv(raw_csv_path) 

1229 

1230 cap_matrix = cap_matrix.averaged_off_diagonals() 

1231 cap_matrix.write_csv(avg_csv_path) 

1232 

1233 cap_matrix_interpreter = FastCapOutputInterpreter() 

1234 

1235 netlist_expander = NetlistExpander() 

1236 expanded_netlist = netlist_expander.expand( 

1237 extracted_netlist=pex_context.lvsdb.netlist(), 

1238 top_cell_name=pex_context.annotated_top_cell.name, 

1239 cap_matrix=cap_matrix, 

1240 cap_matrix_interpreter=cap_matrix_interpreter, 

1241 blackbox_devices=args.blackbox_devices, 

1242 device_models=pex_context.tech.device_models, 

1243 substrate_net_name=pex_context.substrate_net_name, 

1244 exclusions=exclusions 

1245 ) 

1246 

1247 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTCAP2, pex_context.tech) 

1248 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

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

1250 

1251 # FIXME: should this be already reduced? 

1252 if args.output_netlist_path: 

1253 netlist_printer.write(expanded_netlist, args.output_netlist_path) 

1254 info(f"Copied expanded SPICE netlist to: {args.output_netlist_path}") 

1255 

1256 netlist_reducer = NetlistReducer() 

1257 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist, 

1258 top_cell_name=pex_context.annotated_top_cell.name) 

1259 netlist_printer.write(reduced_netlist, reduced_netlist_path) 

1260 

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

1262 

1263 @staticmethod 

1264 def schematic_device_class_problems(schematic_path: str, 

1265 lvs_netlist: kdb.Netlist, 

1266 device_models: DeviceModels, 

1267 top_cell_name: str) -> List[str]: 

1268 """ 

1269 :return: the problems of check_schematic_device_classes and check_schematic_device_totals, 

1270 or that the schematic can't be read or evaluated 

1271 """ 

1272 try: 

1273 netlist = SpiceReader().read(schematic_path) 

1274 except SpiceReaderError as e: 

1275 return [f"kpex can't read the schematic, so no device classes are compared: {e}"] 

1276 try: 

1277 schematic = schematic_devices(netlist=netlist, device_models=device_models, top_cell_name=top_cell_name) 

1278 except SpiceEvaluationError as e: 

1279 return [f"kpex can't evaluate the schematic, so no device classes are compared: {schematic_path}: {e}"] 

1280 if schematic.device_names_by_unknown_model: 

1281 warning("The device class check leaves out the devices of the schematic of models without device model " 

1282 "mapping:\n" + 

1283 '\n'.join(f" - {model}: {first_names(names)}" 

1284 for model, names in sorted(schematic.device_names_by_unknown_model.items()))) 

1285 return check_schematic_device_classes(lvs_netlist=lvs_netlist, schematic=schematic) + \ 

1286 check_schematic_device_totals(lvs_netlist=lvs_netlist, schematic=schematic, device_models=device_models) 

1287 

1288 @staticmethod 

1289 def exclusions_of(args: argparse.Namespace, 

1290 pex_context: KLayoutExtractionContext, 

1291 tech_info: TechInfo) -> Optional[Exclusions]: 

1292 """ 

1293 :return: what is left out of the extraction, None if nothing is 

1294 """ 

1295 marker_mode = args.exclusion_marker_mode 

1296 honors_markers = marker_mode != ExclusionMarkerMode.IGNORE 

1297 if not honors_markers and not args.cut_requests and not args.excluded_net_patterns: 

1298 return None 

1299 

1300 substrate_layer_name = tech_info.internal_substrate_layer_name 

1301 exclusions = Exclusions([], substrate_layer_name) 

1302 if honors_markers or args.cut_requests: 

1303 exclusions = Exclusions.from_layout( 

1304 layout=args.layout, 

1305 cell_name=args.effective_cell_name, 

1306 marker_layers=args.selected_marker_layers if honors_markers else [], 

1307 dbu=pex_context.dbu, 

1308 ports=marker_mode == ExclusionMarkerMode.PORTS, 

1309 substrate_layer_name=substrate_layer_name, 

1310 cut_requests=args.cut_requests, 

1311 cut_layers=CutLayers.of_tech(tech_info) if args.cut_requests else None 

1312 ) 

1313 if honors_markers and not any(block.kind == BlockKind.MARKER for block in exclusions.blocks): 

1314 warning("The layout has no shapes on the exclusion markers, so the extraction leaves nothing out of them") 

1315 

1316 net_names = sorted(net.expanded_name() for net in pex_context.top_circuit.each_net()) 

1317 

1318 def names_of_nets(option: str, pattern: str) -> List[NetName]: 

1319 matching_names = [name for name in net_names if matches_name(name, pattern)] 

1320 if not matching_names: 

1321 raise ExclusionError(f"{option} {pattern}: no such net, the nets are: {', '.join(net_names)}") 

1322 return matching_names 

1323 

1324 for pattern in args.cut_kept_net_patterns or []: 

1325 for net_name in names_of_nets('--cut-keep-net', pattern): 

1326 exclusions.without_shapes(pex_context.net_shapes_by_conductor_layer(net_name)) 

1327 for pattern in args.excluded_net_patterns or []: 

1328 exclusions.excluded_net_names.update(names_of_nets('--exclude-net', pattern)) 

1329 prepare_exclusions(exclusions, pex_context, tech_info) 

1330 return exclusions 

1331 

1332 def run_kpex_2_5d_engine(self, 

1333 args: argparse.Namespace, 

1334 pex_context: KLayoutExtractionContext, 

1335 tech_info: TechInfo, 

1336 report_path: str, 

1337 netlist_csv_path: Optional[str], 

1338 expanded_netlist_path: Optional[str], 

1339 exclusions: Optional[Exclusions]): 

1340 # TODO: make this separatly configurable 

1341 # for now we use 0 

1342 args.rcx25d_delaunay_amax = 0 

1343 args.rcx25d_delaunay_b = 0.5 

1344 

1345 if args.pex_mode.need_capacitance(): 

1346 halo_source = '--halo' if args.halo is not None else f"tech info of {args.pdk}" 

1347 info(f"Sidewall halo: {tech_info.tech.process_parasitics.side_halo:g} µm ({halo_source})") 

1348 

1349 extractor = RCX25Extractor(pex_context=pex_context, 

1350 pex_mode=args.pex_mode, 

1351 delaunay_amax=args.rcx25d_delaunay_amax, 

1352 delaunay_b=args.rcx25d_delaunay_b, 

1353 scale_ratio_to_fit_halo=args.scale_ratio_to_fit_halo, 

1354 tech_info=tech_info, 

1355 report_path=report_path, 

1356 report_caps=args.report_caps, 

1357 exclusions=exclusions) 

1358 extraction_results = extractor.extract() 

1359 removed_device_ids = set(exclusions.removed_device_blocks) if exclusions is not None else set() 

1360 

1361 if netlist_csv_path is not None: 

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

1363 

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

1365 summary = extraction_results.summarize() 

1366 

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

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

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

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

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

1372 

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

1374 with open(netlist_csv_path, 'r', encoding='utf-8') as f: 

1375 for line in f.readlines(): 

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

1377 

1378 rule('Extracted netlist CSV') 

1379 subproc(f"{netlist_csv_path}") 

1380 

1381 if expanded_netlist_path is not None: 

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

1383 netlist_expander = RCX25NetlistExpander() 

1384 expanded_netlist = netlist_expander.expand( 

1385 extracted_netlist=pex_context.lvsdb.netlist(), 

1386 top_cell_name=pex_context.annotated_top_cell.name, 

1387 extraction_results=extraction_results, 

1388 blackbox_devices=args.blackbox_devices, 

1389 device_models=tech_info.device_models, 

1390 substrate_net_name=pex_context.substrate_net_name, 

1391 removed_device_ids=removed_device_ids 

1392 ) 

1393 

1394 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.K25D, tech_info) 

1395 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

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

1397 self._rcx25_netlist_paths = {NetlistFormat.SPICE: expanded_netlist_path} # NOTE: store for test case 

1398 

1399 # NOTE: a defect of the RC netlist simulates to a plausible, but wrong number (#215) 

1400 self._rcx25_netlist_problems = check_rc_netlist( # NOTE: store for test case 

1401 lvs_netlist=pex_context.lvsdb.netlist(), 

1402 rc_netlist=expanded_netlist, 

1403 top_cell_name=pex_context.annotated_top_cell.name, 

1404 summary=extraction_results.summarize(), 

1405 blackbox_devices=args.blackbox_devices, 

1406 device_models=tech_info.device_models, 

1407 substrate_net_name=pex_context.substrate_net_name, 

1408 removed_device_ids=removed_device_ids 

1409 ) 

1410 if args.input_mode == InputMode.GDS and args.schematic_path: 

1411 self._rcx25_netlist_problems += self.schematic_device_class_problems( 

1412 schematic_path=args.schematic_path, 

1413 lvs_netlist=pex_context.lvsdb.netlist(), 

1414 device_models=tech_info.device_models, 

1415 top_cell_name=pex_context.annotated_top_cell.name 

1416 ) 

1417 # NOTE: on by default for RC netlists, as their defects simulate to plausible, but wrong numbers 

1418 check_fails_run = args.check_rc_netlist if args.check_rc_netlist is not None \ 

1419 else args.pex_mode == PEXMode.RC 

1420 if self._rcx25_netlist_problems and not check_fails_run: 

1421 warning("The extracted netlist is inconsistent with the LVS netlist:\n" + 

1422 '\n'.join(f" - {problem}" for problem in self._rcx25_netlist_problems)) 

1423 log_peak_memory('the netlist') 

1424 

1425 # NOTE: the reduction reads the netlist as written, the checks above ran on the full one 

1426 self._rcx25_reduced_netlist_path = None 

1427 reduction_options = args.reduce_mode.options(ticer_tau=args.reduce_tau) 

1428 if reduction_options is not None: 

1429 rule('kpex/2.5D netlist reduction') 

1430 reduction = reduce_spice_netlist(input_path=expanded_netlist_path, 

1431 output_path=reduced_netlist_path(expanded_netlist_path), 

1432 options=reduction_options) 

1433 for scope in reduction.scopes: 

1434 subproc(scope.describe()) 

1435 info(f"Wrote reduced netlist to: {reduction.output_path}") 

1436 self._rcx25_reduced_netlist_path = reduction.output_path 

1437 log_peak_memory('the netlist reduction') 

1438 

1439 # NOTE: --out-netlist gets the reduced netlist with --reduce, in the --netlist-format, 

1440 # which is converted from the SPICE netlist (as 'netlist convert' does) 

1441 netlist_kind = 'reduced' if self._rcx25_reduced_netlist_path is not None else 'expanded' 

1442 netlist_path = self._rcx25_reduced_netlist_path or expanded_netlist_path 

1443 if args.netlist_format == NetlistFormat.VACASK: 

1444 netlist_path = convert_to_vacask( 

1445 input_path=netlist_path, 

1446 output_path=os.path.splitext(netlist_path)[0] + NetlistFormat.VACASK.file_extension, 

1447 subcircuits_with_m=tech_info.device_models.vacask_subcircuits_with_m) 

1448 info(f"Wrote {netlist_kind} VACASK netlist to: {netlist_path}") 

1449 self._rcx25_netlist_paths[NetlistFormat.VACASK] = netlist_path 

1450 

1451 if args.output_netlist_path: 

1452 shutil.copyfile(netlist_path, args.output_netlist_path) 

1453 info(f"Copied {netlist_kind} {args.netlist_format.name} netlist to: " 

1454 f"{args.output_netlist_path}") 

1455 

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

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

1458 try: 

1459 report = rdb.ReportDatabase('') 

1460 report.load(report_path) # try loading rdb 

1461 except Exception as e: 

1462 rule("Repair broken marker DB") 

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

1464 repair_rdb(report_path) 

1465 

1466 if expanded_netlist_path is not None and self._rcx25_netlist_problems and check_fails_run: 

1467 raise RCNetlistCheckError("The extracted netlist is inconsistent with the LVS netlist " 

1468 "(--check n to accept it):\n" + 

1469 '\n'.join(f" - {problem}" for problem in self._rcx25_netlist_problems)) 

1470 

1471 return extraction_results 

1472 

1473 def setup_logging(self, 

1474 args: argparse.Namespace, 

1475 exit_stack: contextlib.ExitStack): 

1476 """ 

1477 :param exit_stack: the one of the run, which closes the log files when the run ends 

1478 (also on errors, so that a later run in this process doesn't write into them) 

1479 """ 

1480 def register_log_file_handler(log_path: str, 

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

1482 handler = logging.FileHandler(log_path, encoding='utf-8') 

1483 handler.setLevel(LogLevel.SUBPROCESS) 

1484 if formatter: 

1485 handler.setFormatter(formatter) 

1486 register_additional_handler(handler) 

1487 exit_stack.callback(close_log_file_handler, handler) 

1488 return handler 

1489 

1490 def close_log_file_handler(handler: logging.Handler): 

1491 deregister_additional_handler(handler) 

1492 handler.close() 

1493 

1494 def reregister_log_file_handler(handler: logging.Handler, 

1495 log_path: str, 

1496 formatter: Optional[logging.Formatter]): 

1497 close_log_file_handler(handler) 

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

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

1500 if os.path.exists(new_path): 

1501 ctime = os.path.getctime(new_path) 

1502 dt = datetime.fromtimestamp(ctime) 

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

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

1505 shutil.move(new_path, backup_path) 

1506 log_path = shutil.move(log_path, new_path) 

1507 register_log_file_handler(log_path, formatter) 

1508 

1509 # setup preliminary logger 

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

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

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

1513 file_handler_plain = register_log_file_handler(cli_log_path_plain, None) 

1514 file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter) 

1515 try: 

1516 self.validate_args(args) 

1517 except ArgumentValidationError: 

1518 if hasattr(args, 'output_dir_path'): 

1519 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) 

1520 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) 

1521 sys.exit(1) 

1522 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) 

1523 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) 

1524 

1525 set_log_level(args.log_level) 

1526 

1527 @staticmethod 

1528 def lvs_input_fingerprint(args: argparse.Namespace) -> LVSInputFingerprint: 

1529 klayout_version = Tool.KLAYOUT.detect_version(args.klayout_exe_path) 

1530 return lvs_input_fingerprint(pdk=str(args.pdk), 

1531 klayout_version='' if klayout_version is None else str(klayout_version), 

1532 lvs_script_path=args.lvs_script_path, 

1533 script_parameters=LVSRunner.result_script_parameters(), 

1534 gds_path=args.gds_path, 

1535 cell_name=args.effective_cell_name, 

1536 schematic_path=args.effective_schematic_path) 

1537 

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

1539 lvsdb = kdb.LayoutVsSchematic() 

1540 

1541 match args.input_mode: 

1542 case InputMode.LVSDB: 

1543 lvsdb.read(args.lvsdb_path) 

1544 case InputMode.GDS: 

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

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

1547 lvs_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_extracted.cir") 

1548 lvs_cache_entry = LVSCacheEntry( 

1549 dir_path=os.path.join(args.cache_dir_path, args.pdk, 

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

1551 cell_name=args.effective_cell_name 

1552 ) 

1553 

1554 lvs_needed = True 

1555 

1556 if args.cache_lvs: 

1557 lvs_fingerprint = self.lvs_input_fingerprint(args) 

1558 cache_miss_reasons = lvs_cache_entry.cache_miss_reasons(lvs_fingerprint) 

1559 if cache_miss_reasons: 

1560 info(f"Cache miss: {'; '.join(cache_miss_reasons)}") 

1561 subproc(lvs_cache_entry.lvsdb_path) 

1562 else: 

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

1564 subproc(lvs_cache_entry.lvsdb_path) 

1565 shutil.copy(lvs_cache_entry.lvsdb_path, lvsdb_path) 

1566 lvs_needed = False 

1567 

1568 if lvs_needed: 

1569 lvs_runner = LVSRunner() 

1570 try: 

1571 lvs_runner.run_klayout_lvs(exe_path=args.klayout_exe_path, 

1572 lvs_script=args.lvs_script_path, 

1573 gds_path=args.effective_gds_path, 

1574 schematic_path=args.effective_schematic_path, 

1575 log_path=lvs_log_path, 

1576 lvsdb_path=lvsdb_path, 

1577 netlist_path=lvs_netlist_path, 

1578 verbose=args.klayout_lvs_verbose) 

1579 except LVSError as e: 

1580 error(str(e)) 

1581 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1582 if args.cache_lvs: 

1583 lvs_cache_entry.store(lvsdb_path=lvsdb_path, fingerprint=lvs_fingerprint) 

1584 

1585 lvsdb.read(lvsdb_path) 

1586 return lvsdb 

1587 

1588 SUBCOMMAND_SECTIONS: Dict[str, Tuple[str, ...]] = { 

1589 'Extraction': ('extract',), 

1590 'PEX25D': ('pex25d',), 

1591 'Help': ('help',), 

1592 } 

1593 """How ``kpex --help`` groups the subcommands; every subcommand is in exactly one section.""" 

1594 

1595 SUBCOMMANDS = ('extract', 'pex25d', 'help') 

1596 LEGACY_SUBCOMMAND = 'extract' 

1597 

1598 @classmethod 

1599 def normalize_argv(cls, argv: List[str]) -> List[str]: 

1600 """ 

1601 Accept the pre-subcommand, flat command line. 

1602 

1603 Before the verb layer, kpex was invoked as 

1604 ``kpex --pdk sky130A --gds foo.gds --2.5D``. Every such run still works 

1605 and means ``kpex extract <same options>``; it just warns. The two forms 

1606 cannot be confused, because a subcommand name never begins with '-' and 

1607 every legacy option does — so the fallback needs no heuristics and can 

1608 never mistake one for the other. 

1609 

1610 This exists for the scripts, CI jobs and docs that already say 

1611 ``kpex --gds ...``, including the test suite. It is meant to be removed 

1612 once those have moved on. 

1613 """ 

1614 tail = argv[1:] 

1615 if not tail: 

1616 return argv 

1617 first = tail[0] 

1618 if first in ('-h', '--help', '-v', '--version'): 

1619 return argv 

1620 if not first.startswith('-'): 

1621 return argv # a subcommand, or a typo argparse will report better than we can 

1622 if any(arg in cls.SUBCOMMANDS for arg in tail): 

1623 # A subcommand is present, just not first — most likely a top-level 

1624 # option that now belongs to the subcommand. Leave it alone and let 

1625 # argparse say so, rather than silently rewriting it into something 

1626 # that fails somewhere else. 

1627 return argv 

1628 warning(f"Calling {PROGRAM_NAME} without a subcommand is deprecated; " 

1629 f"treating this run as '{PROGRAM_NAME} {cls.LEGACY_SUBCOMMAND} …'. " 

1630 f"See '{PROGRAM_NAME} --help' for the available subcommands.") 

1631 return [argv[0], cls.LEGACY_SUBCOMMAND] + tail 

1632 

1633 @staticmethod 

1634 def _writes_artifact_to_stdout(argv: List[str]) -> bool: 

1635 """ 

1636 Whether this invocation sends a PEX25D artifact to stdout. 

1637 

1638 Answered from the raw argv rather than from parsed arguments, because 

1639 the redirect has to be in place before anything — argument validation 

1640 included — has a chance to print. 

1641 """ 

1642 # NOTE: also by the former names of the options 

1643 return any(previous in ('--out-file', '--out-scene', '--output_file', '--output_scene') and current == '-' 

1644 for previous, current in zip(argv, argv[1:])) 

1645 

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

1647 env = Env.from_os_environ() 

1648 argv = self.normalize_argv(argv) 

1649 

1650 # A PEX25D artifact may be destined for stdout, and in that case stdout 

1651 # belongs to the artifact alone — one stray log line and whatever is on 

1652 # the other end of the pipe is parsing garbage. Redirecting sys.stdout to 

1653 # stderr for the whole run is blunt but total: the rich console resolves 

1654 # sys.stdout at write time, so every rule()/info()/subproc() follows, 

1655 # including ones written by code that never considered being in a 

1656 # pipeline. Artifact I/O uses sys.__stdout__ and is unaffected. 

1657 writes_to_stdout = self._writes_artifact_to_stdout(argv) 

1658 

1659 with contextlib.ExitStack() as stack: 

1660 if writes_to_stdout: 

1661 stack.enter_context(contextlib.redirect_stdout(sys.stderr)) 

1662 

1663 if not ({'-v', '--version', '-h', '--help'} & set(argv)) and argv[1:2] != ['help']: 

1664 rule('Command line arguments') 

1665 subproc(' '.join(map(shlex.quote, argv))) 

1666 

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

1668 

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

1670 self.setup_logging(args, stack) 

1671 warn_deprecated_options(args) 

1672 

1673 try: 

1674 tech_info = TechInfo.from_json(args.tech_pbjson_path, 

1675 dielectric_filter=args.dielectric_filter) 

1676 except TechDefError as e: 

1677 error(str(e)) 

1678 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1679 

1680 try: 

1681 match args.command: 

1682 case 'pex25d': 

1683 self.run_pex25d_generation(args=args, tech_info=tech_info) 

1684 case _: 

1685 try: 

1686 self.run_extraction(args=args, tech_info=tech_info) 

1687 except (LVSDBError, RExtractionTechError, CExtractionTechError, DeviceModelError, 

1688 RCNetlistCheckError, SpiceReaderError, VacaskWriterError, FasterCapError, 

1689 ExclusionError) as e: 

1690 error(str(e)) 

1691 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1692 finally: 

1693 # NOTE: also of a failed run, e.g. to size the next one 

1694 log_peak_memory('the run') 

1695 

1696 def run_pex25d_generation(self, 

1697 args: argparse.Namespace, 

1698 tech_info: TechInfo): 

1699 """ 

1700 The dump path: LVS for connectivity, then PEX25D out, and stop. 

1701 

1702 Deliberately does not touch any engine. Everything downstream of the 

1703 artifacts written here — validating them, re-encoding them, exporting 

1704 them to a solver's native input — is the standalone ``pex25d`` tool's 

1705 job, and needs neither a layout nor KLayout to do it. 

1706 """ 

1707 from .klayout.pex25d_builder import BuildError, BuilderOptions, build_pex25d_file 

1708 from .pex25d.codec import save_artifact 

1709 from .pex25d.resolver import ResolveError, resolve 

1710 from .pex25d.validator import validate 

1711 

1712 report = DiagnosticsReport(warnings_are_errors=args.warnings_are_errors) 

1713 

1714 rule('Prepare LVSDB') 

1715 lvsdb = self.create_lvsdb(args) 

1716 

1717 pex_context = KLayoutExtractionContext.prepare_extraction( 

1718 top_cell=args.effective_cell_name, 

1719 lvsdb=lvsdb, 

1720 tech=tech_info, 

1721 blackbox_devices=args.blackbox_devices) 

1722 

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

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

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

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

1727 

1728 rule('PEX25D Generation') 

1729 try: 

1730 pex25d_file = build_pex25d_file( 

1731 pex_context=pex_context, 

1732 tech_info=tech_info, 

1733 cell_name=args.effective_cell_name, 

1734 options=BuilderOptions( 

1735 grid_um=args.pex25d_grid, 

1736 with_source_refs=args.pex25d_with_source_refs, 

1737 dielectric_filter=args.dielectric_filter, 

1738 domain_margin_um=args.pex25d_domain_margin)) 

1739 

1740 if args.pex25d_file_spec is not None: 

1741 save_artifact(pex25d_file, args.pex25d_file_spec, 

1742 comments=args.pex25d_comments) 

1743 info(f"Wrote {args.pex25d_file_spec}") 

1744 

1745 scene = None 

1746 if args.pex25d_scene_spec is not None: 

1747 # The geometric tier belongs to whichever of the two runs it 

1748 # once: the validator when it is on, the resolver otherwise. 

1749 scene = resolve(pex25d_file, report=report, 

1750 strict=args.strict and not args.pex25d_validate) 

1751 save_artifact(scene, args.pex25d_scene_spec, 

1752 comments=args.pex25d_comments) 

1753 info(f"Wrote {args.pex25d_scene_spec}") 

1754 

1755 if args.pex25d_validate: 

1756 validate(pex25d_file, report=report, strict=args.strict, 

1757 scene=scene) 

1758 

1759 except BuildError as e: 

1760 error(f"Could not generate PEX25D: {e}") 

1761 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1762 except ResolveError as e: 

1763 # The scene is not written: the diagnostics say why, and half a 

1764 # scene is worse than none. 

1765 error(str(e)) 

1766 report.render(args.diagnostics_format) 

1767 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1768 except NotImplementedError as e: 

1769 error(f"Not implemented yet: {e}") 

1770 sys.exit(ExitCode.NOT_IMPLEMENTED) 

1771 

1772 writes_to_stdout = any(spec is not None and spec.is_stdio 

1773 for spec in (args.pex25d_file_spec, args.pex25d_scene_spec)) 

1774 with diagnostics_stream(writes_to_stdout=writes_to_stdout) as stream: 

1775 report.render(args.diagnostics_format, stream=stream) 

1776 

1777 if report.exit_code != ExitCode.OK: 

1778 sys.exit(report.exit_code) 

1779 

1780 def run_extraction(self, 

1781 args: argparse.Namespace, 

1782 tech_info: TechInfo): 

1783 if args.halo is not None: 

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

1785 

1786 if args.exclusion_marker_mode != ExclusionMarkerMode.IGNORE: 

1787 stack_layer_names = set(tech_info.canonical_layer_name_by_gds_pair.values()) 

1788 args.selected_marker_layers = selected_marker_layers(tech_info.tech.exclusion_markers, 

1789 stack_layer_names, 

1790 args.exclusion_marker_layers) 

1791 if not args.selected_marker_layers: 

1792 raise ExclusionError(f"--exclusion-markers {args.exclusion_marker_mode}: the tech info of {args.pdk} " 

1793 f"has no exclusion markers, give them with --exclusion-marker-layer") 

1794 

1795 if args.run_magic: 

1796 rule('MAGIC') 

1797 self.run_magic_extraction(args) 

1798 log_peak_memory('MAGIC') 

1799 

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

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

1802 return 

1803 

1804 rule('Prepare LVSDB') 

1805 lvsdb = self.create_lvsdb(args) 

1806 

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

1808 lvsdb=lvsdb, 

1809 tech=tech_info, 

1810 blackbox_devices=args.blackbox_devices) 

1811 log_peak_memory('preparing the LVSDB') 

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

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

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

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

1816 

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

1818 pex_context.annotated_layout.write(gds_path) 

1819 

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

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

1822 

1823 def dump_layers(cell: str, 

1824 layers: List[KLayoutExtractedLayerInfo], 

1825 layout_dump_path: str): 

1826 layout = kdb.Layout() 

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

1828 

1829 top_cell = layout.create_cell(cell) 

1830 for ulyr in layers: 

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

1832 li.name = ulyr.lvs_layer_name 

1833 layer = layout.insert_layer(li) 

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

1835 

1836 write_output_file(layout.write, layout_dump_path) 

1837 

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

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

1840 dump_layers(cell=args.effective_cell_name, 

1841 layers=pex_context.unnamed_layers, 

1842 layout_dump_path=layout_dump_path) 

1843 

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

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

1846 nonempty_layers = [l \ 

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

1848 for l in layers.source_layers] 

1849 dump_layers(cell=args.effective_cell_name, 

1850 layers=nonempty_layers, 

1851 layout_dump_path=layout_dump_path) 

1852 else: 

1853 error("No extracted layers found") 

1854 sys.exit(1) 

1855 

1856 exclusions = self.exclusions_of(args, pex_context, tech_info) 

1857 

1858 if args.run_fastcap or args.run_fastercap: 

1859 if exclusions is not None: 

1860 warn_of_ports_without_nodes(exclusions, "The netlist of a field solver") 

1861 lst_file = self.build_fastercap_input(args=args, 

1862 pex_context=pex_context, 

1863 tech_info=tech_info, 

1864 exclusions=exclusions) 

1865 log_peak_memory('the FasterCap input') 

1866 if args.run_fastercap: 

1867 self.run_fastercap_extraction(args=args, 

1868 pex_context=pex_context, 

1869 lst_file=lst_file, 

1870 exclusions=exclusions) 

1871 log_peak_memory('FasterCap') 

1872 if args.run_fastcap: 

1873 self.run_fastcap_extraction(args=args, 

1874 pex_context=pex_context, 

1875 lst_file=lst_file, 

1876 exclusions=exclusions) 

1877 log_peak_memory('FastCap2') 

1878 

1879 if args.run_2_5D: 

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

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

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

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

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

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

1886 

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

1888 args=args, 

1889 pex_context=pex_context, 

1890 tech_info=tech_info, 

1891 report_path=report_path, 

1892 netlist_csv_path=netlist_csv_path, 

1893 expanded_netlist_path=netlist_spice_path, 

1894 exclusions=exclusions 

1895 ) 

1896 

1897 self._rcx25_extracted_csv_path = netlist_csv_path 

1898 

1899 @property 

1900 def rcx25_extraction_results(self) -> ExtractionResults: 

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

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

1903 return self._rcx25_extraction_results 

1904 

1905 @property 

1906 def rcx25_extracted_csv_path(self) -> str: 

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

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

1909 return self._rcx25_extracted_csv_path 

1910 

1911 @property 

1912 def rcx25_reduced_netlist_path(self) -> Optional[str]: 

1913 """The reduced netlist the 2.5D engine wrote (--reduce), None without reduction""" 

1914 if not hasattr(self, '_rcx25_reduced_netlist_path'): 

1915 raise Exception('rcx25_reduced_netlist_path is not initialized, was run_kpex_2_5d_engine called?') 

1916 return self._rcx25_reduced_netlist_path 

1917 

1918 @property 

1919 def rcx25_netlist_paths(self) -> Dict[NetlistFormat, str]: 

1920 """ 

1921 The paths of the netlists the 2.5D engine wrote into the run directory, by format: 

1922 the SPICE netlist, and the one converted to the --netlist-format (of the reduced netlist with --reduce) 

1923 """ 

1924 if not hasattr(self, '_rcx25_netlist_paths'): 

1925 raise Exception('rcx25_netlist_paths is not initialized, was run_kpex_2_5d_engine called?') 

1926 return self._rcx25_netlist_paths 

1927 

1928 @property 

1929 def rcx25_netlist_problems(self) -> List[str]: 

1930 if not hasattr(self, '_rcx25_netlist_problems'): 

1931 raise Exception('rcx25_netlist_problems is not initialized, was run_kpex_2_5d_engine called?') 

1932 return self._rcx25_netlist_problems 

1933 

1934 @property 

1935 def fastercap_extracted_csv_path(self) -> str: 

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

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

1938 return self._fastercap_extracted_csv_path 

1939 

1940 

1941if __name__ == "__main__": 

1942 cli = KpexCLI() 

1943 cli.main(sys.argv)