Coverage for klayout_pex/kpex_cli.py: 68%
753 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
1#! /usr/bin/env python3
2#
3# --------------------------------------------------------------------------------
4# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
5# Johannes Kepler University, Institute for Integrated Circuits.
6#
7# This file is part of KPEX
8# (see https://github.com/iic-jku/klayout-pex).
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <http://www.gnu.org/licenses/>.
22# SPDX-License-Identifier: GPL-3.0-or-later
23# --------------------------------------------------------------------------------
24#
26import 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 *
43import klayout.db as kdb
44import klayout.rdb as rdb
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_cache import LVSCacheEntry, LVSInputFingerprint, lvs_input_fingerprint
57from .klayout.lvs_runner import LVSError, LVSRunner
58from .klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo, LVSDBError
59from .klayout.netlist_expander import NetlistExpander
60from .klayout.netlist_csv import NetlistCSVWriter
61from .klayout.netlist_printer import NetlistHeader, NetlistPrinter
62from .klayout.netlist_reducer import NetlistReducer
63from .klayout.repair_rdb import repair_rdb
64from .log import (
65 LogLevel,
66 set_log_level,
67 register_additional_handler,
68 deregister_additional_handler,
69 # console,
70 # debug,
71 info,
72 warning,
73 subproc,
74 error,
75 rule
76)
77from .magic.magic_ext_file_parser import parse_magic_pex_run
78from .magic.magic_runner import (
79 MagicPEXMode,
80 MagicShortMode,
81 MagicMergeMode,
82 MagicUniqueMode,
83 run_magic,
84 prepare_magic_script,
85)
86from .magic.magic_log_analyzer import MagicLogAnalyzer
87from .pex25d.artifact import (
88 ArtifactFormat,
89 ArtifactKind,
90 ArtifactNamingError,
91 infer_artifact_spec,
92)
93from .klayout.pex25d_builder import DEFAULT_GRID_UM
94from .pex25d.diagnostics import DiagnosticsReport, ExitCode, diagnostics_stream
95from .pex25d.pex25d_cli import Pex25DCLI
96from .pdk_config import PDK, PDKConfig
97from .rcx25.extractor import CExtractionTechError, RCX25Extractor, ExtractionResults
98from .rcx25.netlist_checks import check_rc_netlist, RCNetlistCheckError
99from .rcx25.netlist_expander import RCX25NetlistExpander
100from .rcx25.pex_mode import PEXMode
101from .rcx25.r.r_extractor import RExtractionTechError
102from .device_models import DeviceModelError
103from .tech_info import TechDefError, TechInfo
104from .tool_version_constraints import (
105 Tool,
106 VersionCheckMode,
107 check_tool_versions
108)
109from .util.multiple_choice import MultipleChoicePattern
110from .util.argparse_helpers import render_enum_help, true_or_false
111from .version import __version__
114# ------------------------------------------------------------------------------------
116PROGRAM_NAME = "kpex"
119class ArgumentValidationError(Exception):
120 pass
123class InputMode(StrEnum):
124 LVSDB = "lvsdb"
125 GDS = "gds"
128class KpexCLI:
129 # ---------------------------------------------------------- argument groups
130 #
131 # Each group is attached to a *given* parser rather than to one flat parser,
132 # so that a subcommand declares exactly the options it can act on. That is
133 # the point of the verb layer: `kpex pex25d` cannot be handed --magic_halo,
134 # and the validation below no longer has to hand-check which combinations
135 # of flags make sense together.
137 @staticmethod
138 def _add_special_options(parser: argparse.ArgumentParser,
139 include_threads: bool = False) -> None:
140 group_special = parser.add_argument_group("Special Options")
141 group_special.add_argument("--help", "-h", action='help',
142 help="show this help message and exit")
143 group_special.add_argument("--log_level", dest='log_level', default='subprocess',
144 help=render_enum_help(topic='log_level', enum_cls=LogLevel))
145 group_special.add_argument("--tool_version_checks", dest='version_check_mode',
146 default=VersionCheckMode.DEFAULT, type=VersionCheckMode,
147 choices=list(VersionCheckMode),
148 help=render_enum_help(topic='tool_version_checks',
149 enum_cls=VersionCheckMode))
150 if include_threads:
151 group_special.add_argument("--threads", dest='num_threads', type=int,
152 default=os.cpu_count() * 4,
153 help="number of threads (e.g. for FasterCap) "
154 "(default is %(default)s)")
156 @staticmethod
157 def _add_pex_setup_arguments(parser: argparse.ArgumentParser,
158 env: Env,
159 include_spice_output: bool = True) -> None:
160 group_pex = parser.add_argument_group("Parasitic Extraction Setup")
162 all_pdk_choices = list(PDK) + list(PDK.legacy_aliases().keys())
164 default_pdk = env.default_pdk
165 pdk_help = render_enum_help(topic='pdk', enum_cls=PDK, print_default=False)
166 if default_pdk:
167 pdk_help += f" (default is '{default_pdk}')"
169 group_pex.add_argument("--pdk", dest="pdk", required=default_pdk is None,
170 type=PDK.from_string, choices=all_pdk_choices,
171 help=pdk_help, default=default_pdk)
173 group_pex.add_argument("--out_dir", dest="output_dir_base_path", default="output",
174 help="Run directory path (default is '%(default)s')")
176 if include_spice_output:
177 group_pex.add_argument("--out_spice", "-o", dest="output_spice_path", default=None,
178 help="Optional additional SPICE output path (default is none)")
181 @staticmethod
182 def _add_pex_input_arguments(parser: argparse.ArgumentParser) -> None:
183 group_pex_input = parser.add_argument_group("Parasitic Extraction Input",
184 description="Either LVS is run, or an existing LVSDB is used")
185 group_pex_input.add_argument("--gds", "-g", dest="gds_path", default=None,
186 help="GDS path (for LVS)")
187 group_pex_input.add_argument("--schematic", "-s", dest="schematic_path",
188 help="Schematic SPICE netlist path (for LVS). "
189 "If none given, a dummy schematic will be created")
190 # Developer shortcut, not a supported entry point. It exists so that a
191 # PEX25D or engine run can be repeated without paying for LVS again while
192 # working on the code downstream of it. An LVSDB is tied to the kpex
193 # version and PDK that produced it and is not an interchange format —
194 # PEX25D is. Anyone who wants to hand a scene to somebody else, or keep
195 # one, should be writing PEX25D, not passing LVSDBs around.
196 group_pex_input.add_argument("--lvsdb", "-l", dest="lvsdb_path", default=None,
197 help="KLayout PEX-LVSDB path from a previous run "
198 "(bypass PEX-LVS). Developer shortcut for "
199 "re-running without repeating LVS; not an "
200 "interchange format — use PEX25D for that")
201 group_pex_input.add_argument("--cell", "-c", dest="cell_name", default=None,
202 help="Cell (default is the top cell)")
204 group_pex_input.add_argument("--cache-lvs", dest="cache_lvs",
205 type=true_or_false, default=True,
206 help="Reuse the cached LVSDB while the LVS inputs are unchanged "
207 "(layout, schematic, LVS deck, KLayout version) (default is %(default)s)")
208 group_pex_input.add_argument("--cache-dir", dest="cache_dir_path", default=None,
209 help="Path for cached LVSDB (default is .kpex_cache within --out_dir)")
210 group_pex_input.add_argument("--lvs-verbose", dest="klayout_lvs_verbose",
211 type=true_or_false, default=False,
212 help="Verbose KLayout LVS output (default is %(default)s)")
215 @staticmethod
216 def _add_engine_arguments(parser: argparse.ArgumentParser) -> None:
217 group_pex_options = parser.add_argument_group("Extraction Engines")
218 group_pex_options.add_argument("--fastercap", dest="run_fastercap",
219 action='store_true', default=False,
220 help="Run FasterCap engine (default is %(default)s)")
221 group_pex_options.add_argument("--fastcap", dest="run_fastcap",
222 action='store_true', default=False,
223 help="Run FastCap2 engine (default is %(default)s)")
224 group_pex_options.add_argument("--magic", dest="run_magic",
225 action='store_true', default=False,
226 help="Run MAGIC engine (default is %(default)s)")
227 group_pex_options.add_argument("--2.5D", dest="run_2_5D",
228 action='store_true', default=False,
229 help="Run 2.5D analytical engine (default is %(default)s)")
232 @staticmethod
233 def _add_extraction_options(parser: argparse.ArgumentParser) -> None:
234 group_pex_options = parser.add_argument_group("Parasitic Extraction Options")
235 group_pex_options.add_argument("--blackbox", dest="blackbox_devices",
236 type=true_or_false, default=False, # TODO: in the future this should be True by default
237 help="Blackbox devices like MIM/MOM caps, as they are handled by SPICE models "
238 "(default is %(default)s for testing now)")
240 @staticmethod
241 def _add_tech_arguments(parser: argparse.ArgumentParser) -> None:
242 group_tech = parser.add_argument_group("Technology Options")
243 group_tech.add_argument("--diel", dest="dielectric_filter",
244 type=str, default="all",
245 help=f"Comma separated list of dielectric filter patterns. "
246 f"Allowed patterns are: (none, all, -dielname1, +dielname2) "
247 f"(default is %(default)s)")
249 @staticmethod
250 def _add_fastercap_arguments(parser: argparse.ArgumentParser) -> None:
251 group_fastercap = parser.add_argument_group("FasterCap Options")
252 group_fastercap.add_argument("--k_void", "-k", dest="k_void",
253 type=float, default=3.9,
254 help="Dielectric constant of void (default is %(default)s)")
256 # TODO: reflect that these are also now used by KPEX/2.5D engine!
257 group_fastercap.add_argument("--delaunay_amax", "-a", dest="delaunay_amax",
258 type=float, default=50,
259 help="Delaunay triangulation maximum area (default is %(default)s)")
260 group_fastercap.add_argument("--delaunay_b", "-b", dest="delaunay_b",
261 type=float, default=0.5,
262 help="Delaunay triangulation b (default is %(default)s)")
263 group_fastercap.add_argument("--geo_check", dest="geometry_check",
264 type=true_or_false, default=False,
265 help=f"Validate geometries before passing to FasterCap "
266 f"(default is False)")
267 group_fastercap.add_argument("--tolerance", dest="fastercap_tolerance",
268 type=float, default=0.05,
269 help="FasterCap -aX error tolerance (default is %(default)s)")
270 group_fastercap.add_argument("--d_coeff", dest="fastercap_d_coeff",
271 type=float, default=0.5,
272 help=f"FasterCap -d direct potential interaction coefficient to mesh refinement "
273 f"(default is %(default)s)")
274 group_fastercap.add_argument("--mesh", dest="fastercap_mesh_refinement_value",
275 type=float, default=0.5,
276 help="FasterCap -m Mesh relative refinement value (default is %(default)s)")
277 group_fastercap.add_argument("--ooc", dest="fastercap_ooc_condition",
278 type=float, default=2,
279 help="FasterCap -f out-of-core free memory to link memory condition "
280 "(0 = don't go OOC, default is %(default)s)")
281 group_fastercap.add_argument("--auto_precond", dest="fastercap_auto_preconditioner",
282 type=true_or_false, default=True,
283 help=f"FasterCap -ap Automatic preconditioner usage (default is %(default)s)")
284 group_fastercap.add_argument("--galerkin", dest="fastercap_galerkin_scheme",
285 action='store_true', default=False,
286 help=f"FasterCap -g Use Galerkin scheme (default is %(default)s)")
287 group_fastercap.add_argument("--jacobi", dest="fastercap_jacobi_preconditioner",
288 action='store_true', default=False,
289 help="FasterCap -pj Use Jacobi preconditioner (default is %(default)s)")
292 @staticmethod
293 def _add_magic_arguments(parser: argparse.ArgumentParser, env: Env) -> None:
294 group_magic = parser.add_argument_group("MAGIC Options")
296 default_magicrc_path = env.default_magicrc_path
297 if default_magicrc_path:
298 magicrc_help = f"Path to magicrc configuration file (default is '{default_magicrc_path}')"
299 else:
300 magicrc_help = "Path to magicrc configuration file "\
301 "(default not available, PDK and PDK_ROOT must be set!)"
303 group_magic.add_argument('--magicrc', dest='magicrc_path', default=default_magicrc_path,
304 help=magicrc_help)
305 group_magic.add_argument("--magic_mode", dest='magic_pex_mode',
306 default=MagicPEXMode.DEFAULT, type=MagicPEXMode, choices=list(MagicPEXMode),
307 help=render_enum_help(topic='magic_mode', enum_cls=MagicPEXMode))
308 group_magic.add_argument("--magic_cthresh", dest="magic_cthresh",
309 type=float, default=0.01,
310 help="Threshold (in fF) for ignored parasitic capacitances (default is %(default)s). "
311 "(MAGIC command: ext2spice cthresh <value>)")
312 group_magic.add_argument("--magic_rthresh", dest="magic_rthresh",
313 type=int, default=100,
314 help="Threshold (in Ω) for ignored parasitic resistances (default is %(default)s). "
315 "(MAGIC command: ext2spice rthresh <value>)")
316 group_magic.add_argument("--magic_thresh", dest="magic_thresh",
317 type=int, default=None,
318 help="Lumped resistance threshold (in mΩ) to trigger network extraction. "
319 "If not supplied, MAGIC's default is implicitly used. "
320 "(MAGIC command: extresist threshold <value>)")
321 group_magic.add_argument("--magic_minres", dest="magic_minres",
322 type=int, default=None,
323 help="Threshold (in mΩ) for removing individual resistors during network simplification. "
324 "If not supplied, MAGIC's default is implicitly used. "
325 "(MAGIC command: extresist minres <value>)")
326 group_magic.add_argument("--magic_mindel", dest="magic_mindel",
327 type=int, default=None,
328 help="Minimum delay threshold (in ps) to trigger network extraction. "
329 "If not supplied, MAGIC's default is implicitly used. "
330 "(MAGIC command: extresist mindelay <value>)")
331 group_magic.add_argument("--magic_halo", dest="magic_halo",
332 type=float, default=None,
333 help="Custom sidewall halo distance (in µm) "
334 "(MAGIC command: extract halo <value>) (default is no custom halo)")
335 group_magic.add_argument("--magic_short", dest='magic_short_mode',
336 default=MagicShortMode.DEFAULT, type=MagicShortMode, choices=list(MagicShortMode),
337 help=render_enum_help(topic='magic_short', enum_cls=MagicShortMode))
338 group_magic.add_argument("--magic_merge", dest='magic_merge_mode',
339 default=MagicMergeMode.DEFAULT, type=MagicMergeMode, choices=list(MagicMergeMode),
340 help=render_enum_help(topic='magic_merge', enum_cls=MagicMergeMode))
341 group_magic.add_argument("--magic_pre_flatten", dest='magic_pre_flatten',
342 action='store_true', default=False,
343 help="Flatten GDS before reading it with Magic (default is %(default)s).")
344 group_magic.add_argument("--magic_unique", dest='magic_unique_mode',
345 default=MagicUniqueMode.DEFAULT, type=MagicUniqueMode, choices=list(MagicUniqueMode),
346 help="Control net uniqueness during extraction "
347 " ('implicit' omits the command, uses MAGIC's default value). "
348 + render_enum_help(topic='magic_unique', enum_cls=MagicUniqueMode))
349 group_magic.add_argument("--magic_analyze", dest='magic_analyze',
350 type=true_or_false, default=False,
351 help="Analyze MAGIC extraction files (report as KLayout RDB)")
353 @staticmethod
354 def _add_analytical_25d_arguments(parser: argparse.ArgumentParser) -> None:
355 group_25d = parser.add_argument_group("2.5D Options")
356 group_25d.add_argument("--mode", dest='pex_mode',
357 default=PEXMode.DEFAULT, type=PEXMode, choices=list(PEXMode),
358 help=render_enum_help(topic='mode', enum_cls=PEXMode))
359 group_25d.add_argument("--halo", dest="halo",
360 type=float, default=None,
361 help="Custom sidewall halo distance (in µm) to override tech info "
362 "(default is no custom halo)")
363 group_25d.add_argument("--scale", dest="scale_ratio_to_fit_halo",
364 type=true_or_false, default=True,
365 help=f"Scale fringe ratios, so that halo distance is 100%% (default is %(default)s)")
366 group_25d.add_argument("--check", dest="check_rc_netlist",
367 type=true_or_false, default=None,
368 help="Fail the run (after writing the netlist), if the extracted netlist is "
369 "inconsistent with the LVS netlist, e.g. a device terminal off the resistor "
370 "network of its net, a port that touches nothing, or a lost capacitance "
371 "(default is True for --mode RC, otherwise these are warnings)")
374 @staticmethod
375 def _add_pex25d_output_arguments(parser: argparse.ArgumentParser) -> None:
376 group_out = parser.add_argument_group(
377 "PEX25D Output",
378 description="At least one of --output_file / --output_scene must be given. "
379 "They are two options rather than one --emit switch because the "
380 "two artifacts are different things with different consumers, and "
381 "a single switch would leave it unclear where each one lands."
382 )
383 group_out.add_argument("--output_file", dest='pex25d_file_path',
384 default=None, metavar='PATH',
385 help="Write the unresolved PEX25DFile here ('-' for stdout). "
386 "The literal form: one message per PEX25D record, "
387 "references still plain strings, nothing derived. This is "
388 "what a reader produces and what a validator checks.")
389 group_out.add_argument("--output_scene", dest='pex25d_scene_path',
390 default=None, metavar='PATH',
391 help="Write the resolved PEX25DScene here ('-' for stdout). "
392 "The adapter-ready form: absolute z extents, CONNECTS / "
393 "BETWEEN / WRAPS resolved, wrap depth flattened, terminal "
394 "intersections computed.")
395 group_out.add_argument("--format", dest='pex25d_format',
396 default=ArtifactFormat.AUTO, type=ArtifactFormat,
397 choices=list(ArtifactFormat),
398 help="Override the encoding otherwise inferred from the output "
399 "file names (default is '%(default)s'). Naming convention: "
400 "NAME.pex25d is text, NAME.pex25d.pb is binary protobuf, "
401 "NAME.pex25d.textpb is protobuf text format, and a .scene "
402 "infix marks a resolved scene. A trailing .gz is honoured.")
403 group_out.add_argument("--grid", dest='pex25d_grid',
404 default=DEFAULT_GRID_UM, metavar='UM',
405 help="Coordinate grid in µm (default is %(default)s). "
406 "Every z offset, thickness and coordinate must be "
407 "an integer multiple of it, and it must divide the "
408 "layout DBU exactly.")
409 group_out.add_argument("--domain_margin", dest='pex25d_domain_margin',
410 type=float, default=None, metavar='UM',
411 help="Emit DOMAIN_MARGIN with this clearance in µm "
412 "(default is none, leaving the domain unset for "
413 "the solver adapter to choose)")
414 group_out.add_argument("--validate", dest='pex25d_validate',
415 type=true_or_false, default=True,
416 help="Check the generated artifacts against the reference "
417 "validator before reporting (default is %(default)s). "
418 "A defect in the technology data — a profile declared "
419 "twice, a film anchored on the wrong link — otherwise "
420 "shows up only when somebody runs 'pex25d validate' on "
421 "the result. The artifacts are written either way; the "
422 "diagnostics decide the exit code.")
423 group_out.add_argument("--comments", dest='pex25d_comments',
424 type=true_or_false, default=False,
425 help="Include the syntax hints from the specification as "
426 "comments (default is %(default)s). Text format only — "
427 "the protobuf encodings have no comments.")
428 group_out.add_argument("--with_source_refs", dest='pex25d_with_source_refs',
429 type=true_or_false, default=False,
430 help="Populate every SourceRef (default is %(default)s). "
431 "Protobuf output formats only — the text format has no "
432 "spelling for a source reference. Roughly doubles the size "
433 "of a layout-scale file, and means little for geometry "
434 "built in memory rather than parsed from text; useful "
435 "mainly when diffing generated output against a "
436 "hand-written file.")
438 # ------------------------------------------------------------------ parsing
440 @staticmethod
441 def parse_args(arg_list: List[str],
442 env: Env) -> argparse.Namespace:
443 # epilog = f"See '{PROGRAM_NAME} <subcommand> -h' for help on subcommand"
444 epilog = EnvVar.help_epilog_table()
445 epilog_md = rich.console.Group(
446 rich.text.Text('Environmental variables:', style='argparse.groups'),
447 rich.markdown.Markdown(epilog, style='argparse.text')
448 )
449 main_parser = argparse.ArgumentParser(prog=PROGRAM_NAME,
450 description=f"{PROGRAM_NAME}: "
451 f"KLayout-integrated Parasitic Extraction Tool",
452 epilog=epilog_md,
453 add_help=False,
454 formatter_class=RichHelpFormatter)
456 group_special = main_parser.add_argument_group("Special Options")
457 group_special.add_argument("--help", "-h", action='help',
458 help="show this help message and exit")
459 group_special.add_argument("--version", "-v", action='version',
460 version=f'{PROGRAM_NAME} {__version__}')
462 subparsers = main_parser.add_subparsers(dest="command", metavar='<subcommand>',
463 help="Sub-commands help")
465 parser_extract = subparsers.add_parser(
466 "extract",
467 help="Run one or more parasitic extraction engines",
468 description="Run PEX end to end: LVS for connectivity, then the selected "
469 "engine(s), then netlist expansion and reduction.",
470 add_help=False, formatter_class=RichHelpFormatter, epilog=epilog_md)
471 KpexCLI._add_special_options(parser_extract, include_threads=True)
472 KpexCLI._add_pex_setup_arguments(parser_extract, env)
473 KpexCLI._add_pex_input_arguments(parser_extract)
474 KpexCLI._add_engine_arguments(parser_extract)
475 KpexCLI._add_extraction_options(parser_extract)
476 KpexCLI._add_tech_arguments(parser_extract)
477 KpexCLI._add_fastercap_arguments(parser_extract)
478 KpexCLI._add_magic_arguments(parser_extract, env)
479 KpexCLI._add_analytical_25d_arguments(parser_extract)
481 parser_pex25d = subparsers.add_parser(
482 "pex25d",
483 help="Generate PEX25D solver input from a layout, without running a solver",
484 description="Run LVS for connectivity and write the scene as PEX25D, stopping "
485 "before any engine is started. This is the cut point in the "
486 "pipeline that lets the geometry be inspected, archived, diffed, "
487 "or handed to somebody else's solver. Format-only work on the "
488 "result — validating, re-encoding, exporting to an engine's "
489 "native input — is the standalone 'pex25d' tool's job.",
490 add_help=False, formatter_class=RichHelpFormatter, epilog=epilog_md)
491 KpexCLI._add_special_options(parser_pex25d)
492 KpexCLI._add_pex_setup_arguments(parser_pex25d, env, include_spice_output=False)
493 KpexCLI._add_pex_input_arguments(parser_pex25d)
494 KpexCLI._add_extraction_options(parser_pex25d)
495 KpexCLI._add_tech_arguments(parser_pex25d)
496 KpexCLI._add_pex25d_output_arguments(parser_pex25d)
497 Pex25DCLI._add_diagnostics_arguments(parser_pex25d)
499 if arg_list is None:
500 arg_list = sys.argv[1:]
501 args = main_parser.parse_args(arg_list)
502 if args.command is None:
503 main_parser.print_help()
504 sys.exit(ExitCode.USAGE)
506 # The engine switches only exist on 'extract'. Defaulting them here keeps
507 # the shared validation and logging paths free of subcommand checks.
508 for engine_attr in ('run_magic', 'run_fastcap', 'run_fastercap', 'run_2_5D'):
509 if not hasattr(args, engine_attr):
510 setattr(args, engine_attr, False)
512 # environmental variables and their defaults
513 args.fastcap_exe_path = env[EnvVar.FASTCAP_EXE]
514 args.fastercap_exe_path = env[EnvVar.FASTERCAP_EXE]
515 args.klayout_exe_path = env[EnvVar.KLAYOUT_EXE]
516 args.magic_exe_path = env[EnvVar.MAGIC_EXE]
518 return args
520 @staticmethod
521 def validate_args(args: argparse.Namespace):
522 found_errors = False
524 pdk_config: PDKConfig = args.pdk.config
525 args.tech_pbjson_path = pdk_config.tech_pb_json_path
526 args.lvs_script_path = pdk_config.pex_lvs_script_path
528 def input_file_stem(path: str):
529 # could be *.gds, or *.gds.gz, so remove all extensions
530 return os.path.basename(path).split(sep='.')[0]
532 if not os.path.isfile(args.tech_pbjson_path):
533 error(f"Can't read technology file at path {args.tech_pbjson_path}")
534 found_errors = True
536 if not os.path.isfile(args.lvs_script_path):
537 error(f"Can't locate LVS script path at {args.lvs_script_path}")
538 found_errors = True
540 rule('Input Layout')
542 # Which engines may be combined with which inputs is a question only
543 # 'extract' has; a dump run has no engine to disagree with.
544 if args.command == 'extract':
545 # check engines VS input possiblities
546 match (args.run_magic, args.run_fastcap, args.run_fastercap, args.run_2_5D,
547 args.gds_path, args.lvsdb_path):
548 case (True, _, _, _, None, _):
549 error(f"Running PEX engine MAGIC requires --gds (--lvsdb not possible)")
550 found_errors = True
551 case (False, False, False, False, _, _): # at least one engine must be activated
552 error("No PEX engines activated")
553 engine_help = """
554 | Argument | Description |
555 | ------------ | ------------------------------- |
556 | --2.5D | Run KPEX/2.5D analytical engine |
557 | --fastcap | Run KPEX/FastCap2 3D engine |
558 | --fastercap | Run KPEX/FasterCap 3D engine |
559 | --magic | Run MAGIC wrapper engine |
560 """
561 subproc(f"\n\nPlease activate one or more engines using the arguments:")
562 rich.print(rich.markdown.Markdown(engine_help, style='argparse.text'))
563 found_errors = True
564 case (_, _, _, _, None, None):
565 error(f"Neither GDS nor LVSDB was provided")
566 found_errors = True
568 # check if we find magicrc
569 if args.run_magic:
570 if args.magicrc_path is None:
571 error(f"magicrc not available, requires any those:\n"
572 f"\t• set environmental variables PDK_ROOT / PDK\n"
573 f"\t• pass argument --magicrc")
574 found_errors = True
575 else:
576 result = validate_files([args.magicrc_path])
577 for f in result.failures:
578 error(f"Invalid magicrc: {f.reason} at {str(f.path)}")
579 found_errors = True
581 # input mode: LVS or existing LVSDB?
582 if args.gds_path:
583 info(f"GDS input file passed, running in LVS mode")
584 args.input_mode = InputMode.GDS
585 if not os.path.isfile(args.gds_path):
586 error(f"Can't read GDS file (LVS input) at path {args.gds_path}")
587 found_errors = True
588 else:
589 args.layout = kdb.Layout()
590 args.layout.read(args.gds_path)
592 top_cells = args.layout.top_cells()
594 if args.cell_name: # explicit user-specified cell name
595 args.effective_cell_name = args.cell_name
597 found_cell: Optional[kdb.Cell] = None
598 for cell in args.layout.cells('*'):
599 if cell.name == args.effective_cell_name:
600 found_cell = cell
601 break
602 if not found_cell:
603 error(f"Could not find cell {args.cell_name} in GDS {args.gds_path}")
604 found_errors = True
606 is_only_top_cell = len(top_cells) == 1 and top_cells[0].name == args.cell_name
607 if is_only_top_cell:
608 info(f"Found cell {args.cell_name} in GDS {args.gds_path} (only top cell)")
609 else: # there are other cells => extract the top cell to a tmp layout
610 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}"
611 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id)
612 os.makedirs(args.output_dir_path, exist_ok=True)
613 args.effective_gds_path = os.path.join(args.output_dir_path,
614 f"{args.cell_name}_exported.gds.gz")
615 info(f"Found cell {args.cell_name} in GDS {args.gds_path}, "
616 f"but it is not the only top cell, "
617 f"so layout is exported to: {args.effective_gds_path}")
619 found_cell.write(args.effective_gds_path)
620 else: # find top cell
621 if len(top_cells) == 1:
622 args.effective_cell_name = top_cells[0].name
623 info(f"No explicit top cell specified, using top cell '{args.effective_cell_name}'")
624 else:
625 args.effective_cell_name = 'TOP'
626 error(f"Could not determine the default top cell in GDS {args.gds_path}, "
627 f"there are multiple: {', '.join([c.name for c in top_cells])}. "
628 f"Use --cell to specify the cell")
629 found_errors = True
631 if not hasattr(args, 'effective_gds_path'):
632 args.effective_gds_path = args.gds_path
633 elif args.lvsdb_path is not None:
634 info(f"LVSDB input file passed, bypassing LVS")
635 args.input_mode = InputMode.LVSDB
636 if not os.path.isfile(args.lvsdb_path):
637 error(f"Can't read KLayout LVSDB file at path {args.lvsdb_path}")
638 found_errors = True
639 else:
640 lvsdb = kdb.LayoutVsSchematic()
641 lvsdb.read(args.lvsdb_path)
642 top_cell: kdb.Cell = lvsdb.internal_top_cell()
643 args.effective_cell_name = top_cell.name
645 if hasattr(args, 'effective_cell_name'):
646 run_dir_id: str
647 match args.input_mode:
648 case InputMode.GDS:
649 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}"
650 case InputMode.LVSDB:
651 run_dir_id = f"{input_file_stem(args.lvsdb_path)}__{args.effective_cell_name}"
652 case _:
653 raise NotImplementedError(f"Unknown input mode {args.input_mode}")
655 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id)
656 os.makedirs(args.output_dir_path, exist_ok=True)
657 if args.input_mode == InputMode.GDS:
658 if args.schematic_path:
659 args.effective_schematic_path = args.schematic_path
660 if not os.path.isfile(args.schematic_path):
661 error(f"Can't read schematic (LVS input) at path {args.schematic_path}")
662 found_errors = True
663 else:
664 info(f"LVS input schematic not specified (argument --schematic), using dummy schematic")
665 args.effective_schematic_path = os.path.join(args.output_dir_path,
666 f"{args.effective_cell_name}_dummy_schematic.spice")
667 with open(args.effective_schematic_path, 'w', encoding='utf-8') as f:
668 f.writelines([
669 f".subckt {args.effective_cell_name} VDD VSS\n",
670 '.ends\n',
671 '.end\n'
672 ])
674 try:
675 args.log_level = LogLevel[args.log_level.upper()]
676 except KeyError:
677 error(f"Requested log level {args.log_level.lower()} does not exist, "
678 f"{render_enum_help(topic='log_level', enum_cls=LogLevel, print_default=False)}")
679 found_errors = True
681 try:
682 pattern_string: str = args.dielectric_filter
683 args.dielectric_filter = MultipleChoicePattern(pattern=pattern_string)
684 except ValueError as e:
685 error("Failed to parse --diel arg", e)
686 found_errors = True
688 if args.cache_dir_path is None:
689 args.cache_dir_path = os.path.join(args.output_dir_base_path, '.kpex_cache')
691 if args.command == 'pex25d':
692 if args.pex25d_file_path is None and args.pex25d_scene_path is None:
693 error("Nothing to write. Give --output_file (the unresolved PEX25DFile), "
694 "--output_scene (the resolved PEX25DScene), or both.")
695 found_errors = True
697 if args.pex25d_file_path is not None \
698 and args.pex25d_file_path == args.pex25d_scene_path:
699 error("--output_file and --output_scene point at the same destination")
700 found_errors = True
702 # Resolved here rather than at write time so that a mistyped output
703 # name fails now, and not after a multi-minute LVS run.
704 args.pex25d_file_spec = None
705 args.pex25d_scene_spec = None
706 try:
707 if args.pex25d_file_path is not None:
708 args.pex25d_file_spec = infer_artifact_spec(
709 args.pex25d_file_path,
710 kind=ArtifactKind.FILE,
711 format=args.pex25d_format,
712 default_format=ArtifactFormat.TEXT)
713 if args.pex25d_scene_path is not None:
714 args.pex25d_scene_spec = infer_artifact_spec(
715 args.pex25d_scene_path,
716 kind=ArtifactKind.SCENE,
717 format=args.pex25d_format,
718 default_format=ArtifactFormat.PB)
719 except ArtifactNamingError as e:
720 error(str(e))
721 found_errors = True
723 # KLayout runs the LVS script, which the MAGIC wrapper does not need
724 # ("no need to run LVS etc if only running magic engine") and which an
725 # LVSDB input has already been through.
726 needs_lvs = getattr(args, 'input_mode', None) == InputMode.GDS and \
727 (args.command != 'extract' or
728 args.run_fastcap or args.run_fastercap or args.run_2_5D)
730 exe_path_by_tool: Dict[Tool, str] = {}
731 if needs_lvs:
732 exe_path_by_tool[Tool.KLAYOUT] = args.klayout_exe_path
733 if args.run_magic:
734 exe_path_by_tool[Tool.MAGIC] = args.magic_exe_path
735 if args.run_fastercap:
736 exe_path_by_tool[Tool.FASTERCAP] = args.fastercap_exe_path
737 if args.run_fastcap:
738 exe_path_by_tool[Tool.FASTCAP] = args.fastcap_exe_path
740 # Only the tools this run will actually start have to be there.
741 for tool, exe_path in list(exe_path_by_tool.items()):
742 if not (os.path.isfile(exe_path) or shutil.which(exe_path)):
743 error(f"Can't locate {tool.value} executable at {exe_path} "
744 f"(see {tool.env_var.value})")
745 found_errors = True
746 del exe_path_by_tool[tool]
748 if not check_tool_versions(exe_path_by_tool=exe_path_by_tool,
749 pdk=args.pdk,
750 mode=args.version_check_mode):
751 found_errors = True
753 if found_errors:
754 raise ArgumentValidationError("Argument validation failed")
756 def create_netlist_printer(self,
757 args: argparse.Namespace,
758 extraction_engine: ExtractionEngine,
759 tech_info: TechInfo) -> NetlistPrinter:
760 header = NetlistHeader(kpex_version=__version__,
761 extraction_engine=str(extraction_engine),
762 tech=args.pdk.name.lower(),
763 date=datetime.now())
764 return NetlistPrinter(header=header,
765 device_models=tech_info.device_models)
767 def build_fastercap_input(self,
768 args: argparse.Namespace,
769 pex_context: KLayoutExtractionContext,
770 tech_info: TechInfo) -> str:
771 rule('Process stackup')
772 fastercap_input_builder = FasterCapInputBuilder(pex_context=pex_context,
773 tech_info=tech_info,
774 substrate_net_name=pex_context.substrate_net_name,
775 k_void=args.k_void,
776 delaunay_amax=args.delaunay_amax,
777 delaunay_b=args.delaunay_b)
778 gen: FasterCapModelGenerator = fastercap_input_builder.build()
780 rule('FasterCap Input File Generation')
781 faster_cap_input_dir_path = os.path.join(args.output_dir_path, 'FasterCap_Input_Files')
782 os.makedirs(faster_cap_input_dir_path, exist_ok=True)
784 lst_file = gen.write_fastcap(output_dir_path=faster_cap_input_dir_path, prefix='FasterCap_Input_')
786 rule('STL File Generation')
787 geometry_dir_path = os.path.join(args.output_dir_path, 'Geometries')
788 os.makedirs(geometry_dir_path, exist_ok=True)
789 gen.dump_stl(output_dir_path=geometry_dir_path, prefix='')
791 if args.geometry_check:
792 rule('Geometry Validation')
793 gen.check()
795 return lst_file
798 def run_fastercap_extraction(self,
799 args: argparse.Namespace,
800 pex_context: KLayoutExtractionContext,
801 lst_file: str):
802 rule('FasterCap Execution')
803 info(f"Configure number of OpenMP threads (environmental variable OMP_NUM_THREADS) as {args.num_threads}")
804 os.environ['OMP_NUM_THREADS'] = f"{args.num_threads}"
806 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Output.txt")
807 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Raw.csv")
808 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Avg.csv")
809 expanded_netlist_path = os.path.join(args.output_dir_path,
810 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.cir")
811 expanded_netlist_csv_path = os.path.join(args.output_dir_path,
812 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.csv")
813 reduced_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Reduced_Netlist.cir")
815 run_fastercap(exe_path=args.fastercap_exe_path,
816 lst_file_path=lst_file,
817 log_path=log_path,
818 tolerance=args.fastercap_tolerance,
819 d_coeff=args.fastercap_d_coeff,
820 mesh_refinement_value=args.fastercap_mesh_refinement_value,
821 ooc_condition=args.fastercap_ooc_condition,
822 auto_preconditioner=args.fastercap_auto_preconditioner,
823 galerkin_scheme=args.fastercap_galerkin_scheme,
824 jacobi_preconditioner=args.fastercap_jacobi_preconditioner)
826 cap_matrix = fastercap_parse_capacitance_matrix(log_path)
827 cap_matrix.write_csv(raw_csv_path)
829 cap_matrix = cap_matrix.averaged_off_diagonals()
830 cap_matrix.write_csv(avg_csv_path)
832 cap_matrix_interpreter = FasterCapOutputInterpreter()
834 netlist_expander = NetlistExpander()
835 expanded_netlist = netlist_expander.expand(
836 extracted_netlist=pex_context.lvsdb.netlist(),
837 top_cell_name=pex_context.annotated_top_cell.name,
838 cap_matrix=cap_matrix,
839 cap_matrix_interpreter=cap_matrix_interpreter,
840 blackbox_devices=args.blackbox_devices,
841 device_models=pex_context.tech.device_models,
842 substrate_net_name=pex_context.substrate_net_name
843 )
845 # create a nice CSV for reports, useful for spreadsheets
846 netlist_csv_writer = NetlistCSVWriter()
847 netlist_csv_writer.write_csv(netlist=expanded_netlist,
848 top_cell_name=pex_context.annotated_top_cell.name,
849 output_path=expanded_netlist_csv_path)
851 rule("Extended netlist (CSV format):")
852 with open(expanded_netlist_csv_path, 'r') as f:
853 for line in f.readlines():
854 subproc(line[:-1]) # abusing subproc, simply want verbatim
855 rule()
857 info(f"Wrote expanded netlist CSV to: {expanded_netlist_csv_path}")
859 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTERCAP, pex_context.tech)
860 netlist_printer.write(expanded_netlist, expanded_netlist_path)
861 info(f"Wrote expanded netlist to: {expanded_netlist_path}")
863 # FIXME: should this be already reduced?
864 if args.output_spice_path:
865 netlist_printer.write(expanded_netlist, args.output_spice_path)
866 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
868 netlist_reducer = NetlistReducer()
869 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist,
870 top_cell_name=pex_context.annotated_top_cell.name)
871 netlist_printer.write(reduced_netlist, reduced_netlist_path)
872 info(f"Wrote reduced netlist to: {reduced_netlist_path}")
874 self._fastercap_extracted_csv_path = expanded_netlist_csv_path
876 def run_magic_extraction(self,
877 args: argparse.Namespace):
878 if args.input_mode != InputMode.GDS:
879 error(f"MAGIC engine only works with GDS input mode"
880 f" (currently {args.input_mode})")
881 return
883 magic_run_dir = os.path.join(args.output_dir_path, f"magic_{args.magic_pex_mode}")
884 magic_log_path = os.path.join(magic_run_dir,
885 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Output.txt")
886 magic_script_path = os.path.join(magic_run_dir,
887 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Script.tcl")
889 output_netlist_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}.pex.spice")
890 report_db_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}_MAGIC_report.rdb.gz")
892 os.makedirs(magic_run_dir, exist_ok=True)
894 layout = kdb.Layout()
895 layout.read(args.effective_gds_path)
897 if args.magic_pre_flatten:
898 tops = list(layout.top_cells())
899 if not tops:
900 raise Exception("No top cells found in the input layout.")
901 for top in tops:
902 top.flatten(True)
903 flattened_gds_path=os.path.join(magic_run_dir, f"{args.effective_cell_name}.gds")
904 layout.write(flattened_gds_path)
905 gds_path = flattened_gds_path
906 else:
907 gds_path = args.effective_gds_path
909 prepare_magic_script(gds_path=gds_path,
910 cell_name=args.effective_cell_name,
911 run_dir_path=magic_run_dir,
912 script_path=magic_script_path,
913 output_netlist_path=output_netlist_path,
914 pex_mode=args.magic_pex_mode,
915 c_threshold=args.magic_cthresh,
916 r_threshold=args.magic_rthresh,
917 threshold=args.magic_thresh,
918 min_res=args.magic_minres,
919 min_delay=args.magic_mindel,
920 halo=args.magic_halo,
921 short_mode=args.magic_short_mode,
922 merge_mode=args.magic_merge_mode,
923 unique_mode=args.magic_unique_mode)
925 run_magic(exe_path=args.magic_exe_path,
926 magicrc_path=args.magicrc_path,
927 script_path=magic_script_path,
928 log_path=magic_log_path)
930 magic_pex_run = parse_magic_pex_run(Path(magic_run_dir))
932 if args.magic_analyze:
933 report = rdb.ReportDatabase('')
934 magic_log_analyzer = MagicLogAnalyzer(magic_pex_run=magic_pex_run,
935 report=report,
936 dbu=layout.dbu)
937 magic_log_analyzer.analyze()
938 report.save(report_db_path)
940 rule("Paths")
941 if args.magic_analyze:
942 subproc(f"Report DB saved at: {report_db_path}")
943 subproc(f"SPICE netlist saved at: {output_netlist_path}")
945 if os.path.exists(output_netlist_path):
946 if args.output_spice_path and os.path.exists(output_netlist_path):
947 shutil.copy(output_netlist_path, args.output_spice_path)
948 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
950 rule("MAGIC PEX SPICE netlist")
951 with open(output_netlist_path, 'r') as f:
952 subproc(f.read())
953 rule()
955 def run_fastcap_extraction(self,
956 args: argparse.Namespace,
957 pex_context: KLayoutExtractionContext,
958 lst_file: str):
959 rule('FastCap2 Execution')
961 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Output.txt")
962 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Raw.csv")
963 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Avg.csv")
964 expanded_netlist_path = os.path.join(args.output_dir_path,
965 f"{args.effective_cell_name}_FastCap2_Expanded_Netlist.cir")
966 reduced_netlist_path = os.path.join(args.output_dir_path,
967 f"{args.effective_cell_name}_FastCap2_Reduced_Netlist.cir")
969 run_fastcap(exe_path=args.fastcap_exe_path,
970 lst_file_path=lst_file,
971 log_path=log_path)
973 cap_matrix = fastcap_parse_capacitance_matrix(log_path)
974 cap_matrix.write_csv(raw_csv_path)
976 cap_matrix = cap_matrix.averaged_off_diagonals()
977 cap_matrix.write_csv(avg_csv_path)
979 cap_matrix_interpreter = FastCapOutputInterpreter()
981 netlist_expander = NetlistExpander()
982 expanded_netlist = netlist_expander.expand(
983 extracted_netlist=pex_context.lvsdb.netlist(),
984 top_cell_name=pex_context.annotated_top_cell.name,
985 cap_matrix=cap_matrix,
986 cap_matrix_interpreter=cap_matrix_interpreter,
987 blackbox_devices=args.blackbox_devices,
988 device_models=pex_context.tech.device_models,
989 substrate_net_name=pex_context.substrate_net_name
990 )
992 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTCAP2, pex_context.tech)
993 netlist_printer.write(expanded_netlist, expanded_netlist_path)
994 info(f"Wrote expanded netlist to: {expanded_netlist_path}")
996 # FIXME: should this be already reduced?
997 if args.output_spice_path:
998 netlist_printer.write(expanded_netlist, args.output_spice_path)
999 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
1001 netlist_reducer = NetlistReducer()
1002 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist,
1003 top_cell_name=pex_context.annotated_top_cell.name)
1004 netlist_printer.write(reduced_netlist, reduced_netlist_path)
1006 info(f"Wrote reduced netlist to: {reduced_netlist_path}")
1008 def run_kpex_2_5d_engine(self,
1009 args: argparse.Namespace,
1010 pex_context: KLayoutExtractionContext,
1011 tech_info: TechInfo,
1012 report_path: str,
1013 netlist_csv_path: Optional[str],
1014 expanded_netlist_path: Optional[str]):
1015 # TODO: make this separatly configurable
1016 # for now we use 0
1017 args.rcx25d_delaunay_amax = 0
1018 args.rcx25d_delaunay_b = 0.5
1020 extractor = RCX25Extractor(pex_context=pex_context,
1021 pex_mode=args.pex_mode,
1022 delaunay_amax=args.rcx25d_delaunay_amax,
1023 delaunay_b=args.rcx25d_delaunay_b,
1024 scale_ratio_to_fit_halo=args.scale_ratio_to_fit_halo,
1025 tech_info=tech_info,
1026 report_path=report_path)
1027 extraction_results = extractor.extract()
1029 if netlist_csv_path is not None:
1030 # TODO: merge this with klayout_pex/klayout/netlist_csv.py
1032 with open(netlist_csv_path, 'w', encoding='utf-8') as f:
1033 summary = extraction_results.summarize()
1035 f.write('Device;Net1;Net2;Capacitance [fF];Resistance [Ω]\n')
1036 for idx, (key, cap_value) in enumerate(sorted(summary.capacitances.items())):
1037 f.write(f"C{idx + 1};{key.net1};{key.net2};{round(cap_value, 3)};\n")
1038 for idx, (key, res_value) in enumerate(sorted(summary.resistances.items())):
1039 f.write(f"R{idx + 1};{key.net1};{key.net2};;{round(res_value, 3)}\n")
1041 rule('kpex/2.5D extracted netlist (CSV format)')
1042 with open(netlist_csv_path, 'r') as f:
1043 for line in f.readlines():
1044 subproc(line[:-1]) # abusing subproc, simply want verbatim
1046 rule('Extracted netlist CSV')
1047 subproc(f"{netlist_csv_path}")
1049 if expanded_netlist_path is not None:
1050 rule('kpex/2.5D extracted netlist (SPICE format)')
1051 netlist_expander = RCX25NetlistExpander()
1052 expanded_netlist = netlist_expander.expand(
1053 extracted_netlist=pex_context.lvsdb.netlist(),
1054 top_cell_name=pex_context.annotated_top_cell.name,
1055 extraction_results=extraction_results,
1056 blackbox_devices=args.blackbox_devices,
1057 device_models=tech_info.device_models,
1058 substrate_net_name=pex_context.substrate_net_name
1059 )
1061 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.K25D, tech_info)
1062 netlist_printer.write(expanded_netlist, expanded_netlist_path)
1063 subproc(f"Wrote expanded netlist to: {expanded_netlist_path}")
1065 # NOTE: a defect of the RC netlist simulates to a plausible, but wrong number (#215)
1066 self._rcx25_netlist_problems = check_rc_netlist( # NOTE: store for test case
1067 lvs_netlist=pex_context.lvsdb.netlist(),
1068 rc_netlist=expanded_netlist,
1069 top_cell_name=pex_context.annotated_top_cell.name,
1070 summary=extraction_results.summarize(),
1071 blackbox_devices=args.blackbox_devices,
1072 device_models=tech_info.device_models,
1073 substrate_net_name=pex_context.substrate_net_name
1074 )
1075 # NOTE: on by default for RC netlists, as their defects simulate to plausible, but wrong numbers
1076 check_fails_run = args.check_rc_netlist if args.check_rc_netlist is not None \
1077 else args.pex_mode == PEXMode.RC
1078 if self._rcx25_netlist_problems and not check_fails_run:
1079 warning("The extracted netlist is inconsistent with the LVS netlist:\n" +
1080 '\n'.join(f" - {problem}" for problem in self._rcx25_netlist_problems))
1082 # FIXME: should this be already reduced?
1083 if args.output_spice_path:
1084 netlist_printer.write(expanded_netlist, args.output_spice_path)
1085 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
1087 # NOTE: there was a KLayout bug that some of the categories were lost,
1088 # so that the marker browser could not load the report file
1089 try:
1090 report = rdb.ReportDatabase('')
1091 report.load(report_path) # try loading rdb
1092 except Exception as e:
1093 rule("Repair broken marker DB")
1094 warning(f"Detected KLayout bug: RDB can't be loaded due to exception {e}")
1095 repair_rdb(report_path)
1097 if expanded_netlist_path is not None and self._rcx25_netlist_problems and check_fails_run:
1098 raise RCNetlistCheckError("The extracted netlist is inconsistent with the LVS netlist "
1099 "(--check n to accept it):\n" +
1100 '\n'.join(f" - {problem}" for problem in self._rcx25_netlist_problems))
1102 return extraction_results
1104 def setup_logging(self, args: argparse.Namespace):
1105 def register_log_file_handler(log_path: str,
1106 formatter: Optional[logging.Formatter]) -> logging.Handler:
1107 handler = logging.FileHandler(log_path)
1108 handler.setLevel(LogLevel.SUBPROCESS)
1109 if formatter:
1110 handler.setFormatter(formatter)
1111 register_additional_handler(handler)
1112 return handler
1114 def reregister_log_file_handler(handler: logging.Handler,
1115 log_path: str,
1116 formatter: Optional[logging.Formatter]):
1117 deregister_additional_handler(handler)
1118 handler.flush()
1119 handler.close()
1120 os.makedirs(args.output_dir_path, exist_ok=True)
1121 new_path = os.path.join(args.output_dir_path, os.path.basename(log_path))
1122 if os.path.exists(new_path):
1123 ctime = os.path.getctime(new_path)
1124 dt = datetime.fromtimestamp(ctime)
1125 timestamp = dt.strftime('%Y-%m-%d_%H-%M-%S')
1126 backup_path = f"{new_path[:-4]}_{timestamp}.bak.log"
1127 shutil.move(new_path, backup_path)
1128 log_path = shutil.move(log_path, new_path)
1129 register_log_file_handler(log_path, formatter)
1131 # setup preliminary logger
1132 cli_log_path_plain = os.path.join(args.output_dir_base_path, f"kpex_plain.log")
1133 cli_log_path_formatted = os.path.join(args.output_dir_base_path, f"kpex.log")
1134 formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
1135 file_handler_plain = register_log_file_handler(cli_log_path_plain, None)
1136 file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter)
1137 try:
1138 self.validate_args(args)
1139 except ArgumentValidationError:
1140 if hasattr(args, 'output_dir_path'):
1141 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None)
1142 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter)
1143 sys.exit(1)
1144 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None)
1145 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter)
1147 set_log_level(args.log_level)
1149 @staticmethod
1150 def lvs_input_fingerprint(args: argparse.Namespace) -> LVSInputFingerprint:
1151 klayout_version = Tool.KLAYOUT.detect_version(args.klayout_exe_path)
1152 return lvs_input_fingerprint(pdk=str(args.pdk),
1153 klayout_version='' if klayout_version is None else str(klayout_version),
1154 lvs_script_path=args.lvs_script_path,
1155 script_parameters=LVSRunner.result_script_parameters(),
1156 gds_path=args.gds_path,
1157 cell_name=args.effective_cell_name,
1158 schematic_path=args.effective_schematic_path)
1160 def create_lvsdb(self, args: argparse.Namespace) -> kdb.LayoutVsSchematic:
1161 lvsdb = kdb.LayoutVsSchematic()
1163 match args.input_mode:
1164 case InputMode.LVSDB:
1165 lvsdb.read(args.lvsdb_path)
1166 case InputMode.GDS:
1167 lvs_log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_lvs.log")
1168 lvsdb_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}.lvsdb.gz")
1169 lvs_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_extracted.cir")
1170 lvs_cache_entry = LVSCacheEntry(
1171 dir_path=os.path.join(args.cache_dir_path, args.pdk,
1172 os.path.splitroot(os.path.abspath(args.gds_path))[-1]),
1173 cell_name=args.effective_cell_name
1174 )
1176 lvs_needed = True
1178 if args.cache_lvs:
1179 lvs_fingerprint = self.lvs_input_fingerprint(args)
1180 cache_miss_reasons = lvs_cache_entry.cache_miss_reasons(lvs_fingerprint)
1181 if cache_miss_reasons:
1182 info(f"Cache miss: {'; '.join(cache_miss_reasons)}")
1183 subproc(lvs_cache_entry.lvsdb_path)
1184 else:
1185 warning(f"Cache hit: Reusing cached LVSDB")
1186 subproc(lvs_cache_entry.lvsdb_path)
1187 shutil.copy(lvs_cache_entry.lvsdb_path, lvsdb_path)
1188 lvs_needed = False
1190 if lvs_needed:
1191 lvs_runner = LVSRunner()
1192 try:
1193 lvs_runner.run_klayout_lvs(exe_path=args.klayout_exe_path,
1194 lvs_script=args.lvs_script_path,
1195 gds_path=args.effective_gds_path,
1196 schematic_path=args.effective_schematic_path,
1197 log_path=lvs_log_path,
1198 lvsdb_path=lvsdb_path,
1199 netlist_path=lvs_netlist_path,
1200 verbose=args.klayout_lvs_verbose)
1201 except LVSError as e:
1202 error(str(e))
1203 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1204 if args.cache_lvs:
1205 lvs_cache_entry.store(lvsdb_path=lvsdb_path, fingerprint=lvs_fingerprint)
1207 lvsdb.read(lvsdb_path)
1208 return lvsdb
1210 SUBCOMMANDS = ('extract', 'pex25d')
1211 LEGACY_SUBCOMMAND = 'extract'
1213 @classmethod
1214 def normalize_argv(cls, argv: List[str]) -> List[str]:
1215 """
1216 Accept the pre-subcommand, flat command line.
1218 Before the verb layer, kpex was invoked as
1219 ``kpex --pdk sky130A --gds foo.gds --2.5D``. Every such run still works
1220 and means ``kpex extract <same options>``; it just warns. The two forms
1221 cannot be confused, because a subcommand name never begins with '-' and
1222 every legacy option does — so the fallback needs no heuristics and can
1223 never mistake one for the other.
1225 This exists for the scripts, CI jobs and docs that already say
1226 ``kpex --gds ...``, including the test suite. It is meant to be removed
1227 once those have moved on.
1228 """
1229 tail = argv[1:]
1230 if not tail:
1231 return argv
1232 first = tail[0]
1233 if first in ('-h', '--help', '-v', '--version'):
1234 return argv
1235 if not first.startswith('-'):
1236 return argv # a subcommand, or a typo argparse will report better than we can
1237 if any(arg in cls.SUBCOMMANDS for arg in tail):
1238 # A subcommand is present, just not first — most likely a top-level
1239 # option that now belongs to the subcommand. Leave it alone and let
1240 # argparse say so, rather than silently rewriting it into something
1241 # that fails somewhere else.
1242 return argv
1243 warning(f"Calling {PROGRAM_NAME} without a subcommand is deprecated; "
1244 f"treating this run as '{PROGRAM_NAME} {cls.LEGACY_SUBCOMMAND} …'. "
1245 f"See '{PROGRAM_NAME} --help' for the available subcommands.")
1246 return [argv[0], cls.LEGACY_SUBCOMMAND] + tail
1248 @staticmethod
1249 def _writes_artifact_to_stdout(argv: List[str]) -> bool:
1250 """
1251 Whether this invocation sends a PEX25D artifact to stdout.
1253 Answered from the raw argv rather than from parsed arguments, because
1254 the redirect has to be in place before anything — argument validation
1255 included — has a chance to print.
1256 """
1257 return any(previous in ('--output_file', '--output_scene') and current == '-'
1258 for previous, current in zip(argv, argv[1:]))
1260 def main(self, argv: List[str]):
1261 env = Env.from_os_environ()
1262 argv = self.normalize_argv(argv)
1264 # A PEX25D artifact may be destined for stdout, and in that case stdout
1265 # belongs to the artifact alone — one stray log line and whatever is on
1266 # the other end of the pipe is parsing garbage. Redirecting sys.stdout to
1267 # stderr for the whole run is blunt but total: the rich console resolves
1268 # sys.stdout at write time, so every rule()/info()/subproc() follows,
1269 # including ones written by code that never considered being in a
1270 # pipeline. Artifact I/O uses sys.__stdout__ and is unaffected.
1271 writes_to_stdout = self._writes_artifact_to_stdout(argv)
1273 with contextlib.ExitStack() as stack:
1274 if writes_to_stdout:
1275 stack.enter_context(contextlib.redirect_stdout(sys.stderr))
1277 if not ({'-v', '--version', '-h', '--help'} & set(argv)):
1278 rule('Command line arguments')
1279 subproc(' '.join(map(shlex.quote, argv)))
1281 args = self.parse_args(arg_list=argv[1:], env=env)
1283 os.makedirs(args.output_dir_base_path, exist_ok=True)
1284 self.setup_logging(args)
1286 try:
1287 tech_info = TechInfo.from_json(args.tech_pbjson_path,
1288 dielectric_filter=args.dielectric_filter)
1289 except TechDefError as e:
1290 error(str(e))
1291 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1293 match args.command:
1294 case 'pex25d':
1295 self.run_pex25d_generation(args=args, tech_info=tech_info)
1296 case _:
1297 try:
1298 self.run_extraction(args=args, tech_info=tech_info)
1299 except (LVSDBError, RExtractionTechError, CExtractionTechError, DeviceModelError,
1300 RCNetlistCheckError) as e:
1301 error(str(e))
1302 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1304 def run_pex25d_generation(self,
1305 args: argparse.Namespace,
1306 tech_info: TechInfo):
1307 """
1308 The dump path: LVS for connectivity, then PEX25D out, and stop.
1310 Deliberately does not touch any engine. Everything downstream of the
1311 artifacts written here — validating them, re-encoding them, exporting
1312 them to a solver's native input — is the standalone ``pex25d`` tool's
1313 job, and needs neither a layout nor KLayout to do it.
1314 """
1315 from .klayout.pex25d_builder import BuildError, BuilderOptions, build_pex25d_file
1316 from .pex25d.codec import save_artifact
1317 from .pex25d.resolver import ResolveError, resolve
1318 from .pex25d.validator import validate
1320 report = DiagnosticsReport(warnings_are_errors=args.warnings_are_errors)
1322 rule('Prepare LVSDB')
1323 lvsdb = self.create_lvsdb(args)
1325 pex_context = KLayoutExtractionContext.prepare_extraction(
1326 top_cell=args.effective_cell_name,
1327 lvsdb=lvsdb,
1328 tech=tech_info,
1329 blackbox_devices=args.blackbox_devices)
1331 rule('Non-empty layers in LVS database')
1332 for gds_pair, layer_info in pex_context.extracted_layers.items():
1333 names = [l.lvs_layer_name for l in layer_info.source_layers]
1334 info(f"{gds_pair} -> ({' '.join(names)})")
1336 rule('PEX25D Generation')
1337 try:
1338 pex25d_file = build_pex25d_file(
1339 pex_context=pex_context,
1340 tech_info=tech_info,
1341 cell_name=args.effective_cell_name,
1342 options=BuilderOptions(
1343 grid_um=args.pex25d_grid,
1344 with_source_refs=args.pex25d_with_source_refs,
1345 dielectric_filter=args.dielectric_filter,
1346 domain_margin_um=args.pex25d_domain_margin))
1348 if args.pex25d_file_spec is not None:
1349 save_artifact(pex25d_file, args.pex25d_file_spec,
1350 comments=args.pex25d_comments)
1351 info(f"Wrote {args.pex25d_file_spec}")
1353 scene = None
1354 if args.pex25d_scene_spec is not None:
1355 # The geometric tier belongs to whichever of the two runs it
1356 # once: the validator when it is on, the resolver otherwise.
1357 scene = resolve(pex25d_file, report=report,
1358 strict=args.strict and not args.pex25d_validate)
1359 save_artifact(scene, args.pex25d_scene_spec,
1360 comments=args.pex25d_comments)
1361 info(f"Wrote {args.pex25d_scene_spec}")
1363 if args.pex25d_validate:
1364 validate(pex25d_file, report=report, strict=args.strict,
1365 scene=scene)
1367 except BuildError as e:
1368 error(f"Could not generate PEX25D: {e}")
1369 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1370 except ResolveError as e:
1371 # The scene is not written: the diagnostics say why, and half a
1372 # scene is worse than none.
1373 error(str(e))
1374 report.render(args.diagnostics_format)
1375 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1376 except NotImplementedError as e:
1377 error(f"Not implemented yet: {e}")
1378 sys.exit(ExitCode.NOT_IMPLEMENTED)
1380 writes_to_stdout = any(spec is not None and spec.is_stdio
1381 for spec in (args.pex25d_file_spec, args.pex25d_scene_spec))
1382 with diagnostics_stream(writes_to_stdout=writes_to_stdout) as stream:
1383 report.render(args.diagnostics_format, stream=stream)
1385 if report.exit_code != ExitCode.OK:
1386 sys.exit(report.exit_code)
1388 def run_extraction(self,
1389 args: argparse.Namespace,
1390 tech_info: TechInfo):
1391 if args.halo is not None:
1392 tech_info.tech.process_parasitics.side_halo = args.halo
1394 if args.run_magic:
1395 rule('MAGIC')
1396 self.run_magic_extraction(args)
1398 # no need to run LVS etc if only running magic engine
1399 if not (args.run_fastcap or args.run_fastercap or args.run_2_5D):
1400 return
1402 rule('Prepare LVSDB')
1403 lvsdb = self.create_lvsdb(args)
1405 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=args.effective_cell_name,
1406 lvsdb=lvsdb,
1407 tech=tech_info,
1408 blackbox_devices=args.blackbox_devices)
1409 rule('Non-empty layers in LVS database')
1410 for gds_pair, layer_info in pex_context.extracted_layers.items():
1411 names = [l.lvs_layer_name for l in layer_info.source_layers]
1412 info(f"{gds_pair} -> ({' '.join(names)})")
1414 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_extracted.oas")
1415 pex_context.annotated_layout.write(gds_path)
1417 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_internal.oas")
1418 pex_context.lvsdb.internal_layout().write(gds_path)
1420 def dump_layers(cell: str,
1421 layers: List[KLayoutExtractedLayerInfo],
1422 layout_dump_path: str):
1423 layout = kdb.Layout()
1424 layout.dbu = lvsdb.internal_layout().dbu
1426 top_cell = layout.create_cell(cell)
1427 for ulyr in layers:
1428 li = kdb.LayerInfo(*ulyr.gds_pair)
1429 li.name = ulyr.lvs_layer_name
1430 layer = layout.insert_layer(li)
1431 layout.insert(top_cell.cell_index(), layer, ulyr.region.dup())
1433 layout.write(layout_dump_path)
1435 if len(pex_context.unnamed_layers) >= 1:
1436 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_unnamed_LVS_layers.gds.gz")
1437 dump_layers(cell=args.effective_cell_name,
1438 layers=pex_context.unnamed_layers,
1439 layout_dump_path=layout_dump_path)
1441 if len(pex_context.extracted_layers) >= 1:
1442 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_nonempty_LVS_layers.gds.gz")
1443 nonempty_layers = [l \
1444 for layers in pex_context.extracted_layers.values() \
1445 for l in layers.source_layers]
1446 dump_layers(cell=args.effective_cell_name,
1447 layers=nonempty_layers,
1448 layout_dump_path=layout_dump_path)
1449 else:
1450 error("No extracted layers found")
1451 sys.exit(1)
1453 if args.run_fastcap or args.run_fastercap:
1454 lst_file = self.build_fastercap_input(args=args,
1455 pex_context=pex_context,
1456 tech_info=tech_info)
1457 if args.run_fastercap:
1458 self.run_fastercap_extraction(args=args,
1459 pex_context=pex_context,
1460 lst_file=lst_file)
1461 if args.run_fastcap:
1462 self.run_fastcap_extraction(args=args,
1463 pex_context=pex_context,
1464 lst_file=lst_file)
1466 if args.run_2_5D:
1467 rule("kpex/2.5D PEX Engine")
1468 report_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_k25d_pex_report.rdb.gz")
1469 netlist_csv_path = os.path.abspath(os.path.join(args.output_dir_path,
1470 f"{args.effective_cell_name}_k25d_pex_netlist.csv"))
1471 netlist_spice_path = os.path.abspath(os.path.join(args.output_dir_path,
1472 f"{args.effective_cell_name}_k25d_pex_netlist.spice"))
1474 self._rcx25_extraction_results = self.run_kpex_2_5d_engine( # NOTE: store for test case
1475 args=args,
1476 pex_context=pex_context,
1477 tech_info=tech_info,
1478 report_path=report_path,
1479 netlist_csv_path=netlist_csv_path,
1480 expanded_netlist_path=netlist_spice_path
1481 )
1483 self._rcx25_extracted_csv_path = netlist_csv_path
1485 @property
1486 def rcx25_extraction_results(self) -> ExtractionResults:
1487 if not hasattr(self, '_rcx25_extraction_results'):
1488 raise Exception('rcx25_extraction_results is not initialized, was run_kpex_2_5d_engine called?')
1489 return self._rcx25_extraction_results
1491 @property
1492 def rcx25_extracted_csv_path(self) -> str:
1493 if not hasattr(self, '_rcx25_extracted_csv_path'):
1494 raise Exception('rcx25_extracted_csv_path is not initialized, was run_kpex_2_5d_engine called?')
1495 return self._rcx25_extracted_csv_path
1497 @property
1498 def rcx25_netlist_problems(self) -> List[str]:
1499 if not hasattr(self, '_rcx25_netlist_problems'):
1500 raise Exception('rcx25_netlist_problems is not initialized, was run_kpex_2_5d_engine called?')
1501 return self._rcx25_netlist_problems
1503 @property
1504 def fastercap_extracted_csv_path(self) -> str:
1505 if not hasattr(self, '_fastercap_extracted_csv_path'):
1506 raise Exception('fastercap_extracted_csv_path is not initialized, was run_fastercap_extraction called?')
1507 return self._fastercap_extracted_csv_path
1510if __name__ == "__main__":
1511 cli = KpexCLI()
1512 cli.main(sys.argv)