Coverage for klayout_pex/kpex_cli.py: 83%
950 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
1#! /usr/bin/env python3
2#
3# --------------------------------------------------------------------------------
4# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
5# Johannes Kepler University, Institute for Integrated Circuits.
6#
7# This file is part of KPEX
8# (see https://github.com/iic-jku/klayout-pex).
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <http://www.gnu.org/licenses/>.
22# SPDX-License-Identifier: GPL-3.0-or-later
23# --------------------------------------------------------------------------------
24#
26import argparse
27import contextlib
28from datetime import datetime
29from enum import StrEnum
30import functools
31import logging
32import os
33import os.path
34from pathlib import Path
35import google.protobuf.json_format
36import rich.console
37import rich.markdown
38import rich.text
39from rich_argparse import RichHelpFormatter
40import shlex
41import shutil
42import sys
43from typing import *
45import klayout.db as kdb
46import klayout.rdb as rdb
48from .klayout import capacitance_matrix_interpreter
49from .common.path_validation import validate_files, FileValidationResult
50from .env import EnvVar, Env
51from .extraction_engine import ExtractionEngine
52from .fastercap.fastercap_input_builder import FasterCapInputBuilder
53from .fastercap.fastercap_model_generator import FasterCapModelGenerator
54from .fastercap.fastercap_error import FasterCapError
55from .fastercap.fastercap_runner import run_fastercap, fastercap_parse_capacitance_matrix
56from .fastercap.output_interpreter import FasterCapOutputInterpreter
57from .fastcap.fastcap_runner import run_fastcap, fastcap_parse_capacitance_matrix
58from .fastcap.output_interpreter import FastCapOutputInterpreter
59from .klayout.lvs_cache import LVSCacheEntry, LVSInputFingerprint, lvs_input_fingerprint
60from .klayout.lvs_runner import LVSError, LVSRunner
61from .klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo, LVSDBError
62from .klayout.netlist_expander import NetlistExpander
63from .klayout.netlist_csv import NetlistCSVWriter
64from .klayout.netlist_printer import NetlistHeader, NetlistPrinter
65from .klayout.netlist_reducer import NetlistReducer
66from .klayout.output_files import write_output_file
67from .klayout.repair_rdb import repair_rdb
68from .log import (
69 LogLevel,
70 set_log_level,
71 register_additional_handler,
72 deregister_additional_handler,
73 # console,
74 # debug,
75 info,
76 warning,
77 subproc,
78 error,
79 rule
80)
81from .magic.magic_ext_file_parser import parse_magic_pex_run
82from .magic.magic_runner import (
83 MagicPEXMode,
84 MagicShortMode,
85 MagicMergeMode,
86 MagicUniqueMode,
87 run_magic,
88 prepare_magic_script,
89)
90from .magic.magic_log_analyzer import MagicLogAnalyzer
91from .netlist.netlist_convert import convert_to_vacask
92from .netlist.netlist_format import NetlistFormat
93from .netlist.spice_hierarchy import SpiceEvaluationError
94from .netlist.spice_reader import SpiceReader, SpiceReaderError
95from .netlist.vacask_writer import VacaskWriterError
96from .pex25d.artifact import (
97 CANONICAL_SUFFIX,
98 ArtifactFormat,
99 ArtifactKind,
100 ArtifactNamingError,
101 infer_artifact_spec,
102 named_kind_and_format,
103)
104from .klayout.pex25d_builder import DEFAULT_GRID_UM
105from .pex25d.diagnostics import DiagnosticsReport, ExitCode, diagnostics_stream
106from .pex25d.pex25d_cli import Pex25DCLI
107from .netlist.netlist_reduction import ReductionMode, reduce_spice_netlist, reduced_netlist_path
108from .netlist.rc_reduction import ReductionOptions
109from .pdk_config import PDK, PDKConfig
110from .rcx25.exclusion import (
111 BlockKind,
112 CutLayers,
113 ExclusionError,
114 ExclusionMarkerMode,
115 Exclusions,
116 cells_called_by,
117 check_instances_of_cut_cells,
118 matches_name,
119 parsed_cut_requests,
120 selected_marker_layers,
121)
122from .rcx25.extractor import (
123 CExtractionTechError,
124 ExtractionResults,
125 RCX25Extractor,
126 prepare_exclusions,
127 warn_of_ports_without_nodes,
128)
129from .rcx25.netlist_checks import (
130 check_rc_netlist,
131 check_schematic_device_classes,
132 check_schematic_device_totals,
133 first_names,
134 RCNetlistCheckError,
135)
136from .rcx25.netlist_expander import RCX25NetlistExpander
137from .rcx25.pex_mode import PEXMode
138from .rcx25.r.r_extractor import RExtractionTechError
139from .schematic_devices import schematic_devices
140from .device_models import DeviceModelError, DeviceModels
141from .tech_info import TechDefError, TechInfo
142from .types import NetName
143from .tool_version_constraints import (
144 Tool,
145 VersionCheckMode,
146 check_tool_versions
147)
148from .util.multiple_choice import MultipleChoicePattern
149from .util.peak_memory import log_peak_memory
150from .util.argparse_helpers import (
151 SectionedHelpFormatter,
152 add_argument_with_former_name,
153 add_help_subcommand,
154 add_log_level_argument,
155 add_subcommand_sections,
156 handle_help_subcommand,
157 positive_spice_seconds,
158 render_enum_help,
159 true_or_false,
160 warn_deprecated_options,
161)
162from .version import __version__
165# ------------------------------------------------------------------------------------
167PROGRAM_NAME = "kpex"
170class ArgumentValidationError(Exception):
171 pass
174class InputMode(StrEnum):
175 LVSDB = "lvsdb"
176 GDS = "gds"
179@functools.cache
180def side_halo_by_pdk() -> Dict[PDK, float]:
181 """
182 :return: the side halo (in µm) of the tech info of each PDK, but of those without a readable one,
183 which fail their own runs (see validate_args), rather than the help of all
184 """
185 side_halos: Dict[PDK, float] = {}
186 for pdk in PDK:
187 try:
188 side_halos[pdk] = TechInfo.parse_tech_def(pdk.config.tech_pb_json_path).process_parasitics.side_halo
189 except (OSError, TechDefError, google.protobuf.json_format.ParseError):
190 continue
191 return side_halos
194def halo_default_help(halo_by_pdk: Mapping[str, float]) -> str:
195 """
196 :param halo_by_pdk: the side halo (in µm) of each PDK (see side_halo_by_pdk)
197 :return: the default of --halo for its help, the side halo of the tech info,
198 e.g. "the tech info's, 8 µm in all PDKs" or "the tech info's, 8 µm in gf180mcuD, 10 µm in sky130A"
199 """
200 halos = [f"{halo:g} µm in {pdk}" for pdk, halo in sorted(halo_by_pdk.items())]
201 if len(set(halo_by_pdk.values())) == 1:
202 halos = [f"{next(iter(halo_by_pdk.values())):g} µm in all PDKs"]
203 return ', '.join(["the tech info's"] + halos)
206class KpexCLI:
207 # ---------------------------------------------------------- argument groups
208 #
209 # Each group is attached to a *given* parser rather than to one flat parser,
210 # so that a subcommand declares exactly the options it can act on. That is
211 # the point of the verb layer: `kpex pex25d` cannot be handed --magic-halo,
212 # and the validation below no longer has to hand-check which combinations
213 # of flags make sense together.
215 @staticmethod
216 def _add_special_options(parser: argparse.ArgumentParser,
217 include_threads: bool = False) -> None:
218 group_special = parser.add_argument_group("Special Options")
219 group_special.add_argument("--help", "-h", action='help',
220 help="show this help message and exit")
221 add_log_level_argument(group_special)
222 add_argument_with_former_name(group_special, "--tool-version-checks", "--tool_version_checks",
223 dest='version_check_mode',
224 default=VersionCheckMode.DEFAULT, type=VersionCheckMode,
225 choices=list(VersionCheckMode),
226 help=render_enum_help(topic='tool_version_checks',
227 enum_cls=VersionCheckMode))
228 if include_threads:
229 group_special.add_argument("--threads", dest='num_threads', type=int,
230 default=os.cpu_count() * 4,
231 help="number of threads (e.g. for FasterCap) "
232 "(default is %(default)s)")
234 @staticmethod
235 def _add_pex_setup_arguments(parser: argparse.ArgumentParser,
236 env: Env,
237 include_spice_output: bool = True) -> None:
238 group_pex = parser.add_argument_group("Parasitic Extraction Setup")
240 all_pdk_choices = list(PDK) + list(PDK.legacy_aliases().keys())
242 default_pdk = env.default_pdk
243 pdk_help = render_enum_help(topic='pdk', enum_cls=PDK, print_default=False)
244 if default_pdk:
245 pdk_help += f" (default is '{default_pdk}')"
247 group_pex.add_argument("--pdk", dest="pdk", required=default_pdk is None,
248 type=PDK.from_string, choices=all_pdk_choices,
249 help=pdk_help, default=default_pdk)
251 add_argument_with_former_name(group_pex, "--out-dir", "--out_dir",
252 dest="output_dir_base_path", default="output",
253 help="Run directory path (default is '%(default)s')")
255 if include_spice_output:
256 add_argument_with_former_name(group_pex, "--out-netlist", "--out_netlist", "-o",
257 dest="output_netlist_path", default=None,
258 help="Optional additional output path of the netlist, "
259 "in the --netlist-format (default is none)")
260 # NOTE: deprecated, the name of --out-netlist before --netlist-format
261 group_pex.add_argument("--out_spice", dest="output_spice_path", default=None, help=argparse.SUPPRESS)
262 add_argument_with_former_name(group_pex, "--netlist-format", "--netlist_format", dest="netlist_format",
263 default=NetlistFormat.SPICE.value, choices=[f.value for f in NetlistFormat],
264 help="Format of the netlist the 2.5D engine writes to --out-netlist: "
265 "'spice' or 'vacask' (for the VACASK simulator, converted from the "
266 "SPICE netlist, as 'netlist convert' does, both are written into "
267 "the run directory) (default is %(default)s)")
270 @staticmethod
271 def _add_pex_input_arguments(parser: argparse.ArgumentParser) -> None:
272 group_pex_input = parser.add_argument_group("Parasitic Extraction Input",
273 description="Either LVS is run, or an existing LVSDB is used")
274 group_pex_input.add_argument("--gds", "-g", dest="gds_path", default=None,
275 help="GDS path (for LVS)")
276 group_pex_input.add_argument("--schematic", "-s", dest="schematic_path",
277 help="Schematic SPICE netlist path (--check checks its device classes "
278 "and their totals only, use regular LVS to compare layout against schematic)")
279 # Developer shortcut, not a supported entry point. It exists so that a
280 # PEX25D or engine run can be repeated without paying for LVS again while
281 # working on the code downstream of it. An LVSDB is tied to the kpex
282 # version and PDK that produced it and is not an interchange format —
283 # PEX25D is. Anyone who wants to hand a scene to somebody else, or keep
284 # one, should be writing PEX25D, not passing LVSDBs around.
285 group_pex_input.add_argument("--lvsdb", "-l", dest="lvsdb_path", default=None,
286 help="KLayout PEX-LVSDB path from a previous run "
287 "(bypass PEX-LVS, also with --gds, which then gives what the LVSDB "
288 "lacks, e.g. the shapes of --exclusion-markers). Developer shortcut for "
289 "re-running without repeating LVS; not an "
290 "interchange format — use PEX25D for that")
291 group_pex_input.add_argument("--cell", "-c", dest="cell_name", default=None,
292 help="Cell (default is the top cell)")
294 group_pex_input.add_argument("--cache-lvs", dest="cache_lvs",
295 type=true_or_false, default=True,
296 help="Reuse the cached LVSDB while the LVS inputs are unchanged "
297 "(layout, LVS deck, KLayout version) (default is %(default)s)")
298 group_pex_input.add_argument("--cache-dir", dest="cache_dir_path", default=None,
299 help="Path for cached LVSDB (default is .kpex_cache within --out-dir)")
300 group_pex_input.add_argument("--lvs-verbose", dest="klayout_lvs_verbose",
301 type=true_or_false, default=False,
302 help="Verbose KLayout LVS output (default is %(default)s)")
305 @staticmethod
306 def _add_engine_arguments(parser: argparse.ArgumentParser) -> None:
307 group_pex_options = parser.add_argument_group("Extraction Engines")
308 group_pex_options.add_argument("--fastercap", dest="run_fastercap",
309 action='store_true', default=False,
310 help="Run FasterCap engine (default is %(default)s)")
311 group_pex_options.add_argument("--fastcap", dest="run_fastcap",
312 action='store_true', default=False,
313 help="Run FastCap2 engine (default is %(default)s)")
314 group_pex_options.add_argument("--magic", dest="run_magic",
315 action='store_true', default=False,
316 help="Run MAGIC engine (default is %(default)s)")
317 group_pex_options.add_argument("--2.5D", dest="run_2_5D",
318 action='store_true', default=False,
319 help="Run 2.5D analytical engine (default is %(default)s)")
322 @staticmethod
323 def _add_extraction_options(parser: argparse.ArgumentParser) -> None:
324 group_pex_options = parser.add_argument_group("Parasitic Extraction Options")
325 group_pex_options.add_argument("--blackbox", dest="blackbox_devices",
326 type=true_or_false, default=False, # TODO: in the future this should be True by default
327 help="Blackbox devices like MIM/MOM caps, as they are handled by SPICE models "
328 "(default is %(default)s for testing now)")
330 @staticmethod
331 def _add_tech_arguments(parser: argparse.ArgumentParser) -> None:
332 group_tech = parser.add_argument_group("Technology Options")
333 group_tech.add_argument("--diel", dest="dielectric_filter",
334 type=str, default="all",
335 help=f"Comma separated list of dielectric filter patterns. "
336 f"Allowed patterns are: (none, all, -dielname1, +dielname2) "
337 f"(default is %(default)s)")
339 @staticmethod
340 def _add_fastercap_arguments(parser: argparse.ArgumentParser) -> None:
341 group_fastercap = parser.add_argument_group("FasterCap Options")
342 add_argument_with_former_name(group_fastercap, "--k-void", "--k_void", "-k", dest="k_void",
343 type=float, default=3.9,
344 help="Dielectric constant of void (default is %(default)s)")
346 # TODO: reflect that these are also now used by KPEX/2.5D engine!
347 add_argument_with_former_name(group_fastercap, "--delaunay-amax", "--delaunay_amax", "-a", dest="delaunay_amax",
348 type=float, default=50,
349 help="Delaunay triangulation maximum area (default is %(default)s)")
350 add_argument_with_former_name(group_fastercap, "--delaunay-b", "--delaunay_b", "-b", dest="delaunay_b",
351 type=float, default=0.5,
352 help="Delaunay triangulation b (default is %(default)s)")
353 add_argument_with_former_name(group_fastercap, "--geo-check", "--geo_check", dest="geometry_check",
354 type=true_or_false, default=False,
355 help=f"Validate geometries before passing to FasterCap "
356 f"(default is False)")
357 group_fastercap.add_argument("--tolerance", dest="fastercap_tolerance",
358 type=float, default=0.05,
359 help="FasterCap -aX error tolerance (default is %(default)s)")
360 add_argument_with_former_name(group_fastercap, "--d-coeff", "--d_coeff", dest="fastercap_d_coeff",
361 type=float, default=0.5,
362 help=f"FasterCap -d direct potential interaction coefficient to mesh refinement "
363 f"(default is %(default)s)")
364 group_fastercap.add_argument("--mesh", dest="fastercap_mesh_refinement_value",
365 type=float, default=0.5,
366 help="FasterCap -m Mesh relative refinement value (default is %(default)s)")
367 group_fastercap.add_argument("--ooc", dest="fastercap_ooc_condition",
368 type=float, default=2,
369 help="FasterCap -f out-of-core free memory to link memory condition "
370 "(0 = don't go OOC, default is %(default)s)")
371 add_argument_with_former_name(group_fastercap, "--auto-precond", "--auto_precond",
372 dest="fastercap_auto_preconditioner",
373 type=true_or_false, default=True,
374 help=f"FasterCap -ap Automatic preconditioner usage (default is %(default)s)")
375 group_fastercap.add_argument("--galerkin", dest="fastercap_galerkin_scheme",
376 action='store_true', default=False,
377 help=f"FasterCap -g Use Galerkin scheme (default is %(default)s)")
378 group_fastercap.add_argument("--jacobi", dest="fastercap_jacobi_preconditioner",
379 action='store_true', default=False,
380 help="FasterCap -pj Use Jacobi preconditioner (default is %(default)s)")
383 @staticmethod
384 def _add_magic_arguments(parser: argparse.ArgumentParser, env: Env) -> None:
385 group_magic = parser.add_argument_group("MAGIC Options")
387 default_magicrc_path = env.default_magicrc_path
388 if default_magicrc_path:
389 magicrc_help = f"Path to magicrc configuration file (default is '{default_magicrc_path}')"
390 else:
391 magicrc_help = "Path to magicrc configuration file "\
392 "(default not available, PDK and PDK_ROOT must be set!)"
394 group_magic.add_argument('--magicrc', dest='magicrc_path', default=default_magicrc_path,
395 help=magicrc_help)
396 add_argument_with_former_name(group_magic, "--magic-mode", "--magic_mode", dest='magic_pex_mode',
397 default=MagicPEXMode.DEFAULT, type=MagicPEXMode, choices=list(MagicPEXMode),
398 help=render_enum_help(topic='magic_mode', enum_cls=MagicPEXMode))
399 add_argument_with_former_name(group_magic, "--magic-cthresh", "--magic_cthresh", dest="magic_cthresh",
400 type=float, default=0.01,
401 help="Threshold (in fF) for ignored parasitic capacitances "
402 "(default is %(default)s). (MAGIC command: ext2spice cthresh <value>)")
403 add_argument_with_former_name(group_magic, "--magic-rthresh", "--magic_rthresh", dest="magic_rthresh",
404 type=float, default=100,
405 help="Threshold (in Ω) for ignored parasitic resistances "
406 "(default is %(default)s). (MAGIC command: ext2spice rthresh <value>)")
407 add_argument_with_former_name(group_magic, "--magic-thresh", "--magic_thresh", dest="magic_thresh",
408 type=float, default=None,
409 help="Lumped resistance threshold (in mΩ) to trigger network extraction. "
410 "If not supplied, MAGIC's default is implicitly used. "
411 "(MAGIC command: extresist threshold <value>)")
412 add_argument_with_former_name(group_magic, "--magic-minres", "--magic_minres", dest="magic_minres",
413 type=float, default=None,
414 help="Threshold (in mΩ) for removing individual resistors during network simplification. "
415 "If not supplied, MAGIC's default is implicitly used. "
416 "(MAGIC command: extresist minres <value>)")
417 add_argument_with_former_name(group_magic, "--magic-mindel", "--magic_mindel", dest="magic_mindel",
418 type=float, default=None,
419 help="Minimum delay threshold (in ps) to trigger network extraction. "
420 "If not supplied, MAGIC's default is implicitly used. "
421 "(MAGIC command: extresist mindelay <value>)")
422 add_argument_with_former_name(group_magic, "--magic-halo", "--magic_halo", dest="magic_halo",
423 type=float, default=None,
424 help="Custom sidewall halo distance (in µm) "
425 "(MAGIC command: extract halo <value>) (default is no custom halo)")
426 add_argument_with_former_name(group_magic, "--magic-short", "--magic_short", dest='magic_short_mode',
427 default=MagicShortMode.DEFAULT, type=MagicShortMode, choices=list(MagicShortMode),
428 help=render_enum_help(topic='magic_short', enum_cls=MagicShortMode))
429 add_argument_with_former_name(group_magic, "--magic-merge", "--magic_merge", dest='magic_merge_mode',
430 default=MagicMergeMode.DEFAULT, type=MagicMergeMode, choices=list(MagicMergeMode),
431 help=render_enum_help(topic='magic_merge', enum_cls=MagicMergeMode))
432 add_argument_with_former_name(group_magic, "--magic-pre-flatten", "--magic_pre_flatten",
433 dest='magic_pre_flatten',
434 action='store_true', default=False,
435 help="Flatten GDS before reading it with Magic (default is %(default)s).")
436 add_argument_with_former_name(group_magic, "--magic-unique", "--magic_unique", dest='magic_unique_mode',
437 default=MagicUniqueMode.DEFAULT, type=MagicUniqueMode,
438 choices=list(MagicUniqueMode),
439 help="Control net uniqueness during extraction "
440 " ('implicit' omits the command, uses MAGIC's default value). "
441 + render_enum_help(topic='magic_unique', enum_cls=MagicUniqueMode))
442 add_argument_with_former_name(group_magic, "--magic-analyze", "--magic_analyze", dest='magic_analyze',
443 type=true_or_false, default=False,
444 help="Analyze MAGIC extraction files (report as KLayout RDB)")
446 @staticmethod
447 def _add_analytical_25d_arguments(parser: argparse.ArgumentParser) -> None:
448 group_25d = parser.add_argument_group("2.5D Options")
449 group_25d.add_argument("--mode", dest='pex_mode',
450 default=PEXMode.DEFAULT, type=PEXMode, choices=list(PEXMode),
451 help=render_enum_help(topic='mode', enum_cls=PEXMode))
452 group_25d.add_argument("--halo", dest="halo",
453 type=float, default=None,
454 help=f"Custom sidewall halo distance (in µm) to override tech info "
455 f"(default is {halo_default_help(side_halo_by_pdk())})")
456 group_25d.add_argument("--scale", dest="scale_ratio_to_fit_halo",
457 type=true_or_false, default=True,
458 help=f"Scale fringe ratios, so that halo distance is 100%% (default is %(default)s)")
459 group_25d.add_argument("--check", dest="check_rc_netlist",
460 type=true_or_false, default=None,
461 help="Fail the run (after writing the netlist), if the extracted netlist is "
462 "inconsistent with the LVS netlist, e.g. a device terminal off the resistor "
463 "network of its net, a port that touches nothing, or a lost capacitance, "
464 "or if the LVS netlist lacks a device class of --schematic "
465 "or differs in its totals (e.g. Σ W·L of a MOS) "
466 "(default is True for --mode RC, otherwise these are warnings)")
467 group_25d.add_argument("--reduce", dest="reduce_mode",
468 default=ReductionMode.DEFAULT.value, choices=[m.value for m in ReductionMode],
469 help="Reduce the netlist of --mode R or RC, with the passes of 'netlist reduce': "
470 "'floating' merges the floating islands and eliminates the capacitor-only nodes "
471 "exactly, 'ticer' also eliminates the nodes whose time constant is at most "
472 "--reduce-tau (the error grows with the frequency, and losses are not kept). "
473 "Written next to the netlist (*_k25d_pex_netlist_reduced.spice), "
474 "and to --out-netlist. "
475 + render_enum_help(topic='reduce', enum_cls=ReductionMode))
476 add_argument_with_former_name(group_25d, "--reduce-tau", "--reduce_tau", dest="reduce_tau", metavar="TAU",
477 type=positive_spice_seconds, default=None,
478 help=f"TICER's time constant threshold in s for --reduce ticer, a SPICE number "
479 f"like '0.1p' (default is {ReductionOptions().ticer_tau * 1e12:g}p)")
480 add_argument_with_former_name(group_25d, "--report-caps", "--report_caps", dest="report_caps",
481 type=true_or_false, default=False,
482 help="Write each capacitance contribution with its shapes into the report "
483 "(*_k25d_pex_report.rdb.gz), which takes much memory and time "
484 "on large layouts (default is %(default)s)")
487 @staticmethod
488 def _add_exclusion_arguments(parser: argparse.ArgumentParser):
489 group = parser.add_argument_group("Exclusion Options",
490 "Leave parts of the layout to models or ideal sources, with the 2.5D engine "
491 "or the field solvers. The netlists of the field solvers, and of --mode CC, "
492 "have one node per net, so no ports for models")
493 group.add_argument("--exclusion-markers", dest='exclusion_marker_mode',
494 default=ExclusionMarkerMode.DEFAULT, type=ExclusionMarkerMode,
495 choices=list(ExclusionMarkerMode),
496 help="What to do with the exclusion markers of the PDK (e.g. IHP NoRCX.drawing), which "
497 "mark what a model has the parasitics of: 'ignore' extracts what is within them, "
498 "'tie' leaves out the capacitances within them and to the substrate, but not "
499 "the shapes, which still shield the rest of the layout, nor their capacitances "
500 "to it, and has no resistance within them, so that each net stays connected "
501 "across them, 'ports' does the same, but each cut of a net at a marker and each "
502 "device terminal within it is a port of the netlist, for a model to connect, "
503 "named after the pin of the net within the marker nearest to it, otherwise "
504 "<net>__m<marker>_<number>. Needs --gds, which has the markers. "
505 + render_enum_help(topic='exclusion_markers', enum_cls=ExclusionMarkerMode))
506 group.add_argument("--exclusion-marker-layer", dest='exclusion_marker_layers', action='append',
507 default=None, metavar='LAYER',
508 help="An exclusion marker of the PDK by its name (e.g. IHP Metal3.noqrc), or any layer "
509 "of the layout, as L/D for all layers of the stack, or as L/D:layer,layer for "
510 "these (e.g. 235/5:met2,met3), which also is how a PDK without exclusion markers "
511 "(sky130A, gf180mcuD) gets them. Repeatable, it replaces the markers "
512 "the PDK has on by default, e.g. NoRCX.drawing")
513 group.add_argument("--cut", dest='cut_specifications', action='append', default=None,
514 metavar='CELL[=PREFIX]',
515 help="Leave the instances of the cell (a name, or a glob pattern) to a model, "
516 "like what an exclusion marker marks, but only on the shapes of the cell, so that "
517 "routing over it stays extracted, and leave out its devices. Where a net enters "
518 "it is a port of the netlist: PREFIX__PIN by a label of the cell on the net, "
519 "by the terminal of a removed device, otherwise PREFIX__NET_NUMBER, the prefix "
520 "by default the name of the cell and a number. Needs --gds. Repeatable")
521 group.add_argument("--cut-keep-net", dest='cut_kept_net_patterns', action='append', default=None,
522 metavar='NET',
523 help="Extract the shapes of the net (a name, or a glob pattern) within the cells "
524 "of --cut like the rest of the layout, e.g. a guard ring. Repeatable")
525 group.add_argument("--exclude-net", dest='excluded_net_patterns', action='append', default=None,
526 metavar='NET',
527 help="Leave the net (a name, or a glob pattern) to a model or an ideal source, "
528 "e.g. the power grid of a block, which the top level extracts: it has no "
529 "resistance, and no capacitance to the substrate, while the capacitances "
530 "between it and other nets stay. Repeatable")
532 @staticmethod
533 def _add_pex25d_output_arguments(parser: argparse.ArgumentParser) -> None:
534 group_out = parser.add_argument_group(
535 "PEX25D Output",
536 description="At least one of --out-file / --out-scene must be given. "
537 "They are two options rather than one --emit switch because the "
538 "two artifacts are different things with different consumers, and "
539 "a single switch would leave it unclear where each one lands."
540 )
541 add_argument_with_former_name(group_out, "--out-file", "--output_file", dest='pex25d_file_path',
542 default=None, metavar='PATH',
543 help="Write the unresolved PEX25DFile here ('-' for stdout). "
544 "The literal form: one message per PEX25D record, "
545 "references still plain strings, nothing derived. This is "
546 "what a reader produces and what a validator checks.")
547 add_argument_with_former_name(group_out, "--out-scene", "--output_scene", dest='pex25d_scene_path',
548 default=None, metavar='PATH',
549 help="Write the resolved PEX25DScene here ('-' for stdout). "
550 "The adapter-ready form: absolute z extents, CONNECTS / "
551 "BETWEEN / WRAPS resolved, wrap depth flattened, terminal "
552 "intersections computed.")
553 group_out.add_argument("--format", dest='pex25d_format',
554 default=ArtifactFormat.AUTO, type=ArtifactFormat,
555 choices=list(ArtifactFormat),
556 help="Override the encoding otherwise inferred from the output "
557 "file names (default is '%(default)s'). Naming convention: "
558 "NAME.pex25d is text, NAME.pex25d.pb is binary protobuf, "
559 "NAME.pex25d.textpb is protobuf text format, and a .scene "
560 "infix marks a resolved scene. A trailing .gz is honoured.")
561 group_out.add_argument("--grid", dest='pex25d_grid',
562 default=DEFAULT_GRID_UM, metavar='UM',
563 help="Coordinate grid in µm (default is %(default)s). "
564 "Every z offset, thickness and coordinate must be "
565 "an integer multiple of it, and it must divide the "
566 "layout DBU exactly.")
567 add_argument_with_former_name(group_out, "--domain-margin", "--domain_margin", dest='pex25d_domain_margin',
568 type=float, default=None, metavar='UM',
569 help="Emit DOMAIN_MARGIN with this clearance in µm "
570 "(default is none, leaving the domain unset for "
571 "the solver adapter to choose)")
572 group_out.add_argument("--validate", dest='pex25d_validate',
573 type=true_or_false, default=True,
574 help="Check the generated artifacts against the reference "
575 "validator before reporting (default is %(default)s). "
576 "A defect in the technology data — a profile declared "
577 "twice, a film anchored on the wrong link — otherwise "
578 "shows up only when somebody runs 'pex25d validate' on "
579 "the result. The artifacts are written either way; the "
580 "diagnostics decide the exit code.")
581 group_out.add_argument("--comments", dest='pex25d_comments',
582 type=true_or_false, default=False,
583 help="Include the syntax hints from the specification as "
584 "comments (default is %(default)s). Text format only — "
585 "the protobuf encodings have no comments.")
586 add_argument_with_former_name(group_out, "--with-source-refs", "--with_source_refs",
587 dest='pex25d_with_source_refs',
588 type=true_or_false, default=False,
589 help="Populate every SourceRef (default is %(default)s). "
590 "Protobuf output formats only — the text format has no "
591 "spelling for a source reference. Roughly doubles the size "
592 "of a layout-scale file, and means little for geometry "
593 "built in memory rather than parsed from text; useful "
594 "mainly when diffing generated output against a "
595 "hand-written file.")
597 # ------------------------------------------------------------------ parsing
599 @staticmethod
600 def parse_args(arg_list: List[str],
601 env: Env) -> argparse.Namespace:
602 # epilog = f"See '{PROGRAM_NAME} <subcommand> -h' for help on subcommand"
603 epilog = EnvVar.help_epilog_table()
604 epilog_md = rich.console.Group(
605 rich.text.Text('Environmental variables:', style='argparse.groups'),
606 rich.markdown.Markdown(epilog, style='argparse.text')
607 )
608 main_parser = argparse.ArgumentParser(prog=PROGRAM_NAME,
609 description=f"{PROGRAM_NAME}: "
610 f"KLayout-integrated Parasitic Extraction Tool",
611 epilog=epilog_md,
612 add_help=False,
613 formatter_class=SectionedHelpFormatter)
615 group_special = main_parser.add_argument_group("Special Options")
616 group_special.add_argument("--help", "-h", action='help',
617 help="show this help message and exit")
618 group_special.add_argument("--version", "-v", action='version',
619 version=f'{PROGRAM_NAME} {__version__}')
621 subparsers = main_parser.add_subparsers(dest="command", metavar='<subcommand>')
623 parser_extract = subparsers.add_parser(
624 "extract",
625 help="Run one or more parasitic extraction engines",
626 description="Run PEX end to end: LVS for connectivity, then the selected "
627 "engine(s), then netlist expansion and reduction.",
628 add_help=False, formatter_class=RichHelpFormatter, epilog=epilog_md)
629 KpexCLI._add_special_options(parser_extract, include_threads=True)
630 KpexCLI._add_pex_setup_arguments(parser_extract, env)
631 KpexCLI._add_pex_input_arguments(parser_extract)
632 KpexCLI._add_engine_arguments(parser_extract)
633 KpexCLI._add_extraction_options(parser_extract)
634 KpexCLI._add_tech_arguments(parser_extract)
635 KpexCLI._add_fastercap_arguments(parser_extract)
636 KpexCLI._add_magic_arguments(parser_extract, env)
637 KpexCLI._add_analytical_25d_arguments(parser_extract)
638 KpexCLI._add_exclusion_arguments(parser_extract)
640 parser_pex25d = subparsers.add_parser(
641 "pex25d",
642 help="Generate PEX25D solver input from a layout, without running a solver",
643 description="Run LVS for connectivity and write the scene as PEX25D, stopping "
644 "before any engine is started. This is the cut point in the "
645 "pipeline that lets the geometry be inspected, archived, diffed, "
646 "or handed to somebody else's solver. Format-only work on the "
647 "result — validating, re-encoding, exporting to an engine's "
648 "native input — is the standalone 'pex25d' tool's job.",
649 add_help=False, formatter_class=RichHelpFormatter, epilog=epilog_md)
650 KpexCLI._add_special_options(parser_pex25d)
651 KpexCLI._add_pex_setup_arguments(parser_pex25d, env, include_spice_output=False)
652 KpexCLI._add_pex_input_arguments(parser_pex25d)
653 KpexCLI._add_extraction_options(parser_pex25d)
654 KpexCLI._add_tech_arguments(parser_pex25d)
655 KpexCLI._add_pex25d_output_arguments(parser_pex25d)
656 Pex25DCLI._add_diagnostics_arguments(parser_pex25d)
658 add_help_subcommand(subparsers)
659 add_subcommand_sections(main_parser, subparsers, KpexCLI.SUBCOMMAND_SECTIONS)
661 if arg_list is None:
662 arg_list = sys.argv[1:]
663 args = main_parser.parse_args(arg_list)
664 handle_help_subcommand(main_parser, args)
665 if args.command is None:
666 main_parser.print_help()
667 sys.exit(ExitCode.USAGE)
669 # The engine switches only exist on 'extract'. Defaulting them here keeps
670 # the shared validation and logging paths free of subcommand checks.
671 for engine_attr in ('run_magic', 'run_fastcap', 'run_fastercap', 'run_2_5D'):
672 if not hasattr(args, engine_attr):
673 setattr(args, engine_attr, False)
675 # environmental variables and their defaults
676 args.fastcap_exe_path = env[EnvVar.FASTCAP_EXE]
677 args.fastercap_exe_path = env[EnvVar.FASTERCAP_EXE]
678 args.klayout_exe_path = env[EnvVar.KLAYOUT_EXE]
679 args.magic_exe_path = env[EnvVar.MAGIC_EXE]
681 return args
683 @staticmethod
684 def validate_args(args: argparse.Namespace):
685 found_errors = False
687 pdk_config: PDKConfig = args.pdk.config
688 args.tech_pbjson_path = pdk_config.tech_pb_json_path
689 args.lvs_script_path = pdk_config.pex_lvs_script_path
691 def input_file_stem(path: str):
692 # could be *.gds, or *.gds.gz, so remove all extensions
693 return os.path.basename(path).split(sep='.')[0]
695 if not os.path.isfile(args.tech_pbjson_path):
696 error(f"Can't read technology file at path {args.tech_pbjson_path}")
697 found_errors = True
699 if not os.path.isfile(args.lvs_script_path):
700 error(f"Can't locate LVS script path at {args.lvs_script_path}")
701 found_errors = True
703 rule('Input Layout')
705 # Which engines may be combined with which inputs is a question only
706 # 'extract' has; a dump run has no engine to disagree with.
707 if args.command == 'extract':
708 # check engines VS input possiblities
709 match (args.run_magic, args.run_fastcap, args.run_fastercap, args.run_2_5D,
710 args.gds_path, args.lvsdb_path):
711 case (True, _, _, _, None, _):
712 error(f"Running PEX engine MAGIC requires --gds (--lvsdb not possible)")
713 found_errors = True
714 case (False, False, False, False, _, _): # at least one engine must be activated
715 error("No PEX engines activated")
716 engine_help = """
717 | Argument | Description |
718 | ------------ | ------------------------------- |
719 | --2.5D | Run KPEX/2.5D analytical engine |
720 | --fastcap | Run KPEX/FastCap2 3D engine |
721 | --fastercap | Run KPEX/FasterCap 3D engine |
722 | --magic | Run MAGIC wrapper engine |
723 """
724 subproc(f"\n\nPlease activate one or more engines using the arguments:")
725 rich.print(rich.markdown.Markdown(engine_help, style='argparse.text'))
726 found_errors = True
727 case (_, _, _, _, None, None):
728 error(f"Neither GDS nor LVSDB was provided")
729 found_errors = True
731 args.reduce_mode = ReductionMode(args.reduce_mode)
732 if args.reduce_mode != ReductionMode.NONE:
733 if not args.run_2_5D:
734 error(f"--reduce {args.reduce_mode} reduces the netlist of the 2.5D engine, it needs --2.5D")
735 found_errors = True
736 elif not args.pex_mode.need_resistance():
737 error(f"--reduce {args.reduce_mode} needs --mode R or RC, "
738 f"a {args.pex_mode} netlist has no nodes to reduce")
739 found_errors = True
740 if args.reduce_tau is not None and args.reduce_mode != ReductionMode.TICER:
741 error(f"--reduce-tau applies to --reduce ticer only, not to --reduce {args.reduce_mode}")
742 found_errors = True
744 if args.exclusion_marker_layers and args.exclusion_marker_mode == ExclusionMarkerMode.IGNORE:
745 error(f"--exclusion-marker-layer {args.exclusion_marker_layers[0]} needs --exclusion-markers "
746 f"{ExclusionMarkerMode.TIE} or {ExclusionMarkerMode.PORTS}")
747 found_errors = True
749 if args.cut_kept_net_patterns and not args.cut_specifications:
750 error(f"--cut-keep-net {args.cut_kept_net_patterns[0]} needs --cut")
751 found_errors = True
753 layout_options = [f"--exclusion-markers {args.exclusion_marker_mode}"
754 if args.exclusion_marker_mode != ExclusionMarkerMode.IGNORE else None,
755 f"--cut {args.cut_specifications[0]}" if args.cut_specifications else None]
756 net_options = [f"--exclude-net {args.excluded_net_patterns[0]}" if args.excluded_net_patterns else None]
757 for exclusion_option in filter(None, layout_options + net_options):
758 # NOTE: MAGIC would extract what is left out
759 if args.run_magic:
760 error(f"{exclusion_option} applies to the 2.5D engine, FasterCap and FastCap2, not to MAGIC")
761 found_errors = True
762 for exclusion_option in filter(None, layout_options):
763 # NOTE: the LVS database only has the shapes of nets
764 if args.gds_path is None:
765 error(f"{exclusion_option} reads its shapes from the layout, so it needs --gds, also with --lvsdb")
766 found_errors = True
768 args.netlist_format = NetlistFormat(args.netlist_format)
769 if args.netlist_format != NetlistFormat.SPICE:
770 # NOTE: MAGIC writes its SPICE netlist itself, FasterCap and FastCap2 have no other format (yet)
771 other_engines = [name for name, is_run in (('MAGIC', args.run_magic),
772 ('FastCap2', args.run_fastcap),
773 ('FasterCap', args.run_fastercap)) if is_run]
774 if other_engines:
775 error(f"Only the 2.5D engine writes {args.netlist_format} netlists "
776 f"(--netlist-format {args.netlist_format}), not {', '.join(other_engines)}")
777 found_errors = True
779 if args.output_spice_path is not None:
780 if args.output_netlist_path is not None:
781 error("--out_spice is the deprecated name of --out-netlist, give --out-netlist only")
782 found_errors = True
783 elif args.netlist_format != NetlistFormat.SPICE:
784 error(f"--out_spice writes a SPICE netlist, not --netlist-format {args.netlist_format}, "
785 f"use --out-netlist")
786 found_errors = True
787 else:
788 warning("--out_spice is deprecated, use --out-netlist")
789 args.output_netlist_path = args.output_spice_path
791 # check if we find magicrc
792 if args.run_magic:
793 if args.magicrc_path is None:
794 error(f"magicrc not available, requires any those:\n"
795 f"\t• set environmental variables PDK_ROOT / PDK\n"
796 f"\t• pass argument --magicrc")
797 found_errors = True
798 else:
799 result = validate_files([args.magicrc_path])
800 for f in result.failures:
801 error(f"Invalid magicrc: {f.reason} at {str(f.path)}")
802 found_errors = True
804 args.cut_requests = []
805 if args.command == 'extract':
806 try:
807 args.cut_requests = parsed_cut_requests(args.cut_specifications or [])
808 except ExclusionError as e:
809 error(str(e))
810 found_errors = True
812 # input mode: LVS or existing LVSDB?
813 if args.gds_path:
814 if args.lvsdb_path is None:
815 info(f"GDS input file passed, running in LVS mode")
816 args.input_mode = InputMode.GDS
817 if not os.path.isfile(args.gds_path):
818 error(f"Can't read GDS file (LVS input) at path {args.gds_path}")
819 found_errors = True
820 else:
821 args.layout = kdb.Layout()
822 args.layout.read(args.gds_path)
824 top_cells = args.layout.top_cells()
826 if args.cell_name: # explicit user-specified cell name
827 args.effective_cell_name = args.cell_name
829 found_cell: Optional[kdb.Cell] = None
830 for cell in args.layout.cells('*'):
831 if cell.name == args.effective_cell_name:
832 found_cell = cell
833 break
834 if not found_cell:
835 error(f"Could not find cell {args.cell_name} in GDS {args.gds_path}")
836 found_errors = True
838 is_only_top_cell = len(top_cells) == 1 and top_cells[0].name == args.cell_name
839 if is_only_top_cell:
840 info(f"Found cell {args.cell_name} in GDS {args.gds_path} (only top cell)")
841 else: # there are other cells => extract the top cell to a tmp layout
842 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}"
843 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id)
844 os.makedirs(args.output_dir_path, exist_ok=True)
845 args.effective_gds_path = os.path.join(args.output_dir_path,
846 f"{args.cell_name}_exported.gds.gz")
847 info(f"Found cell {args.cell_name} in GDS {args.gds_path}, "
848 f"but it is not the only top cell, "
849 f"so layout is exported to: {args.effective_gds_path}")
851 write_output_file(found_cell.write, args.effective_gds_path)
852 else: # find top cell
853 if len(top_cells) == 1:
854 args.effective_cell_name = top_cells[0].name
855 info(f"No explicit top cell specified, using top cell '{args.effective_cell_name}'")
856 else:
857 args.effective_cell_name = 'TOP'
858 error(f"Could not determine the default top cell in GDS {args.gds_path}, "
859 f"there are multiple: {', '.join([c.name for c in top_cells])}. "
860 f"Use --cell to specify the cell")
861 found_errors = True
863 if not hasattr(args, 'effective_gds_path'):
864 args.effective_gds_path = args.gds_path
865 if args.lvsdb_path is not None:
866 info(f"LVSDB input file passed, bypassing LVS")
867 args.input_mode = InputMode.LVSDB
868 if not os.path.isfile(args.lvsdb_path):
869 error(f"Can't read KLayout LVSDB file at path {args.lvsdb_path}")
870 found_errors = True
871 else:
872 lvsdb = kdb.LayoutVsSchematic()
873 lvsdb.read(args.lvsdb_path)
874 top_cell: kdb.Cell = lvsdb.internal_top_cell()
875 if args.gds_path and not found_errors and args.effective_cell_name != top_cell.name:
876 error(f"The LVS database {args.lvsdb_path} is of the cell {top_cell.name}, "
877 f"not of the cell {args.effective_cell_name} of the GDS {args.gds_path}")
878 found_errors = True
879 args.effective_cell_name = top_cell.name
881 if args.cut_requests and getattr(args, 'layout', None) is not None and hasattr(args, 'effective_cell_name') \
882 and args.layout.cell(args.effective_cell_name) is not None:
883 cell_names = cells_called_by(args.layout, args.layout.cell(args.effective_cell_name))
884 for cut_request in args.cut_requests:
885 if not any(matches_name(name, cut_request.cell_pattern) for name in cell_names):
886 error(f"--cut {cut_request.cell_pattern}: no such cell below {args.effective_cell_name}, "
887 f"which has: {', '.join(cell_names) or 'none'}")
888 found_errors = True
889 if not found_errors:
890 try:
891 check_instances_of_cut_cells(args.layout, args.layout.cell(args.effective_cell_name),
892 args.cut_requests)
893 except ExclusionError as e:
894 error(str(e))
895 found_errors = True
897 if hasattr(args, 'effective_cell_name'):
898 run_dir_id: str
899 match args.input_mode:
900 case InputMode.GDS:
901 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}"
902 case InputMode.LVSDB:
903 run_dir_id = f"{input_file_stem(args.lvsdb_path)}__{args.effective_cell_name}"
904 case _:
905 raise NotImplementedError(f"Unknown input mode {args.input_mode}")
907 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id)
908 os.makedirs(args.output_dir_path, exist_ok=True)
909 if args.input_mode == InputMode.GDS:
910 if args.schematic_path:
911 if not os.path.isfile(args.schematic_path):
912 error(f"Can't read schematic at path {args.schematic_path}")
913 found_errors = True
914 warning("--schematic is checked for its device classes and their totals only, "
915 "use regular LVS to compare layout against schematic")
916 args.effective_schematic_path = os.path.join(args.output_dir_path,
917 f"{args.effective_cell_name}_dummy_schematic.spice")
918 LVSRunner.write_dummy_schematic(args.effective_schematic_path, args.effective_cell_name)
920 try:
921 pattern_string: str = args.dielectric_filter
922 args.dielectric_filter = MultipleChoicePattern(pattern=pattern_string)
923 except ValueError as e:
924 error("Failed to parse --diel arg", e)
925 found_errors = True
927 if args.cache_dir_path is None:
928 args.cache_dir_path = os.path.join(args.output_dir_base_path, '.kpex_cache')
930 if args.command == 'pex25d':
931 if args.pex25d_file_path is None and args.pex25d_scene_path is None:
932 error("Nothing to write. Give --out-file (the unresolved PEX25DFile), "
933 "--out-scene (the resolved PEX25DScene), or both.")
934 found_errors = True
936 if args.pex25d_file_path is not None \
937 and args.pex25d_file_path == args.pex25d_scene_path:
938 error("--out-file and --out-scene point at the same destination")
939 found_errors = True
941 # Resolved here rather than at write time so that a mistyped output
942 # name fails now, and not after a multi-minute LVS run.
943 args.pex25d_file_spec = None
944 args.pex25d_scene_spec = None
945 try:
946 if args.pex25d_file_path is not None:
947 args.pex25d_file_spec = infer_artifact_spec(
948 args.pex25d_file_path,
949 kind=ArtifactKind.FILE,
950 format=args.pex25d_format,
951 default_format=ArtifactFormat.TEXT)
952 if args.pex25d_scene_path is not None:
953 args.pex25d_scene_spec = infer_artifact_spec(
954 args.pex25d_scene_path,
955 kind=ArtifactKind.SCENE,
956 format=args.pex25d_format,
957 default_format=ArtifactFormat.PB)
958 except ArtifactNamingError as e:
959 error(str(e))
960 found_errors = True
962 # NOTE: the name says the kind too, which the readers go by (binary protobuf doesn't record it)
963 for option, path, kind in (('--out-file', args.pex25d_file_path, ArtifactKind.FILE),
964 ('--out-scene', args.pex25d_scene_path, ArtifactKind.SCENE)):
965 named = named_kind_and_format(path) if path is not None else None
966 if named is not None and named[0] != kind:
967 suffixes = [suffix for (k, _), suffix in CANONICAL_SUFFIX.items() if k == kind]
968 error(f"{option} {path} is named as a PEX25D {named[0].value} ({CANONICAL_SUFFIX[named]}), "
969 f"which pex25d would read it as; name a {kind.value} with one of: {', '.join(suffixes)}")
970 found_errors = True
972 # KLayout runs the LVS script, which the MAGIC wrapper does not need
973 # ("no need to run LVS etc if only running magic engine") and which an
974 # LVSDB input has already been through.
975 needs_lvs = getattr(args, 'input_mode', None) == InputMode.GDS and \
976 (args.command != 'extract' or
977 args.run_fastcap or args.run_fastercap or args.run_2_5D)
979 exe_path_by_tool: Dict[Tool, str] = {}
980 if needs_lvs:
981 exe_path_by_tool[Tool.KLAYOUT] = args.klayout_exe_path
982 if args.run_magic:
983 exe_path_by_tool[Tool.MAGIC] = args.magic_exe_path
984 if args.run_fastercap:
985 exe_path_by_tool[Tool.FASTERCAP] = args.fastercap_exe_path
986 if args.run_fastcap:
987 exe_path_by_tool[Tool.FASTCAP] = args.fastcap_exe_path
989 # Only the tools this run will actually start have to be there.
990 for tool, exe_path in list(exe_path_by_tool.items()):
991 if not (os.path.isfile(exe_path) or shutil.which(exe_path)):
992 error(f"Can't locate {tool.value} executable at {exe_path} "
993 f"(see {tool.env_var.value})")
994 found_errors = True
995 del exe_path_by_tool[tool]
997 if not check_tool_versions(exe_path_by_tool=exe_path_by_tool,
998 pdk=args.pdk,
999 mode=args.version_check_mode):
1000 found_errors = True
1002 if found_errors:
1003 raise ArgumentValidationError("Argument validation failed")
1005 def create_netlist_printer(self,
1006 args: argparse.Namespace,
1007 extraction_engine: ExtractionEngine,
1008 tech_info: TechInfo) -> NetlistPrinter:
1009 header = NetlistHeader(kpex_version=__version__,
1010 extraction_engine=str(extraction_engine),
1011 tech=args.pdk.name.lower(),
1012 date=datetime.now())
1013 return NetlistPrinter(header=header,
1014 device_models=tech_info.device_models)
1016 def build_fastercap_input(self,
1017 args: argparse.Namespace,
1018 pex_context: KLayoutExtractionContext,
1019 tech_info: TechInfo,
1020 exclusions: Optional[Exclusions]) -> str:
1021 rule('Process stackup')
1022 fastercap_input_builder = FasterCapInputBuilder(pex_context=pex_context,
1023 tech_info=tech_info,
1024 substrate_net_name=pex_context.substrate_net_name,
1025 k_void=args.k_void,
1026 delaunay_amax=args.delaunay_amax,
1027 delaunay_b=args.delaunay_b,
1028 exclusions=exclusions)
1029 gen: FasterCapModelGenerator = fastercap_input_builder.build()
1031 rule('FasterCap Input File Generation')
1032 faster_cap_input_dir_path = os.path.join(args.output_dir_path, 'FasterCap_Input_Files')
1033 os.makedirs(faster_cap_input_dir_path, exist_ok=True)
1035 written = gen.write_fastcap(output_dir_path=faster_cap_input_dir_path, prefix='FasterCap_Input_')
1037 rule('STL File Generation')
1038 geometry_dir_path = os.path.join(args.output_dir_path, 'Geometries')
1039 os.makedirs(geometry_dir_path, exist_ok=True)
1040 gen.dump_stl(output_dir_path=geometry_dir_path, prefix='')
1042 if args.geometry_check:
1043 rule('Geometry Validation')
1044 gen.check()
1046 return written[0]
1049 def run_fastercap_extraction(self,
1050 args: argparse.Namespace,
1051 pex_context: KLayoutExtractionContext,
1052 lst_file: str,
1053 exclusions: Optional[Exclusions]):
1054 rule('FasterCap Execution')
1055 info(f"Configure number of OpenMP threads (environmental variable OMP_NUM_THREADS) as {args.num_threads}")
1056 os.environ['OMP_NUM_THREADS'] = f"{args.num_threads}"
1058 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Output.txt")
1059 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Raw.csv")
1060 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Avg.csv")
1061 expanded_netlist_path = os.path.join(args.output_dir_path,
1062 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.cir")
1063 expanded_netlist_csv_path = os.path.join(args.output_dir_path,
1064 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.csv")
1065 reduced_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Reduced_Netlist.cir")
1067 run_fastercap(exe_path=args.fastercap_exe_path,
1068 lst_file_path=lst_file,
1069 log_path=log_path,
1070 tolerance=args.fastercap_tolerance,
1071 d_coeff=args.fastercap_d_coeff,
1072 mesh_refinement_value=args.fastercap_mesh_refinement_value,
1073 ooc_condition=args.fastercap_ooc_condition,
1074 auto_preconditioner=args.fastercap_auto_preconditioner,
1075 galerkin_scheme=args.fastercap_galerkin_scheme,
1076 jacobi_preconditioner=args.fastercap_jacobi_preconditioner)
1078 cap_matrix = fastercap_parse_capacitance_matrix(log_path)
1079 cap_matrix.write_csv(raw_csv_path)
1081 cap_matrix = cap_matrix.averaged_off_diagonals()
1082 cap_matrix.write_csv(avg_csv_path)
1084 cap_matrix_interpreter = FasterCapOutputInterpreter()
1086 netlist_expander = NetlistExpander()
1087 expanded_netlist = netlist_expander.expand(
1088 extracted_netlist=pex_context.lvsdb.netlist(),
1089 top_cell_name=pex_context.annotated_top_cell.name,
1090 cap_matrix=cap_matrix,
1091 cap_matrix_interpreter=cap_matrix_interpreter,
1092 blackbox_devices=args.blackbox_devices,
1093 device_models=pex_context.tech.device_models,
1094 substrate_net_name=pex_context.substrate_net_name,
1095 exclusions=exclusions
1096 )
1098 # create a nice CSV for reports, useful for spreadsheets
1099 netlist_csv_writer = NetlistCSVWriter()
1100 netlist_csv_writer.write_csv(netlist=expanded_netlist,
1101 top_cell_name=pex_context.annotated_top_cell.name,
1102 output_path=expanded_netlist_csv_path)
1104 rule("Extended netlist (CSV format):")
1105 with open(expanded_netlist_csv_path, 'r', encoding='utf-8') as f:
1106 for line in f.readlines():
1107 subproc(line[:-1]) # abusing subproc, simply want verbatim
1108 rule()
1110 info(f"Wrote expanded netlist CSV to: {expanded_netlist_csv_path}")
1112 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTERCAP, pex_context.tech)
1113 netlist_printer.write(expanded_netlist, expanded_netlist_path)
1114 info(f"Wrote expanded netlist to: {expanded_netlist_path}")
1116 # FIXME: should this be already reduced?
1117 if args.output_netlist_path:
1118 netlist_printer.write(expanded_netlist, args.output_netlist_path)
1119 info(f"Copied expanded SPICE netlist to: {args.output_netlist_path}")
1121 netlist_reducer = NetlistReducer()
1122 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist,
1123 top_cell_name=pex_context.annotated_top_cell.name)
1124 netlist_printer.write(reduced_netlist, reduced_netlist_path)
1125 info(f"Wrote reduced netlist to: {reduced_netlist_path}")
1127 self._fastercap_extracted_csv_path = expanded_netlist_csv_path
1129 def run_magic_extraction(self,
1130 args: argparse.Namespace):
1131 if args.input_mode != InputMode.GDS:
1132 error(f"MAGIC engine only works with GDS input mode"
1133 f" (currently {args.input_mode})")
1134 return
1136 magic_run_dir = os.path.join(args.output_dir_path, f"magic_{args.magic_pex_mode}")
1137 magic_log_path = os.path.join(magic_run_dir,
1138 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Output.txt")
1139 magic_script_path = os.path.join(magic_run_dir,
1140 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Script.tcl")
1142 output_netlist_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}.pex.spice")
1143 report_db_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}_MAGIC_report.rdb.gz")
1145 os.makedirs(magic_run_dir, exist_ok=True)
1147 layout = kdb.Layout()
1148 layout.read(args.effective_gds_path)
1150 if args.magic_pre_flatten:
1151 tops = list(layout.top_cells())
1152 if not tops:
1153 raise Exception("No top cells found in the input layout.")
1154 for top in tops:
1155 top.flatten(True)
1156 flattened_gds_path=os.path.join(magic_run_dir, f"{args.effective_cell_name}.gds")
1157 layout.write(flattened_gds_path)
1158 gds_path = flattened_gds_path
1159 else:
1160 gds_path = args.effective_gds_path
1162 prepare_magic_script(gds_path=gds_path,
1163 cell_name=args.effective_cell_name,
1164 run_dir_path=magic_run_dir,
1165 script_path=magic_script_path,
1166 output_netlist_path=output_netlist_path,
1167 pex_mode=args.magic_pex_mode,
1168 c_threshold=args.magic_cthresh,
1169 r_threshold=args.magic_rthresh,
1170 threshold=args.magic_thresh,
1171 min_res=args.magic_minres,
1172 min_delay=args.magic_mindel,
1173 halo=args.magic_halo,
1174 short_mode=args.magic_short_mode,
1175 merge_mode=args.magic_merge_mode,
1176 unique_mode=args.magic_unique_mode)
1178 run_magic(exe_path=args.magic_exe_path,
1179 magicrc_path=args.magicrc_path,
1180 script_path=magic_script_path,
1181 log_path=magic_log_path)
1183 magic_pex_run = parse_magic_pex_run(Path(magic_run_dir))
1185 if args.magic_analyze:
1186 report = rdb.ReportDatabase('')
1187 magic_log_analyzer = MagicLogAnalyzer(magic_pex_run=magic_pex_run,
1188 report=report,
1189 dbu=layout.dbu)
1190 magic_log_analyzer.analyze()
1191 write_output_file(report.save, report_db_path)
1193 rule("Paths")
1194 if args.magic_analyze:
1195 subproc(f"Report DB saved at: {report_db_path}")
1196 subproc(f"SPICE netlist saved at: {output_netlist_path}")
1198 if os.path.exists(output_netlist_path):
1199 if args.output_netlist_path and os.path.exists(output_netlist_path):
1200 shutil.copy(output_netlist_path, args.output_netlist_path)
1201 info(f"Copied expanded SPICE netlist to: {args.output_netlist_path}")
1203 rule("MAGIC PEX SPICE netlist")
1204 with open(output_netlist_path, 'r', encoding='utf-8', errors='replace') as f:
1205 subproc(f.read())
1206 rule()
1208 def run_fastcap_extraction(self,
1209 args: argparse.Namespace,
1210 pex_context: KLayoutExtractionContext,
1211 lst_file: str,
1212 exclusions: Optional[Exclusions]):
1213 rule('FastCap2 Execution')
1215 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Output.txt")
1216 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Raw.csv")
1217 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Avg.csv")
1218 expanded_netlist_path = os.path.join(args.output_dir_path,
1219 f"{args.effective_cell_name}_FastCap2_Expanded_Netlist.cir")
1220 reduced_netlist_path = os.path.join(args.output_dir_path,
1221 f"{args.effective_cell_name}_FastCap2_Reduced_Netlist.cir")
1223 run_fastcap(exe_path=args.fastcap_exe_path,
1224 lst_file_path=lst_file,
1225 log_path=log_path)
1227 cap_matrix = fastcap_parse_capacitance_matrix(log_path)
1228 cap_matrix.write_csv(raw_csv_path)
1230 cap_matrix = cap_matrix.averaged_off_diagonals()
1231 cap_matrix.write_csv(avg_csv_path)
1233 cap_matrix_interpreter = FastCapOutputInterpreter()
1235 netlist_expander = NetlistExpander()
1236 expanded_netlist = netlist_expander.expand(
1237 extracted_netlist=pex_context.lvsdb.netlist(),
1238 top_cell_name=pex_context.annotated_top_cell.name,
1239 cap_matrix=cap_matrix,
1240 cap_matrix_interpreter=cap_matrix_interpreter,
1241 blackbox_devices=args.blackbox_devices,
1242 device_models=pex_context.tech.device_models,
1243 substrate_net_name=pex_context.substrate_net_name,
1244 exclusions=exclusions
1245 )
1247 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTCAP2, pex_context.tech)
1248 netlist_printer.write(expanded_netlist, expanded_netlist_path)
1249 info(f"Wrote expanded netlist to: {expanded_netlist_path}")
1251 # FIXME: should this be already reduced?
1252 if args.output_netlist_path:
1253 netlist_printer.write(expanded_netlist, args.output_netlist_path)
1254 info(f"Copied expanded SPICE netlist to: {args.output_netlist_path}")
1256 netlist_reducer = NetlistReducer()
1257 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist,
1258 top_cell_name=pex_context.annotated_top_cell.name)
1259 netlist_printer.write(reduced_netlist, reduced_netlist_path)
1261 info(f"Wrote reduced netlist to: {reduced_netlist_path}")
1263 @staticmethod
1264 def schematic_device_class_problems(schematic_path: str,
1265 lvs_netlist: kdb.Netlist,
1266 device_models: DeviceModels,
1267 top_cell_name: str) -> List[str]:
1268 """
1269 :return: the problems of check_schematic_device_classes and check_schematic_device_totals,
1270 or that the schematic can't be read or evaluated
1271 """
1272 try:
1273 netlist = SpiceReader().read(schematic_path)
1274 except SpiceReaderError as e:
1275 return [f"kpex can't read the schematic, so no device classes are compared: {e}"]
1276 try:
1277 schematic = schematic_devices(netlist=netlist, device_models=device_models, top_cell_name=top_cell_name)
1278 except SpiceEvaluationError as e:
1279 return [f"kpex can't evaluate the schematic, so no device classes are compared: {schematic_path}: {e}"]
1280 if schematic.device_names_by_unknown_model:
1281 warning("The device class check leaves out the devices of the schematic of models without device model "
1282 "mapping:\n" +
1283 '\n'.join(f" - {model}: {first_names(names)}"
1284 for model, names in sorted(schematic.device_names_by_unknown_model.items())))
1285 return check_schematic_device_classes(lvs_netlist=lvs_netlist, schematic=schematic) + \
1286 check_schematic_device_totals(lvs_netlist=lvs_netlist, schematic=schematic, device_models=device_models)
1288 @staticmethod
1289 def exclusions_of(args: argparse.Namespace,
1290 pex_context: KLayoutExtractionContext,
1291 tech_info: TechInfo) -> Optional[Exclusions]:
1292 """
1293 :return: what is left out of the extraction, None if nothing is
1294 """
1295 marker_mode = args.exclusion_marker_mode
1296 honors_markers = marker_mode != ExclusionMarkerMode.IGNORE
1297 if not honors_markers and not args.cut_requests and not args.excluded_net_patterns:
1298 return None
1300 substrate_layer_name = tech_info.internal_substrate_layer_name
1301 exclusions = Exclusions([], substrate_layer_name)
1302 if honors_markers or args.cut_requests:
1303 exclusions = Exclusions.from_layout(
1304 layout=args.layout,
1305 cell_name=args.effective_cell_name,
1306 marker_layers=args.selected_marker_layers if honors_markers else [],
1307 dbu=pex_context.dbu,
1308 ports=marker_mode == ExclusionMarkerMode.PORTS,
1309 substrate_layer_name=substrate_layer_name,
1310 cut_requests=args.cut_requests,
1311 cut_layers=CutLayers.of_tech(tech_info) if args.cut_requests else None
1312 )
1313 if honors_markers and not any(block.kind == BlockKind.MARKER for block in exclusions.blocks):
1314 warning("The layout has no shapes on the exclusion markers, so the extraction leaves nothing out of them")
1316 net_names = sorted(net.expanded_name() for net in pex_context.top_circuit.each_net())
1318 def names_of_nets(option: str, pattern: str) -> List[NetName]:
1319 matching_names = [name for name in net_names if matches_name(name, pattern)]
1320 if not matching_names:
1321 raise ExclusionError(f"{option} {pattern}: no such net, the nets are: {', '.join(net_names)}")
1322 return matching_names
1324 for pattern in args.cut_kept_net_patterns or []:
1325 for net_name in names_of_nets('--cut-keep-net', pattern):
1326 exclusions.without_shapes(pex_context.net_shapes_by_conductor_layer(net_name))
1327 for pattern in args.excluded_net_patterns or []:
1328 exclusions.excluded_net_names.update(names_of_nets('--exclude-net', pattern))
1329 prepare_exclusions(exclusions, pex_context, tech_info)
1330 return exclusions
1332 def run_kpex_2_5d_engine(self,
1333 args: argparse.Namespace,
1334 pex_context: KLayoutExtractionContext,
1335 tech_info: TechInfo,
1336 report_path: str,
1337 netlist_csv_path: Optional[str],
1338 expanded_netlist_path: Optional[str],
1339 exclusions: Optional[Exclusions]):
1340 # TODO: make this separatly configurable
1341 # for now we use 0
1342 args.rcx25d_delaunay_amax = 0
1343 args.rcx25d_delaunay_b = 0.5
1345 if args.pex_mode.need_capacitance():
1346 halo_source = '--halo' if args.halo is not None else f"tech info of {args.pdk}"
1347 info(f"Sidewall halo: {tech_info.tech.process_parasitics.side_halo:g} µm ({halo_source})")
1349 extractor = RCX25Extractor(pex_context=pex_context,
1350 pex_mode=args.pex_mode,
1351 delaunay_amax=args.rcx25d_delaunay_amax,
1352 delaunay_b=args.rcx25d_delaunay_b,
1353 scale_ratio_to_fit_halo=args.scale_ratio_to_fit_halo,
1354 tech_info=tech_info,
1355 report_path=report_path,
1356 report_caps=args.report_caps,
1357 exclusions=exclusions)
1358 extraction_results = extractor.extract()
1359 removed_device_ids = set(exclusions.removed_device_blocks) if exclusions is not None else set()
1361 if netlist_csv_path is not None:
1362 # TODO: merge this with klayout_pex/klayout/netlist_csv.py
1364 with open(netlist_csv_path, 'w', encoding='utf-8') as f:
1365 summary = extraction_results.summarize()
1367 f.write('Device;Net1;Net2;Capacitance [fF];Resistance [Ω]\n')
1368 for idx, (key, cap_value) in enumerate(sorted(summary.capacitances.items())):
1369 f.write(f"C{idx + 1};{key.net1};{key.net2};{round(cap_value, 3)};\n")
1370 for idx, (key, res_value) in enumerate(sorted(summary.resistances.items())):
1371 f.write(f"R{idx + 1};{key.net1};{key.net2};;{round(res_value, 3)}\n")
1373 rule('kpex/2.5D extracted netlist (CSV format)')
1374 with open(netlist_csv_path, 'r', encoding='utf-8') as f:
1375 for line in f.readlines():
1376 subproc(line[:-1]) # abusing subproc, simply want verbatim
1378 rule('Extracted netlist CSV')
1379 subproc(f"{netlist_csv_path}")
1381 if expanded_netlist_path is not None:
1382 rule('kpex/2.5D extracted netlist (SPICE format)')
1383 netlist_expander = RCX25NetlistExpander()
1384 expanded_netlist = netlist_expander.expand(
1385 extracted_netlist=pex_context.lvsdb.netlist(),
1386 top_cell_name=pex_context.annotated_top_cell.name,
1387 extraction_results=extraction_results,
1388 blackbox_devices=args.blackbox_devices,
1389 device_models=tech_info.device_models,
1390 substrate_net_name=pex_context.substrate_net_name,
1391 removed_device_ids=removed_device_ids
1392 )
1394 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.K25D, tech_info)
1395 netlist_printer.write(expanded_netlist, expanded_netlist_path)
1396 subproc(f"Wrote expanded netlist to: {expanded_netlist_path}")
1397 self._rcx25_netlist_paths = {NetlistFormat.SPICE: expanded_netlist_path} # NOTE: store for test case
1399 # NOTE: a defect of the RC netlist simulates to a plausible, but wrong number (#215)
1400 self._rcx25_netlist_problems = check_rc_netlist( # NOTE: store for test case
1401 lvs_netlist=pex_context.lvsdb.netlist(),
1402 rc_netlist=expanded_netlist,
1403 top_cell_name=pex_context.annotated_top_cell.name,
1404 summary=extraction_results.summarize(),
1405 blackbox_devices=args.blackbox_devices,
1406 device_models=tech_info.device_models,
1407 substrate_net_name=pex_context.substrate_net_name,
1408 removed_device_ids=removed_device_ids
1409 )
1410 if args.input_mode == InputMode.GDS and args.schematic_path:
1411 self._rcx25_netlist_problems += self.schematic_device_class_problems(
1412 schematic_path=args.schematic_path,
1413 lvs_netlist=pex_context.lvsdb.netlist(),
1414 device_models=tech_info.device_models,
1415 top_cell_name=pex_context.annotated_top_cell.name
1416 )
1417 # NOTE: on by default for RC netlists, as their defects simulate to plausible, but wrong numbers
1418 check_fails_run = args.check_rc_netlist if args.check_rc_netlist is not None \
1419 else args.pex_mode == PEXMode.RC
1420 if self._rcx25_netlist_problems and not check_fails_run:
1421 warning("The extracted netlist is inconsistent with the LVS netlist:\n" +
1422 '\n'.join(f" - {problem}" for problem in self._rcx25_netlist_problems))
1423 log_peak_memory('the netlist')
1425 # NOTE: the reduction reads the netlist as written, the checks above ran on the full one
1426 self._rcx25_reduced_netlist_path = None
1427 reduction_options = args.reduce_mode.options(ticer_tau=args.reduce_tau)
1428 if reduction_options is not None:
1429 rule('kpex/2.5D netlist reduction')
1430 reduction = reduce_spice_netlist(input_path=expanded_netlist_path,
1431 output_path=reduced_netlist_path(expanded_netlist_path),
1432 options=reduction_options)
1433 for scope in reduction.scopes:
1434 subproc(scope.describe())
1435 info(f"Wrote reduced netlist to: {reduction.output_path}")
1436 self._rcx25_reduced_netlist_path = reduction.output_path
1437 log_peak_memory('the netlist reduction')
1439 # NOTE: --out-netlist gets the reduced netlist with --reduce, in the --netlist-format,
1440 # which is converted from the SPICE netlist (as 'netlist convert' does)
1441 netlist_kind = 'reduced' if self._rcx25_reduced_netlist_path is not None else 'expanded'
1442 netlist_path = self._rcx25_reduced_netlist_path or expanded_netlist_path
1443 if args.netlist_format == NetlistFormat.VACASK:
1444 netlist_path = convert_to_vacask(
1445 input_path=netlist_path,
1446 output_path=os.path.splitext(netlist_path)[0] + NetlistFormat.VACASK.file_extension,
1447 subcircuits_with_m=tech_info.device_models.vacask_subcircuits_with_m)
1448 info(f"Wrote {netlist_kind} VACASK netlist to: {netlist_path}")
1449 self._rcx25_netlist_paths[NetlistFormat.VACASK] = netlist_path
1451 if args.output_netlist_path:
1452 shutil.copyfile(netlist_path, args.output_netlist_path)
1453 info(f"Copied {netlist_kind} {args.netlist_format.name} netlist to: "
1454 f"{args.output_netlist_path}")
1456 # NOTE: there was a KLayout bug that some of the categories were lost,
1457 # so that the marker browser could not load the report file
1458 try:
1459 report = rdb.ReportDatabase('')
1460 report.load(report_path) # try loading rdb
1461 except Exception as e:
1462 rule("Repair broken marker DB")
1463 warning(f"Detected KLayout bug: RDB can't be loaded due to exception {e}")
1464 repair_rdb(report_path)
1466 if expanded_netlist_path is not None and self._rcx25_netlist_problems and check_fails_run:
1467 raise RCNetlistCheckError("The extracted netlist is inconsistent with the LVS netlist "
1468 "(--check n to accept it):\n" +
1469 '\n'.join(f" - {problem}" for problem in self._rcx25_netlist_problems))
1471 return extraction_results
1473 def setup_logging(self,
1474 args: argparse.Namespace,
1475 exit_stack: contextlib.ExitStack):
1476 """
1477 :param exit_stack: the one of the run, which closes the log files when the run ends
1478 (also on errors, so that a later run in this process doesn't write into them)
1479 """
1480 def register_log_file_handler(log_path: str,
1481 formatter: Optional[logging.Formatter]) -> logging.Handler:
1482 handler = logging.FileHandler(log_path, encoding='utf-8')
1483 handler.setLevel(LogLevel.SUBPROCESS)
1484 if formatter:
1485 handler.setFormatter(formatter)
1486 register_additional_handler(handler)
1487 exit_stack.callback(close_log_file_handler, handler)
1488 return handler
1490 def close_log_file_handler(handler: logging.Handler):
1491 deregister_additional_handler(handler)
1492 handler.close()
1494 def reregister_log_file_handler(handler: logging.Handler,
1495 log_path: str,
1496 formatter: Optional[logging.Formatter]):
1497 close_log_file_handler(handler)
1498 os.makedirs(args.output_dir_path, exist_ok=True)
1499 new_path = os.path.join(args.output_dir_path, os.path.basename(log_path))
1500 if os.path.exists(new_path):
1501 ctime = os.path.getctime(new_path)
1502 dt = datetime.fromtimestamp(ctime)
1503 timestamp = dt.strftime('%Y-%m-%d_%H-%M-%S')
1504 backup_path = f"{new_path[:-4]}_{timestamp}.bak.log"
1505 shutil.move(new_path, backup_path)
1506 log_path = shutil.move(log_path, new_path)
1507 register_log_file_handler(log_path, formatter)
1509 # setup preliminary logger
1510 cli_log_path_plain = os.path.join(args.output_dir_base_path, f"kpex_plain.log")
1511 cli_log_path_formatted = os.path.join(args.output_dir_base_path, f"kpex.log")
1512 formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
1513 file_handler_plain = register_log_file_handler(cli_log_path_plain, None)
1514 file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter)
1515 try:
1516 self.validate_args(args)
1517 except ArgumentValidationError:
1518 if hasattr(args, 'output_dir_path'):
1519 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None)
1520 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter)
1521 sys.exit(1)
1522 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None)
1523 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter)
1525 set_log_level(args.log_level)
1527 @staticmethod
1528 def lvs_input_fingerprint(args: argparse.Namespace) -> LVSInputFingerprint:
1529 klayout_version = Tool.KLAYOUT.detect_version(args.klayout_exe_path)
1530 return lvs_input_fingerprint(pdk=str(args.pdk),
1531 klayout_version='' if klayout_version is None else str(klayout_version),
1532 lvs_script_path=args.lvs_script_path,
1533 script_parameters=LVSRunner.result_script_parameters(),
1534 gds_path=args.gds_path,
1535 cell_name=args.effective_cell_name,
1536 schematic_path=args.effective_schematic_path)
1538 def create_lvsdb(self, args: argparse.Namespace) -> kdb.LayoutVsSchematic:
1539 lvsdb = kdb.LayoutVsSchematic()
1541 match args.input_mode:
1542 case InputMode.LVSDB:
1543 lvsdb.read(args.lvsdb_path)
1544 case InputMode.GDS:
1545 lvs_log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_lvs.log")
1546 lvsdb_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}.lvsdb.gz")
1547 lvs_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_extracted.cir")
1548 lvs_cache_entry = LVSCacheEntry(
1549 dir_path=os.path.join(args.cache_dir_path, args.pdk,
1550 os.path.splitroot(os.path.abspath(args.gds_path))[-1]),
1551 cell_name=args.effective_cell_name
1552 )
1554 lvs_needed = True
1556 if args.cache_lvs:
1557 lvs_fingerprint = self.lvs_input_fingerprint(args)
1558 cache_miss_reasons = lvs_cache_entry.cache_miss_reasons(lvs_fingerprint)
1559 if cache_miss_reasons:
1560 info(f"Cache miss: {'; '.join(cache_miss_reasons)}")
1561 subproc(lvs_cache_entry.lvsdb_path)
1562 else:
1563 warning(f"Cache hit: Reusing cached LVSDB")
1564 subproc(lvs_cache_entry.lvsdb_path)
1565 shutil.copy(lvs_cache_entry.lvsdb_path, lvsdb_path)
1566 lvs_needed = False
1568 if lvs_needed:
1569 lvs_runner = LVSRunner()
1570 try:
1571 lvs_runner.run_klayout_lvs(exe_path=args.klayout_exe_path,
1572 lvs_script=args.lvs_script_path,
1573 gds_path=args.effective_gds_path,
1574 schematic_path=args.effective_schematic_path,
1575 log_path=lvs_log_path,
1576 lvsdb_path=lvsdb_path,
1577 netlist_path=lvs_netlist_path,
1578 verbose=args.klayout_lvs_verbose)
1579 except LVSError as e:
1580 error(str(e))
1581 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1582 if args.cache_lvs:
1583 lvs_cache_entry.store(lvsdb_path=lvsdb_path, fingerprint=lvs_fingerprint)
1585 lvsdb.read(lvsdb_path)
1586 return lvsdb
1588 SUBCOMMAND_SECTIONS: Dict[str, Tuple[str, ...]] = {
1589 'Extraction': ('extract',),
1590 'PEX25D': ('pex25d',),
1591 'Help': ('help',),
1592 }
1593 """How ``kpex --help`` groups the subcommands; every subcommand is in exactly one section."""
1595 SUBCOMMANDS = ('extract', 'pex25d', 'help')
1596 LEGACY_SUBCOMMAND = 'extract'
1598 @classmethod
1599 def normalize_argv(cls, argv: List[str]) -> List[str]:
1600 """
1601 Accept the pre-subcommand, flat command line.
1603 Before the verb layer, kpex was invoked as
1604 ``kpex --pdk sky130A --gds foo.gds --2.5D``. Every such run still works
1605 and means ``kpex extract <same options>``; it just warns. The two forms
1606 cannot be confused, because a subcommand name never begins with '-' and
1607 every legacy option does — so the fallback needs no heuristics and can
1608 never mistake one for the other.
1610 This exists for the scripts, CI jobs and docs that already say
1611 ``kpex --gds ...``, including the test suite. It is meant to be removed
1612 once those have moved on.
1613 """
1614 tail = argv[1:]
1615 if not tail:
1616 return argv
1617 first = tail[0]
1618 if first in ('-h', '--help', '-v', '--version'):
1619 return argv
1620 if not first.startswith('-'):
1621 return argv # a subcommand, or a typo argparse will report better than we can
1622 if any(arg in cls.SUBCOMMANDS for arg in tail):
1623 # A subcommand is present, just not first — most likely a top-level
1624 # option that now belongs to the subcommand. Leave it alone and let
1625 # argparse say so, rather than silently rewriting it into something
1626 # that fails somewhere else.
1627 return argv
1628 warning(f"Calling {PROGRAM_NAME} without a subcommand is deprecated; "
1629 f"treating this run as '{PROGRAM_NAME} {cls.LEGACY_SUBCOMMAND} …'. "
1630 f"See '{PROGRAM_NAME} --help' for the available subcommands.")
1631 return [argv[0], cls.LEGACY_SUBCOMMAND] + tail
1633 @staticmethod
1634 def _writes_artifact_to_stdout(argv: List[str]) -> bool:
1635 """
1636 Whether this invocation sends a PEX25D artifact to stdout.
1638 Answered from the raw argv rather than from parsed arguments, because
1639 the redirect has to be in place before anything — argument validation
1640 included — has a chance to print.
1641 """
1642 # NOTE: also by the former names of the options
1643 return any(previous in ('--out-file', '--out-scene', '--output_file', '--output_scene') and current == '-'
1644 for previous, current in zip(argv, argv[1:]))
1646 def main(self, argv: List[str]):
1647 env = Env.from_os_environ()
1648 argv = self.normalize_argv(argv)
1650 # A PEX25D artifact may be destined for stdout, and in that case stdout
1651 # belongs to the artifact alone — one stray log line and whatever is on
1652 # the other end of the pipe is parsing garbage. Redirecting sys.stdout to
1653 # stderr for the whole run is blunt but total: the rich console resolves
1654 # sys.stdout at write time, so every rule()/info()/subproc() follows,
1655 # including ones written by code that never considered being in a
1656 # pipeline. Artifact I/O uses sys.__stdout__ and is unaffected.
1657 writes_to_stdout = self._writes_artifact_to_stdout(argv)
1659 with contextlib.ExitStack() as stack:
1660 if writes_to_stdout:
1661 stack.enter_context(contextlib.redirect_stdout(sys.stderr))
1663 if not ({'-v', '--version', '-h', '--help'} & set(argv)) and argv[1:2] != ['help']:
1664 rule('Command line arguments')
1665 subproc(' '.join(map(shlex.quote, argv)))
1667 args = self.parse_args(arg_list=argv[1:], env=env)
1669 os.makedirs(args.output_dir_base_path, exist_ok=True)
1670 self.setup_logging(args, stack)
1671 warn_deprecated_options(args)
1673 try:
1674 tech_info = TechInfo.from_json(args.tech_pbjson_path,
1675 dielectric_filter=args.dielectric_filter)
1676 except TechDefError as e:
1677 error(str(e))
1678 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1680 try:
1681 match args.command:
1682 case 'pex25d':
1683 self.run_pex25d_generation(args=args, tech_info=tech_info)
1684 case _:
1685 try:
1686 self.run_extraction(args=args, tech_info=tech_info)
1687 except (LVSDBError, RExtractionTechError, CExtractionTechError, DeviceModelError,
1688 RCNetlistCheckError, SpiceReaderError, VacaskWriterError, FasterCapError,
1689 ExclusionError) as e:
1690 error(str(e))
1691 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1692 finally:
1693 # NOTE: also of a failed run, e.g. to size the next one
1694 log_peak_memory('the run')
1696 def run_pex25d_generation(self,
1697 args: argparse.Namespace,
1698 tech_info: TechInfo):
1699 """
1700 The dump path: LVS for connectivity, then PEX25D out, and stop.
1702 Deliberately does not touch any engine. Everything downstream of the
1703 artifacts written here — validating them, re-encoding them, exporting
1704 them to a solver's native input — is the standalone ``pex25d`` tool's
1705 job, and needs neither a layout nor KLayout to do it.
1706 """
1707 from .klayout.pex25d_builder import BuildError, BuilderOptions, build_pex25d_file
1708 from .pex25d.codec import save_artifact
1709 from .pex25d.resolver import ResolveError, resolve
1710 from .pex25d.validator import validate
1712 report = DiagnosticsReport(warnings_are_errors=args.warnings_are_errors)
1714 rule('Prepare LVSDB')
1715 lvsdb = self.create_lvsdb(args)
1717 pex_context = KLayoutExtractionContext.prepare_extraction(
1718 top_cell=args.effective_cell_name,
1719 lvsdb=lvsdb,
1720 tech=tech_info,
1721 blackbox_devices=args.blackbox_devices)
1723 rule('Non-empty layers in LVS database')
1724 for gds_pair, layer_info in pex_context.extracted_layers.items():
1725 names = [l.lvs_layer_name for l in layer_info.source_layers]
1726 info(f"{gds_pair} -> ({' '.join(names)})")
1728 rule('PEX25D Generation')
1729 try:
1730 pex25d_file = build_pex25d_file(
1731 pex_context=pex_context,
1732 tech_info=tech_info,
1733 cell_name=args.effective_cell_name,
1734 options=BuilderOptions(
1735 grid_um=args.pex25d_grid,
1736 with_source_refs=args.pex25d_with_source_refs,
1737 dielectric_filter=args.dielectric_filter,
1738 domain_margin_um=args.pex25d_domain_margin))
1740 if args.pex25d_file_spec is not None:
1741 save_artifact(pex25d_file, args.pex25d_file_spec,
1742 comments=args.pex25d_comments)
1743 info(f"Wrote {args.pex25d_file_spec}")
1745 scene = None
1746 if args.pex25d_scene_spec is not None:
1747 # The geometric tier belongs to whichever of the two runs it
1748 # once: the validator when it is on, the resolver otherwise.
1749 scene = resolve(pex25d_file, report=report,
1750 strict=args.strict and not args.pex25d_validate)
1751 save_artifact(scene, args.pex25d_scene_spec,
1752 comments=args.pex25d_comments)
1753 info(f"Wrote {args.pex25d_scene_spec}")
1755 if args.pex25d_validate:
1756 validate(pex25d_file, report=report, strict=args.strict,
1757 scene=scene)
1759 except BuildError as e:
1760 error(f"Could not generate PEX25D: {e}")
1761 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1762 except ResolveError as e:
1763 # The scene is not written: the diagnostics say why, and half a
1764 # scene is worse than none.
1765 error(str(e))
1766 report.render(args.diagnostics_format)
1767 sys.exit(ExitCode.DIAGNOSTIC_ERRORS)
1768 except NotImplementedError as e:
1769 error(f"Not implemented yet: {e}")
1770 sys.exit(ExitCode.NOT_IMPLEMENTED)
1772 writes_to_stdout = any(spec is not None and spec.is_stdio
1773 for spec in (args.pex25d_file_spec, args.pex25d_scene_spec))
1774 with diagnostics_stream(writes_to_stdout=writes_to_stdout) as stream:
1775 report.render(args.diagnostics_format, stream=stream)
1777 if report.exit_code != ExitCode.OK:
1778 sys.exit(report.exit_code)
1780 def run_extraction(self,
1781 args: argparse.Namespace,
1782 tech_info: TechInfo):
1783 if args.halo is not None:
1784 tech_info.tech.process_parasitics.side_halo = args.halo
1786 if args.exclusion_marker_mode != ExclusionMarkerMode.IGNORE:
1787 stack_layer_names = set(tech_info.canonical_layer_name_by_gds_pair.values())
1788 args.selected_marker_layers = selected_marker_layers(tech_info.tech.exclusion_markers,
1789 stack_layer_names,
1790 args.exclusion_marker_layers)
1791 if not args.selected_marker_layers:
1792 raise ExclusionError(f"--exclusion-markers {args.exclusion_marker_mode}: the tech info of {args.pdk} "
1793 f"has no exclusion markers, give them with --exclusion-marker-layer")
1795 if args.run_magic:
1796 rule('MAGIC')
1797 self.run_magic_extraction(args)
1798 log_peak_memory('MAGIC')
1800 # no need to run LVS etc if only running magic engine
1801 if not (args.run_fastcap or args.run_fastercap or args.run_2_5D):
1802 return
1804 rule('Prepare LVSDB')
1805 lvsdb = self.create_lvsdb(args)
1807 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=args.effective_cell_name,
1808 lvsdb=lvsdb,
1809 tech=tech_info,
1810 blackbox_devices=args.blackbox_devices)
1811 log_peak_memory('preparing the LVSDB')
1812 rule('Non-empty layers in LVS database')
1813 for gds_pair, layer_info in pex_context.extracted_layers.items():
1814 names = [l.lvs_layer_name for l in layer_info.source_layers]
1815 info(f"{gds_pair} -> ({' '.join(names)})")
1817 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_extracted.oas")
1818 pex_context.annotated_layout.write(gds_path)
1820 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_internal.oas")
1821 pex_context.lvsdb.internal_layout().write(gds_path)
1823 def dump_layers(cell: str,
1824 layers: List[KLayoutExtractedLayerInfo],
1825 layout_dump_path: str):
1826 layout = kdb.Layout()
1827 layout.dbu = lvsdb.internal_layout().dbu
1829 top_cell = layout.create_cell(cell)
1830 for ulyr in layers:
1831 li = kdb.LayerInfo(*ulyr.gds_pair)
1832 li.name = ulyr.lvs_layer_name
1833 layer = layout.insert_layer(li)
1834 layout.insert(top_cell.cell_index(), layer, ulyr.region.dup())
1836 write_output_file(layout.write, layout_dump_path)
1838 if len(pex_context.unnamed_layers) >= 1:
1839 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_unnamed_LVS_layers.gds.gz")
1840 dump_layers(cell=args.effective_cell_name,
1841 layers=pex_context.unnamed_layers,
1842 layout_dump_path=layout_dump_path)
1844 if len(pex_context.extracted_layers) >= 1:
1845 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_nonempty_LVS_layers.gds.gz")
1846 nonempty_layers = [l \
1847 for layers in pex_context.extracted_layers.values() \
1848 for l in layers.source_layers]
1849 dump_layers(cell=args.effective_cell_name,
1850 layers=nonempty_layers,
1851 layout_dump_path=layout_dump_path)
1852 else:
1853 error("No extracted layers found")
1854 sys.exit(1)
1856 exclusions = self.exclusions_of(args, pex_context, tech_info)
1858 if args.run_fastcap or args.run_fastercap:
1859 if exclusions is not None:
1860 warn_of_ports_without_nodes(exclusions, "The netlist of a field solver")
1861 lst_file = self.build_fastercap_input(args=args,
1862 pex_context=pex_context,
1863 tech_info=tech_info,
1864 exclusions=exclusions)
1865 log_peak_memory('the FasterCap input')
1866 if args.run_fastercap:
1867 self.run_fastercap_extraction(args=args,
1868 pex_context=pex_context,
1869 lst_file=lst_file,
1870 exclusions=exclusions)
1871 log_peak_memory('FasterCap')
1872 if args.run_fastcap:
1873 self.run_fastcap_extraction(args=args,
1874 pex_context=pex_context,
1875 lst_file=lst_file,
1876 exclusions=exclusions)
1877 log_peak_memory('FastCap2')
1879 if args.run_2_5D:
1880 rule("kpex/2.5D PEX Engine")
1881 report_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_k25d_pex_report.rdb.gz")
1882 netlist_csv_path = os.path.abspath(os.path.join(args.output_dir_path,
1883 f"{args.effective_cell_name}_k25d_pex_netlist.csv"))
1884 netlist_spice_path = os.path.abspath(os.path.join(args.output_dir_path,
1885 f"{args.effective_cell_name}_k25d_pex_netlist.spice"))
1887 self._rcx25_extraction_results = self.run_kpex_2_5d_engine( # NOTE: store for test case
1888 args=args,
1889 pex_context=pex_context,
1890 tech_info=tech_info,
1891 report_path=report_path,
1892 netlist_csv_path=netlist_csv_path,
1893 expanded_netlist_path=netlist_spice_path,
1894 exclusions=exclusions
1895 )
1897 self._rcx25_extracted_csv_path = netlist_csv_path
1899 @property
1900 def rcx25_extraction_results(self) -> ExtractionResults:
1901 if not hasattr(self, '_rcx25_extraction_results'):
1902 raise Exception('rcx25_extraction_results is not initialized, was run_kpex_2_5d_engine called?')
1903 return self._rcx25_extraction_results
1905 @property
1906 def rcx25_extracted_csv_path(self) -> str:
1907 if not hasattr(self, '_rcx25_extracted_csv_path'):
1908 raise Exception('rcx25_extracted_csv_path is not initialized, was run_kpex_2_5d_engine called?')
1909 return self._rcx25_extracted_csv_path
1911 @property
1912 def rcx25_reduced_netlist_path(self) -> Optional[str]:
1913 """The reduced netlist the 2.5D engine wrote (--reduce), None without reduction"""
1914 if not hasattr(self, '_rcx25_reduced_netlist_path'):
1915 raise Exception('rcx25_reduced_netlist_path is not initialized, was run_kpex_2_5d_engine called?')
1916 return self._rcx25_reduced_netlist_path
1918 @property
1919 def rcx25_netlist_paths(self) -> Dict[NetlistFormat, str]:
1920 """
1921 The paths of the netlists the 2.5D engine wrote into the run directory, by format:
1922 the SPICE netlist, and the one converted to the --netlist-format (of the reduced netlist with --reduce)
1923 """
1924 if not hasattr(self, '_rcx25_netlist_paths'):
1925 raise Exception('rcx25_netlist_paths is not initialized, was run_kpex_2_5d_engine called?')
1926 return self._rcx25_netlist_paths
1928 @property
1929 def rcx25_netlist_problems(self) -> List[str]:
1930 if not hasattr(self, '_rcx25_netlist_problems'):
1931 raise Exception('rcx25_netlist_problems is not initialized, was run_kpex_2_5d_engine called?')
1932 return self._rcx25_netlist_problems
1934 @property
1935 def fastercap_extracted_csv_path(self) -> str:
1936 if not hasattr(self, '_fastercap_extracted_csv_path'):
1937 raise Exception('fastercap_extracted_csv_path is not initialized, was run_fastercap_extraction called?')
1938 return self._fastercap_extracted_csv_path
1941if __name__ == "__main__":
1942 cli = KpexCLI()
1943 cli.main(sys.argv)