Coverage for klayout_pex/kpex_cli.py: 67%

731 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-17 19:08 +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 logging 

31import os 

32import os.path 

33from pathlib import Path 

34import rich.console 

35import rich.markdown 

36import rich.text 

37from rich_argparse import RichHelpFormatter 

38import shlex 

39import shutil 

40import sys 

41from typing import * 

42 

43import klayout.db as kdb 

44import klayout.rdb as rdb 

45 

46from .klayout import capacitance_matrix_interpreter 

47from .common.path_validation import validate_files, FileValidationResult 

48from .env import EnvVar, Env 

49from .extraction_engine import ExtractionEngine 

50from .fastercap.fastercap_input_builder import FasterCapInputBuilder 

51from .fastercap.fastercap_model_generator import FasterCapModelGenerator 

52from .fastercap.fastercap_runner import run_fastercap, fastercap_parse_capacitance_matrix 

53from .fastercap.output_interpreter import FasterCapOutputInterpreter 

54from .fastcap.fastcap_runner import run_fastcap, fastcap_parse_capacitance_matrix 

55from .fastcap.output_interpreter import FastCapOutputInterpreter 

56from .klayout.lvs_runner import LVSRunner 

57from .klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo 

58from .klayout.netlist_expander import NetlistExpander 

59from .klayout.netlist_csv import NetlistCSVWriter 

60from .klayout.netlist_printer import NetlistPrinter 

61from .klayout.netlist_reducer import NetlistReducer 

62from .klayout.repair_rdb import repair_rdb 

63from .log import ( 

64 LogLevel, 

65 set_log_level, 

66 register_additional_handler, 

67 deregister_additional_handler, 

68 # console, 

69 # debug, 

70 info, 

71 warning, 

72 subproc, 

73 error, 

74 rule 

75) 

76from .magic.magic_ext_file_parser import parse_magic_pex_run 

77from .magic.magic_runner import ( 

78 MagicPEXMode, 

79 MagicShortMode, 

80 MagicMergeMode, 

81 MagicUniqueMode, 

82 run_magic, 

83 prepare_magic_script, 

84) 

85from .magic.magic_log_analyzer import MagicLogAnalyzer 

86from .pex25d.artifact import ( 

87 ArtifactFormat, 

88 ArtifactKind, 

89 ArtifactNamingError, 

90 infer_artifact_spec, 

91) 

92from .klayout.pex25d_builder import DEFAULT_GRID_UM 

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

94from .pex25d.pex25d_cli import Pex25DCLI 

95from .pdk_config import PDK, PDKConfig 

96from .rcx25.extractor import RCX25Extractor, ExtractionResults 

97from .rcx25.netlist_expander import RCX25NetlistExpander 

98from .rcx25.pex_mode import PEXMode 

99from .tech_info import TechDefError, TechInfo 

100from .tool_version_constraints import ( 

101 Tool, 

102 VersionCheckMode, 

103 check_tool_versions 

104) 

105from .util.multiple_choice import MultipleChoicePattern 

106from .util.argparse_helpers import render_enum_help, true_or_false 

107from .version import __version__ 

108 

109 

110# ------------------------------------------------------------------------------------ 

111 

112PROGRAM_NAME = "kpex" 

113 

114 

115class ArgumentValidationError(Exception): 

116 pass 

117 

118 

119class InputMode(StrEnum): 

120 LVSDB = "lvsdb" 

121 GDS = "gds" 

122 

123 

124class KpexCLI: 

125 # ---------------------------------------------------------- argument groups 

126 # 

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

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

129 # the point of the verb layer: `kpex pex25d` cannot be handed --magic_halo, 

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

131 # of flags make sense together. 

132 

133 @staticmethod 

134 def _add_special_options(parser: argparse.ArgumentParser, 

135 include_threads: bool = False) -> None: 

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

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

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

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

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

141 group_special.add_argument("--tool_version_checks", dest='version_check_mode', 

142 default=VersionCheckMode.DEFAULT, type=VersionCheckMode, 

143 choices=list(VersionCheckMode), 

144 help=render_enum_help(topic='tool_version_checks', 

145 enum_cls=VersionCheckMode)) 

146 if include_threads: 

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

148 default=os.cpu_count() * 4, 

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

150 "(default is %(default)s)") 

151 

152 @staticmethod 

153 def _add_pex_setup_arguments(parser: argparse.ArgumentParser, 

154 env: Env, 

155 include_spice_output: bool = True) -> None: 

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

157 

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

159 

160 default_pdk = env.default_pdk 

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

162 if default_pdk: 

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

164 

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

166 type=PDK.from_string, choices=all_pdk_choices, 

167 help=pdk_help, default=default_pdk) 

168 

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

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

171 

172 if include_spice_output: 

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

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

175 

176 

177 @staticmethod 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

194 "(bypass PEX-LVS). Developer shortcut for " 

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

196 "interchange format — use PEX25D for that") 

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

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

199 

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

201 type=true_or_false, default=True, 

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

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

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

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

206 type=true_or_false, default=False, 

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

208 

209 

210 @staticmethod 

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

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

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

214 action='store_true', default=False, 

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

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

217 action='store_true', default=False, 

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

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

220 action='store_true', default=False, 

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

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

223 action='store_true', default=False, 

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

225 

226 

227 @staticmethod 

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

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

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

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

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

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

234 

235 @staticmethod 

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

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

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

239 type=str, default="all", 

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

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

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

243 

244 @staticmethod 

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

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

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

248 type=float, default=3.9, 

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

250 

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

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

253 type=float, default=50, 

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

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

256 type=float, default=0.5, 

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

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

259 type=true_or_false, default=False, 

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

261 f"(default is False)") 

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

263 type=float, default=0.05, 

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

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

266 type=float, default=0.5, 

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

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

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

270 type=float, default=0.5, 

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

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

273 type=float, default=2, 

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

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

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

277 type=true_or_false, default=True, 

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

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

280 action='store_true', default=False, 

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

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

283 action='store_true', default=False, 

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

285 

286 

287 @staticmethod 

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

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

290 

291 default_magicrc_path = env.default_magicrc_path 

292 if default_magicrc_path: 

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

294 else: 

295 magicrc_help = "Path to magicrc configuration file "\ 

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

297 

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

299 help=magicrc_help) 

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

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

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

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

304 type=float, default=0.01, 

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

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

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

308 type=int, default=100, 

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

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

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

312 type=int, default=None, 

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

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

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

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

317 type=int, default=None, 

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

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

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

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

322 type=int, default=None, 

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

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

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

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

327 type=float, default=None, 

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

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

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

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

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

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

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

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

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

337 action='store_true', default=False, 

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

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

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

341 help="Control net uniqueness during extraction " 

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

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

344 group_magic.add_argument("--magic_analyze", dest='magic_analyze', 

345 type=true_or_false, default=False, 

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

347 

348 @staticmethod 

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

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

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

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

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

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

355 type=float, default=None, 

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

357 "(default is no custom halo)") 

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

359 type=true_or_false, default=True, 

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

361 

362 

363 @staticmethod 

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

365 group_out = parser.add_argument_group( 

366 "PEX25D Output", 

367 description="At least one of --output_file / --output_scene must be given. " 

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

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

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

371 ) 

372 group_out.add_argument("--output_file", dest='pex25d_file_path', 

373 default=None, metavar='PATH', 

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

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

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

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

378 group_out.add_argument("--output_scene", dest='pex25d_scene_path', 

379 default=None, metavar='PATH', 

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

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

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

383 "intersections computed.") 

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

385 default=ArtifactFormat.AUTO, type=ArtifactFormat, 

386 choices=list(ArtifactFormat), 

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

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

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

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

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

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

393 default=DEFAULT_GRID_UM, metavar='UM', 

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

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

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

397 "layout DBU exactly.") 

398 group_out.add_argument("--domain_margin", dest='pex25d_domain_margin', 

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

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

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

402 "the solver adapter to choose)") 

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

404 type=true_or_false, default=True, 

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

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

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

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

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

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

411 "diagnostics decide the exit code.") 

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

413 type=true_or_false, default=False, 

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

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

416 "the protobuf encodings have no comments.") 

417 group_out.add_argument("--with_source_refs", dest='pex25d_with_source_refs', 

418 type=true_or_false, default=False, 

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

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

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

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

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

424 "mainly when diffing generated output against a " 

425 "hand-written file.") 

426 

427 # ------------------------------------------------------------------ parsing 

428 

429 @staticmethod 

430 def parse_args(arg_list: List[str], 

431 env: Env) -> argparse.Namespace: 

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

433 epilog = EnvVar.help_epilog_table() 

434 epilog_md = rich.console.Group( 

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

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

437 ) 

438 main_parser = argparse.ArgumentParser(prog=PROGRAM_NAME, 

439 description=f"{PROGRAM_NAME}: " 

440 f"KLayout-integrated Parasitic Extraction Tool", 

441 epilog=epilog_md, 

442 add_help=False, 

443 formatter_class=RichHelpFormatter) 

444 

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

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

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

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

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

450 

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

452 help="Sub-commands help") 

453 

454 parser_extract = subparsers.add_parser( 

455 "extract", 

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

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

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

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

460 KpexCLI._add_special_options(parser_extract, include_threads=True) 

461 KpexCLI._add_pex_setup_arguments(parser_extract, env) 

462 KpexCLI._add_pex_input_arguments(parser_extract) 

463 KpexCLI._add_engine_arguments(parser_extract) 

464 KpexCLI._add_extraction_options(parser_extract) 

465 KpexCLI._add_tech_arguments(parser_extract) 

466 KpexCLI._add_fastercap_arguments(parser_extract) 

467 KpexCLI._add_magic_arguments(parser_extract, env) 

468 KpexCLI._add_analytical_25d_arguments(parser_extract) 

469 

470 parser_pex25d = subparsers.add_parser( 

471 "pex25d", 

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

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

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

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

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

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

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

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

480 KpexCLI._add_special_options(parser_pex25d) 

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

482 KpexCLI._add_pex_input_arguments(parser_pex25d) 

483 KpexCLI._add_extraction_options(parser_pex25d) 

484 KpexCLI._add_tech_arguments(parser_pex25d) 

485 KpexCLI._add_pex25d_output_arguments(parser_pex25d) 

486 Pex25DCLI._add_diagnostics_arguments(parser_pex25d) 

487 

488 if arg_list is None: 

489 arg_list = sys.argv[1:] 

490 args = main_parser.parse_args(arg_list) 

491 if args.command is None: 

492 main_parser.print_help() 

493 sys.exit(ExitCode.USAGE) 

494 

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

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

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

498 if not hasattr(args, engine_attr): 

499 setattr(args, engine_attr, False) 

500 

501 # environmental variables and their defaults 

502 args.fastcap_exe_path = env[EnvVar.FASTCAP_EXE] 

503 args.fastercap_exe_path = env[EnvVar.FASTERCAP_EXE] 

504 args.klayout_exe_path = env[EnvVar.KLAYOUT_EXE] 

505 args.magic_exe_path = env[EnvVar.MAGIC_EXE] 

506 

507 return args 

508 

509 @staticmethod 

510 def validate_args(args: argparse.Namespace): 

511 found_errors = False 

512 

513 pdk_config: PDKConfig = args.pdk.config 

514 args.tech_pbjson_path = pdk_config.tech_pb_json_path 

515 args.lvs_script_path = pdk_config.pex_lvs_script_path 

516 

517 def input_file_stem(path: str): 

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

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

520 

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

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

523 found_errors = True 

524 

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

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

527 found_errors = True 

528 

529 rule('Input Layout') 

530 

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

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

533 if args.command == 'extract': 

534 # check engines VS input possiblities 

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

536 args.gds_path, args.lvsdb_path): 

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

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

539 found_errors = True 

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

541 error("No PEX engines activated") 

542 engine_help = """ 

543 | Argument | Description | 

544 | ------------ | ------------------------------- | 

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

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

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

548 | --magic | Run MAGIC wrapper engine | 

549 """ 

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

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

552 found_errors = True 

553 case (_, _, _, _, None, None): 

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

555 found_errors = True 

556 

557 # check if we find magicrc 

558 if args.run_magic: 

559 if args.magicrc_path is None: 

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

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

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

563 found_errors = True 

564 else: 

565 result = validate_files([args.magicrc_path]) 

566 for f in result.failures: 

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

568 found_errors = True 

569 

570 # input mode: LVS or existing LVSDB? 

571 if args.gds_path: 

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

573 args.input_mode = InputMode.GDS 

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

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

576 found_errors = True 

577 else: 

578 args.layout = kdb.Layout() 

579 args.layout.read(args.gds_path) 

580 

581 top_cells = args.layout.top_cells() 

582 

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

584 args.effective_cell_name = args.cell_name 

585 

586 found_cell: Optional[kdb.Cell] = None 

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

588 if cell.name == args.effective_cell_name: 

589 found_cell = cell 

590 break 

591 if not found_cell: 

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

593 found_errors = True 

594 

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

596 if is_only_top_cell: 

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

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

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

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

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

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

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

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

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

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

607 

608 found_cell.write(args.effective_gds_path) 

609 else: # find top cell 

610 if len(top_cells) == 1: 

611 args.effective_cell_name = top_cells[0].name 

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

613 else: 

614 args.effective_cell_name = 'TOP' 

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

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

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

618 found_errors = True 

619 

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

621 args.effective_gds_path = args.gds_path 

622 elif args.lvsdb_path is not None: 

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

624 args.input_mode = InputMode.LVSDB 

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

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

627 found_errors = True 

628 else: 

629 lvsdb = kdb.LayoutVsSchematic() 

630 lvsdb.read(args.lvsdb_path) 

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

632 args.effective_cell_name = top_cell.name 

633 

634 if hasattr(args, 'effective_cell_name'): 

635 run_dir_id: str 

636 match args.input_mode: 

637 case InputMode.GDS: 

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

639 case InputMode.LVSDB: 

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

641 case _: 

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

643 

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

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

646 if args.input_mode == InputMode.GDS: 

647 if args.schematic_path: 

648 args.effective_schematic_path = args.schematic_path 

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

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

651 found_errors = True 

652 else: 

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

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

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

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

657 f.writelines([ 

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

659 '.ends\n', 

660 '.end\n' 

661 ]) 

662 

663 try: 

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

665 except KeyError: 

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

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

668 found_errors = True 

669 

670 try: 

671 pattern_string: str = args.dielectric_filter 

672 args.dielectric_filter = MultipleChoicePattern(pattern=pattern_string) 

673 except ValueError as e: 

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

675 found_errors = True 

676 

677 if args.cache_dir_path is None: 

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

679 

680 if args.command == 'pex25d': 

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

682 error("Nothing to write. Give --output_file (the unresolved PEX25DFile), " 

683 "--output_scene (the resolved PEX25DScene), or both.") 

684 found_errors = True 

685 

686 if args.pex25d_file_path is not None \ 

687 and args.pex25d_file_path == args.pex25d_scene_path: 

688 error("--output_file and --output_scene point at the same destination") 

689 found_errors = True 

690 

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

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

693 args.pex25d_file_spec = None 

694 args.pex25d_scene_spec = None 

695 try: 

696 if args.pex25d_file_path is not None: 

697 args.pex25d_file_spec = infer_artifact_spec( 

698 args.pex25d_file_path, 

699 kind=ArtifactKind.FILE, 

700 format=args.pex25d_format, 

701 default_format=ArtifactFormat.TEXT) 

702 if args.pex25d_scene_path is not None: 

703 args.pex25d_scene_spec = infer_artifact_spec( 

704 args.pex25d_scene_path, 

705 kind=ArtifactKind.SCENE, 

706 format=args.pex25d_format, 

707 default_format=ArtifactFormat.PB) 

708 except ArtifactNamingError as e: 

709 error(str(e)) 

710 found_errors = True 

711 

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

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

714 # LVSDB input has already been through. 

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

716 (args.command != 'extract' or 

717 args.run_fastcap or args.run_fastercap or args.run_2_5D) 

718 

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

720 if needs_lvs: 

721 exe_path_by_tool[Tool.KLAYOUT] = args.klayout_exe_path 

722 if args.run_magic: 

723 exe_path_by_tool[Tool.MAGIC] = args.magic_exe_path 

724 if args.run_fastercap: 

725 exe_path_by_tool[Tool.FASTERCAP] = args.fastercap_exe_path 

726 if args.run_fastcap: 

727 exe_path_by_tool[Tool.FASTCAP] = args.fastcap_exe_path 

728 

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

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

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

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

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

734 found_errors = True 

735 del exe_path_by_tool[tool] 

736 

737 if not check_tool_versions(exe_path_by_tool=exe_path_by_tool, 

738 pdk=args.pdk, 

739 mode=args.version_check_mode): 

740 found_errors = True 

741 

742 if found_errors: 

743 raise ArgumentValidationError("Argument validation failed") 

744 

745 def create_netlist_printer(self, 

746 args: argparse.Namespace, 

747 extraction_engine: ExtractionEngine): 

748 printer = NetlistPrinter(extraction_engine=extraction_engine, 

749 pdk=args.pdk) 

750 return printer 

751 

752 def build_fastercap_input(self, 

753 args: argparse.Namespace, 

754 pex_context: KLayoutExtractionContext, 

755 tech_info: TechInfo) -> str: 

756 rule('Process stackup') 

757 fastercap_input_builder = FasterCapInputBuilder(pex_context=pex_context, 

758 tech_info=tech_info, 

759 k_void=args.k_void, 

760 delaunay_amax=args.delaunay_amax, 

761 delaunay_b=args.delaunay_b) 

762 gen: FasterCapModelGenerator = fastercap_input_builder.build() 

763 

764 rule('FasterCap Input File Generation') 

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

766 os.makedirs(faster_cap_input_dir_path, exist_ok=True) 

767 

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

769 

770 rule('STL File Generation') 

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

772 os.makedirs(geometry_dir_path, exist_ok=True) 

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

774 

775 if args.geometry_check: 

776 rule('Geometry Validation') 

777 gen.check() 

778 

779 return lst_file 

780 

781 

782 def run_fastercap_extraction(self, 

783 args: argparse.Namespace, 

784 pex_context: KLayoutExtractionContext, 

785 lst_file: str): 

786 rule('FasterCap Execution') 

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

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

789 

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

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

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

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

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

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

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

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

798 

799 run_fastercap(exe_path=args.fastercap_exe_path, 

800 lst_file_path=lst_file, 

801 log_path=log_path, 

802 tolerance=args.fastercap_tolerance, 

803 d_coeff=args.fastercap_d_coeff, 

804 mesh_refinement_value=args.fastercap_mesh_refinement_value, 

805 ooc_condition=args.fastercap_ooc_condition, 

806 auto_preconditioner=args.fastercap_auto_preconditioner, 

807 galerkin_scheme=args.fastercap_galerkin_scheme, 

808 jacobi_preconditioner=args.fastercap_jacobi_preconditioner) 

809 

810 cap_matrix = fastercap_parse_capacitance_matrix(log_path) 

811 cap_matrix.write_csv(raw_csv_path) 

812 

813 cap_matrix = cap_matrix.averaged_off_diagonals() 

814 cap_matrix.write_csv(avg_csv_path) 

815 

816 cap_matrix_interpreter = FasterCapOutputInterpreter() 

817 

818 netlist_expander = NetlistExpander() 

819 expanded_netlist = netlist_expander.expand( 

820 extracted_netlist=pex_context.lvsdb.netlist(), 

821 top_cell_name=pex_context.annotated_top_cell.name, 

822 cap_matrix=cap_matrix, 

823 cap_matrix_interpreter=cap_matrix_interpreter, 

824 blackbox_devices=args.blackbox_devices 

825 ) 

826 

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

828 netlist_csv_writer = NetlistCSVWriter() 

829 netlist_csv_writer.write_csv(netlist=expanded_netlist, 

830 top_cell_name=pex_context.annotated_top_cell.name, 

831 output_path=expanded_netlist_csv_path) 

832 

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

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

835 for line in f.readlines(): 

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

837 rule() 

838 

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

840 

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

842 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

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

844 

845 # FIXME: should this be already reduced? 

846 if args.output_spice_path: 

847 netlist_printer.write(expanded_netlist, args.output_spice_path) 

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

849 

850 netlist_reducer = NetlistReducer() 

851 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist, 

852 top_cell_name=pex_context.annotated_top_cell.name) 

853 netlist_printer.write(reduced_netlist, reduced_netlist_path) 

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

855 

856 self._fastercap_extracted_csv_path = expanded_netlist_csv_path 

857 

858 def run_magic_extraction(self, 

859 args: argparse.Namespace): 

860 if args.input_mode != InputMode.GDS: 

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

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

863 return 

864 

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

866 magic_log_path = os.path.join(magic_run_dir, 

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

868 magic_script_path = os.path.join(magic_run_dir, 

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

870 

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

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

873 

874 os.makedirs(magic_run_dir, exist_ok=True) 

875 

876 layout = kdb.Layout() 

877 layout.read(args.effective_gds_path) 

878 

879 if args.magic_pre_flatten: 

880 tops = list(layout.top_cells()) 

881 if not tops: 

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

883 for top in tops: 

884 top.flatten(True) 

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

886 layout.write(flattened_gds_path) 

887 gds_path = flattened_gds_path 

888 else: 

889 gds_path = args.effective_gds_path 

890 

891 prepare_magic_script(gds_path=gds_path, 

892 cell_name=args.effective_cell_name, 

893 run_dir_path=magic_run_dir, 

894 script_path=magic_script_path, 

895 output_netlist_path=output_netlist_path, 

896 pex_mode=args.magic_pex_mode, 

897 c_threshold=args.magic_cthresh, 

898 r_threshold=args.magic_rthresh, 

899 threshold=args.magic_thresh, 

900 min_res=args.magic_minres, 

901 min_delay=args.magic_mindel, 

902 halo=args.magic_halo, 

903 short_mode=args.magic_short_mode, 

904 merge_mode=args.magic_merge_mode, 

905 unique_mode=args.magic_unique_mode) 

906 

907 run_magic(exe_path=args.magic_exe_path, 

908 magicrc_path=args.magicrc_path, 

909 script_path=magic_script_path, 

910 log_path=magic_log_path) 

911 

912 magic_pex_run = parse_magic_pex_run(Path(magic_run_dir)) 

913 

914 if args.magic_analyze: 

915 report = rdb.ReportDatabase('') 

916 magic_log_analyzer = MagicLogAnalyzer(magic_pex_run=magic_pex_run, 

917 report=report, 

918 dbu=layout.dbu) 

919 magic_log_analyzer.analyze() 

920 report.save(report_db_path) 

921 

922 rule("Paths") 

923 if args.magic_analyze: 

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

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

926 

927 if os.path.exists(output_netlist_path): 

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

929 shutil.copy(output_netlist_path, args.output_spice_path) 

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

931 

932 rule("MAGIC PEX SPICE netlist") 

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

934 subproc(f.read()) 

935 rule() 

936 

937 def run_fastcap_extraction(self, 

938 args: argparse.Namespace, 

939 pex_context: KLayoutExtractionContext, 

940 lst_file: str): 

941 rule('FastCap2 Execution') 

942 

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

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

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

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

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

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

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

950 

951 run_fastcap(exe_path=args.fastcap_exe_path, 

952 lst_file_path=lst_file, 

953 log_path=log_path) 

954 

955 cap_matrix = fastcap_parse_capacitance_matrix(log_path) 

956 cap_matrix.write_csv(raw_csv_path) 

957 

958 cap_matrix = cap_matrix.averaged_off_diagonals() 

959 cap_matrix.write_csv(avg_csv_path) 

960 

961 cap_matrix_interpreter = FastCapOutputInterpreter() 

962 

963 netlist_expander = NetlistExpander() 

964 expanded_netlist = netlist_expander.expand( 

965 extracted_netlist=pex_context.lvsdb.netlist(), 

966 top_cell_name=pex_context.annotated_top_cell.name, 

967 cap_matrix=cap_matrix, 

968 cap_matrix_interpreter=cap_matrix_interpreter, 

969 blackbox_devices=args.blackbox_devices 

970 ) 

971 

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

973 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

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

975 

976 # FIXME: should this be already reduced? 

977 if args.output_spice_path: 

978 netlist_printer.write(expanded_netlist, args.output_spice_path) 

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

980 

981 netlist_reducer = NetlistReducer() 

982 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist, 

983 top_cell_name=pex_context.annotated_top_cell.name) 

984 netlist_printer.write(reduced_netlist, reduced_netlist_path) 

985 

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

987 

988 def run_kpex_2_5d_engine(self, 

989 args: argparse.Namespace, 

990 pex_context: KLayoutExtractionContext, 

991 tech_info: TechInfo, 

992 report_path: str, 

993 netlist_csv_path: Optional[str], 

994 expanded_netlist_path: Optional[str]): 

995 # TODO: make this separatly configurable 

996 # for now we use 0 

997 args.rcx25d_delaunay_amax = 0 

998 args.rcx25d_delaunay_b = 0.5 

999 

1000 extractor = RCX25Extractor(pex_context=pex_context, 

1001 pex_mode=args.pex_mode, 

1002 delaunay_amax=args.rcx25d_delaunay_amax, 

1003 delaunay_b=args.rcx25d_delaunay_b, 

1004 scale_ratio_to_fit_halo=args.scale_ratio_to_fit_halo, 

1005 tech_info=tech_info, 

1006 report_path=report_path) 

1007 extraction_results = extractor.extract() 

1008 

1009 if netlist_csv_path is not None: 

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

1011 

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

1013 summary = extraction_results.summarize() 

1014 

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

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

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

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

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

1020 

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

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

1023 for line in f.readlines(): 

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

1025 

1026 rule('Extracted netlist CSV') 

1027 subproc(f"{netlist_csv_path}") 

1028 

1029 if expanded_netlist_path is not None: 

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

1031 netlist_expander = RCX25NetlistExpander() 

1032 expanded_netlist = netlist_expander.expand( 

1033 extracted_netlist=pex_context.lvsdb.netlist(), 

1034 top_cell_name=pex_context.annotated_top_cell.name, 

1035 extraction_results=extraction_results, 

1036 blackbox_devices=args.blackbox_devices 

1037 ) 

1038 

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

1040 netlist_printer.write(expanded_netlist, expanded_netlist_path) 

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

1042 

1043 # FIXME: should this be already reduced? 

1044 if args.output_spice_path: 

1045 netlist_printer.write(expanded_netlist, args.output_spice_path) 

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

1047 

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

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

1050 try: 

1051 report = rdb.ReportDatabase('') 

1052 report.load(report_path) # try loading rdb 

1053 except Exception as e: 

1054 rule("Repair broken marker DB") 

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

1056 repair_rdb(report_path) 

1057 

1058 return extraction_results 

1059 

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

1061 def register_log_file_handler(log_path: str, 

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

1063 handler = logging.FileHandler(log_path) 

1064 handler.setLevel(LogLevel.SUBPROCESS) 

1065 if formatter: 

1066 handler.setFormatter(formatter) 

1067 register_additional_handler(handler) 

1068 return handler 

1069 

1070 def reregister_log_file_handler(handler: logging.Handler, 

1071 log_path: str, 

1072 formatter: Optional[logging.Formatter]): 

1073 deregister_additional_handler(handler) 

1074 handler.flush() 

1075 handler.close() 

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

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

1078 if os.path.exists(new_path): 

1079 ctime = os.path.getctime(new_path) 

1080 dt = datetime.fromtimestamp(ctime) 

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

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

1083 shutil.move(new_path, backup_path) 

1084 log_path = shutil.move(log_path, new_path) 

1085 register_log_file_handler(log_path, formatter) 

1086 

1087 # setup preliminary logger 

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

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

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

1091 file_handler_plain = register_log_file_handler(cli_log_path_plain, None) 

1092 file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter) 

1093 try: 

1094 self.validate_args(args) 

1095 except ArgumentValidationError: 

1096 if hasattr(args, 'output_dir_path'): 

1097 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) 

1098 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) 

1099 sys.exit(1) 

1100 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) 

1101 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) 

1102 

1103 set_log_level(args.log_level) 

1104 

1105 @staticmethod 

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

1107 t = os.path.getmtime(filename) 

1108 return datetime.fromtimestamp(t) 

1109 

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

1111 lvsdb = kdb.LayoutVsSchematic() 

1112 

1113 match args.input_mode: 

1114 case InputMode.LVSDB: 

1115 lvsdb.read(args.lvsdb_path) 

1116 case InputMode.GDS: 

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

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

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

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

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

1122 

1123 lvs_needed = True 

1124 

1125 if args.cache_lvs: 

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

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

1128 subproc(lvsdb_cache_path) 

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

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

1131 subproc(lvsdb_cache_path) 

1132 else: 

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

1134 subproc(lvsdb_cache_path) 

1135 lvs_needed = False 

1136 

1137 if lvs_needed: 

1138 lvs_runner = LVSRunner() 

1139 lvs_runner.run_klayout_lvs(exe_path=args.klayout_exe_path, 

1140 lvs_script=args.lvs_script_path, 

1141 gds_path=args.effective_gds_path, 

1142 schematic_path=args.effective_schematic_path, 

1143 log_path=lvs_log_path, 

1144 lvsdb_path=lvsdb_path, 

1145 verbose=args.klayout_lvs_verbose) 

1146 if args.cache_lvs: 

1147 cache_dir_path = os.path.dirname(lvsdb_cache_path) 

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

1149 os.makedirs(cache_dir_path, exist_ok=True) 

1150 shutil.copy(lvsdb_path, lvsdb_cache_path) 

1151 

1152 lvsdb.read(lvsdb_path) 

1153 return lvsdb 

1154 

1155 SUBCOMMANDS = ('extract', 'pex25d') 

1156 LEGACY_SUBCOMMAND = 'extract' 

1157 

1158 @classmethod 

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

1160 """ 

1161 Accept the pre-subcommand, flat command line. 

1162 

1163 Before the verb layer, kpex was invoked as 

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

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

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

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

1168 never mistake one for the other. 

1169 

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

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

1172 once those have moved on. 

1173 """ 

1174 tail = argv[1:] 

1175 if not tail: 

1176 return argv 

1177 first = tail[0] 

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

1179 return argv 

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

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

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

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

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

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

1186 # that fails somewhere else. 

1187 return argv 

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

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

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

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

1192 

1193 @staticmethod 

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

1195 """ 

1196 Whether this invocation sends a PEX25D artifact to stdout. 

1197 

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

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

1200 included — has a chance to print. 

1201 """ 

1202 return any(previous in ('--output_file', '--output_scene') and current == '-' 

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

1204 

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

1206 env = Env.from_os_environ() 

1207 argv = self.normalize_argv(argv) 

1208 

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

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

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

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

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

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

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

1216 writes_to_stdout = self._writes_artifact_to_stdout(argv) 

1217 

1218 with contextlib.ExitStack() as stack: 

1219 if writes_to_stdout: 

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

1221 

1222 if not ({'-v', '--version', '-h', '--help'} & set(argv)): 

1223 rule('Command line arguments') 

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

1225 

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

1227 

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

1229 self.setup_logging(args) 

1230 

1231 try: 

1232 tech_info = TechInfo.from_json(args.tech_pbjson_path, 

1233 dielectric_filter=args.dielectric_filter) 

1234 except TechDefError as e: 

1235 error(str(e)) 

1236 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1237 

1238 match args.command: 

1239 case 'pex25d': 

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

1241 case _: 

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

1243 

1244 def run_pex25d_generation(self, 

1245 args: argparse.Namespace, 

1246 tech_info: TechInfo): 

1247 """ 

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

1249 

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

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

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

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

1254 """ 

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

1256 from .pex25d.codec import save_artifact 

1257 from .pex25d.resolver import ResolveError, resolve 

1258 from .pex25d.validator import validate 

1259 

1260 report = DiagnosticsReport(warnings_are_errors=args.warnings_are_errors) 

1261 

1262 rule('Prepare LVSDB') 

1263 lvsdb = self.create_lvsdb(args) 

1264 

1265 pex_context = KLayoutExtractionContext.prepare_extraction( 

1266 top_cell=args.effective_cell_name, 

1267 lvsdb=lvsdb, 

1268 tech=tech_info, 

1269 blackbox_devices=args.blackbox_devices) 

1270 

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

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

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

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

1275 

1276 rule('PEX25D Generation') 

1277 try: 

1278 pex25d_file = build_pex25d_file( 

1279 pex_context=pex_context, 

1280 tech_info=tech_info, 

1281 cell_name=args.effective_cell_name, 

1282 options=BuilderOptions( 

1283 grid_um=args.pex25d_grid, 

1284 with_source_refs=args.pex25d_with_source_refs, 

1285 dielectric_filter=args.dielectric_filter, 

1286 domain_margin_um=args.pex25d_domain_margin)) 

1287 

1288 if args.pex25d_file_spec is not None: 

1289 save_artifact(pex25d_file, args.pex25d_file_spec, 

1290 comments=args.pex25d_comments) 

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

1292 

1293 scene = None 

1294 if args.pex25d_scene_spec is not None: 

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

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

1297 scene = resolve(pex25d_file, report=report, 

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

1299 save_artifact(scene, args.pex25d_scene_spec, 

1300 comments=args.pex25d_comments) 

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

1302 

1303 if args.pex25d_validate: 

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

1305 scene=scene) 

1306 

1307 except BuildError as e: 

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

1309 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1310 except ResolveError as e: 

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

1312 # scene is worse than none. 

1313 error(str(e)) 

1314 report.render(args.diagnostics_format) 

1315 sys.exit(ExitCode.DIAGNOSTIC_ERRORS) 

1316 except NotImplementedError as e: 

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

1318 sys.exit(ExitCode.NOT_IMPLEMENTED) 

1319 

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

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

1322 with diagnostics_stream(writes_to_stdout=writes_to_stdout) as stream: 

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

1324 

1325 if report.exit_code != ExitCode.OK: 

1326 sys.exit(report.exit_code) 

1327 

1328 def run_extraction(self, 

1329 args: argparse.Namespace, 

1330 tech_info: TechInfo): 

1331 if args.halo is not None: 

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

1333 

1334 if args.run_magic: 

1335 rule('MAGIC') 

1336 self.run_magic_extraction(args) 

1337 

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

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

1340 return 

1341 

1342 rule('Prepare LVSDB') 

1343 lvsdb = self.create_lvsdb(args) 

1344 

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

1346 lvsdb=lvsdb, 

1347 tech=tech_info, 

1348 blackbox_devices=args.blackbox_devices) 

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

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

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

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

1353 

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

1355 pex_context.annotated_layout.write(gds_path) 

1356 

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

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

1359 

1360 def dump_layers(cell: str, 

1361 layers: List[KLayoutExtractedLayerInfo], 

1362 layout_dump_path: str): 

1363 layout = kdb.Layout() 

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

1365 

1366 top_cell = layout.create_cell(cell) 

1367 for ulyr in layers: 

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

1369 li.name = ulyr.lvs_layer_name 

1370 layer = layout.insert_layer(li) 

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

1372 

1373 layout.write(layout_dump_path) 

1374 

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

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

1377 dump_layers(cell=args.effective_cell_name, 

1378 layers=pex_context.unnamed_layers, 

1379 layout_dump_path=layout_dump_path) 

1380 

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

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

1383 nonempty_layers = [l \ 

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

1385 for l in layers.source_layers] 

1386 dump_layers(cell=args.effective_cell_name, 

1387 layers=nonempty_layers, 

1388 layout_dump_path=layout_dump_path) 

1389 else: 

1390 error("No extracted layers found") 

1391 sys.exit(1) 

1392 

1393 if args.run_fastcap or args.run_fastercap: 

1394 lst_file = self.build_fastercap_input(args=args, 

1395 pex_context=pex_context, 

1396 tech_info=tech_info) 

1397 if args.run_fastercap: 

1398 self.run_fastercap_extraction(args=args, 

1399 pex_context=pex_context, 

1400 lst_file=lst_file) 

1401 if args.run_fastcap: 

1402 self.run_fastcap_extraction(args=args, 

1403 pex_context=pex_context, 

1404 lst_file=lst_file) 

1405 

1406 if args.run_2_5D: 

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

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

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

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

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

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

1413 

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

1415 args=args, 

1416 pex_context=pex_context, 

1417 tech_info=tech_info, 

1418 report_path=report_path, 

1419 netlist_csv_path=netlist_csv_path, 

1420 expanded_netlist_path=netlist_spice_path 

1421 ) 

1422 

1423 self._rcx25_extracted_csv_path = netlist_csv_path 

1424 

1425 @property 

1426 def rcx25_extraction_results(self) -> ExtractionResults: 

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

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

1429 return self._rcx25_extraction_results 

1430 

1431 @property 

1432 def rcx25_extracted_csv_path(self) -> str: 

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

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

1435 return self._rcx25_extracted_csv_path 

1436 

1437 @property 

1438 def fastercap_extracted_csv_path(self) -> str: 

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

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

1441 return self._fastercap_extracted_csv_path 

1442 

1443 

1444if __name__ == "__main__": 

1445 cli = KpexCLI() 

1446 cli.main(sys.argv)