Coverage for klayout_pex/kpex_cli.py: 71%
537 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 12:43 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 12:43 +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
27from datetime import datetime
28from enum import StrEnum
29import logging
30import os
31import os.path
32from pathlib import Path
33import rich.console
34import rich.markdown
35import rich.text
36from rich_argparse import RichHelpFormatter
37import shlex
38import shutil
39import sys
40from typing import *
42import klayout.db as kdb
43import klayout.rdb as rdb
45from .common.path_validation import validate_files, FileValidationResult
46from .env import EnvVar, Env
47from .extraction_engine import ExtractionEngine
48from .fastercap.fastercap_input_builder import FasterCapInputBuilder
49from .fastercap.fastercap_model_generator import FasterCapModelGenerator
50from .fastercap.fastercap_runner import run_fastercap, fastercap_parse_capacitance_matrix
51from .fastcap.fastcap_runner import run_fastcap, fastcap_parse_capacitance_matrix
52from .klayout.lvs_runner import LVSRunner
53from .klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo
54from .klayout.netlist_expander import NetlistExpander
55from .klayout.netlist_csv import NetlistCSVWriter
56from .klayout.netlist_printer import NetlistPrinter
57from .klayout.netlist_reducer import NetlistReducer
58from .klayout.repair_rdb import repair_rdb
59from .log import (
60 LogLevel,
61 set_log_level,
62 register_additional_handler,
63 deregister_additional_handler,
64 # console,
65 # debug,
66 info,
67 warning,
68 subproc,
69 error,
70 rule
71)
72from .magic.magic_ext_file_parser import parse_magic_pex_run
73from .magic.magic_runner import (
74 MagicPEXMode,
75 MagicShortMode,
76 MagicMergeMode,
77 run_magic,
78 prepare_magic_script,
79)
80from .magic.magic_log_analyzer import MagicLogAnalyzer
81from .pdk_config import PDK, PDKConfig
82from .rcx25.extractor import RCX25Extractor, ExtractionResults
83from .rcx25.netlist_expander import RCX25NetlistExpander
84from .rcx25.pex_mode import PEXMode
85from .tech_info import TechInfo
86from .util.multiple_choice import MultipleChoicePattern
87from .util.argparse_helpers import render_enum_help, true_or_false
88from .version import __version__
91# ------------------------------------------------------------------------------------
93PROGRAM_NAME = "kpex"
96class ArgumentValidationError(Exception):
97 pass
100class InputMode(StrEnum):
101 LVSDB = "lvsdb"
102 GDS = "gds"
105class KpexCLI:
106 @staticmethod
107 def parse_args(arg_list: List[str],
108 env: Env) -> argparse.Namespace:
109 # epilog = f"See '{PROGRAM_NAME} <subcommand> -h' for help on subcommand"
110 epilog = EnvVar.help_epilog_table()
111 epilog_md = rich.console.Group(
112 rich.text.Text('Environmental variables:', style='argparse.groups'),
113 rich.markdown.Markdown(epilog, style='argparse.text')
114 )
115 main_parser = argparse.ArgumentParser(description=f"{PROGRAM_NAME}: "
116 f"KLayout-integrated Parasitic Extraction Tool",
117 epilog=epilog_md,
118 add_help=False,
119 formatter_class=RichHelpFormatter)
121 group_special = main_parser.add_argument_group("Special options")
122 group_special.add_argument("--help", "-h", action='help', help="show this help message and exit")
123 group_special.add_argument("--version", "-v", action='version', version=f'{PROGRAM_NAME} {__version__}')
124 group_special.add_argument("--log_level", dest='log_level', default='subprocess',
125 help=render_enum_help(topic='log_level', enum_cls=LogLevel))
126 group_special.add_argument("--threads", dest='num_threads', type=int,
127 default=os.cpu_count() * 4,
128 help="number of threads (e.g. for FasterCap) (default is %(default)s)")
130 group_pex = main_parser.add_argument_group("Parasitic Extraction Setup")
132 all_pdk_choices = list(PDK) + list(PDK.legacy_aliases().keys())
134 default_pdk = env.default_pdk
135 pdk_help = render_enum_help(topic='pdk', enum_cls=PDK, print_default=False)
136 if default_pdk:
137 pdk_help += f" (default is '{default_pdk}')"
139 group_pex.add_argument("--pdk", dest="pdk", required=default_pdk is None,
140 type=PDK.from_string, choices=all_pdk_choices,
141 help=pdk_help, default=default_pdk)
143 group_pex.add_argument("--out_dir", dest="output_dir_base_path", default="output",
144 help="Run directory path (default is '%(default)s')")
146 group_pex.add_argument("--out_spice", "-o", dest="output_spice_path", default=None,
147 help="Optional additional SPICE output path (default is none)")
149 group_pex_input = main_parser.add_argument_group("Parasitic Extraction Input",
150 description="Either LVS is run, or an existing LVSDB is used")
151 group_pex_input.add_argument("--gds", "-g", dest="gds_path", default=None,
152 help="GDS path (for LVS)")
153 group_pex_input.add_argument("--schematic", "-s", dest="schematic_path",
154 help="Schematic SPICE netlist path (for LVS). "
155 "If none given, a dummy schematic will be created")
156 group_pex_input.add_argument("--lvsdb", "-l", dest="lvsdb_path", default=None,
157 help="KLayout LVSDB path (bypass LVS)")
158 group_pex_input.add_argument("--cell", "-c", dest="cell_name", default=None,
159 help="Cell (default is the top cell)")
161 group_pex_input.add_argument("--cache-lvs", dest="cache_lvs",
162 type=true_or_false, default=True,
163 help="Used cached LVSDB (for given input GDS) (default is %(default)s)")
164 group_pex_input.add_argument("--cache-dir", dest="cache_dir_path", default=None,
165 help="Path for cached LVSDB (default is .kpex_cache within --out_dir)")
166 group_pex_input.add_argument("--lvs-verbose", dest="klayout_lvs_verbose",
167 type=true_or_false, default=False,
168 help="Verbose KLayout LVS output (default is %(default)s)")
170 group_pex_options = main_parser.add_argument_group("Parasitic Extraction Options")
171 group_pex_options.add_argument("--blackbox", dest="blackbox_devices",
172 type=true_or_false, default=False, # TODO: in the future this should be True by default
173 help="Blackbox devices like MIM/MOM caps, as they are handled by SPICE models "
174 "(default is %(default)s for testing now)")
175 group_pex_options.add_argument("--fastercap", dest="run_fastercap",
176 action='store_true', default=False,
177 help="Run FasterCap engine (default is %(default)s)")
178 group_pex_options.add_argument("--fastcap", dest="run_fastcap",
179 action='store_true', default=False,
180 help="Run FastCap2 engine (default is %(default)s)")
181 group_pex_options.add_argument("--magic", dest="run_magic",
182 action='store_true', default=False,
183 help="Run MAGIC engine (default is %(default)s)")
184 group_pex_options.add_argument("--2.5D", dest="run_2_5D",
185 action='store_true', default=False,
186 help="Run 2.5D analytical engine (default is %(default)s)")
188 group_fastercap = main_parser.add_argument_group("FasterCap options")
189 group_fastercap.add_argument("--k_void", "-k", dest="k_void",
190 type=float, default=3.9,
191 help="Dielectric constant of void (default is %(default)s)")
193 # TODO: reflect that these are also now used by KPEX/2.5D engine!
194 group_fastercap.add_argument("--delaunay_amax", "-a", dest="delaunay_amax",
195 type=float, default=50,
196 help="Delaunay triangulation maximum area (default is %(default)s)")
197 group_fastercap.add_argument("--delaunay_b", "-b", dest="delaunay_b",
198 type=float, default=0.5,
199 help="Delaunay triangulation b (default is %(default)s)")
200 group_fastercap.add_argument("--geo_check", dest="geometry_check",
201 type=true_or_false, default=False,
202 help=f"Validate geometries before passing to FasterCap "
203 f"(default is False)")
204 group_fastercap.add_argument("--diel", dest="dielectric_filter",
205 type=str, default="all",
206 help=f"Comma separated list of dielectric filter patterns. "
207 f"Allowed patterns are: (none, all, -dielname1, +dielname2) "
208 f"(default is %(default)s)")
210 group_fastercap.add_argument("--tolerance", dest="fastercap_tolerance",
211 type=float, default=0.05,
212 help="FasterCap -aX error tolerance (default is %(default)s)")
213 group_fastercap.add_argument("--d_coeff", dest="fastercap_d_coeff",
214 type=float, default=0.5,
215 help=f"FasterCap -d direct potential interaction coefficient to mesh refinement "
216 f"(default is %(default)s)")
217 group_fastercap.add_argument("--mesh", dest="fastercap_mesh_refinement_value",
218 type=float, default=0.5,
219 help="FasterCap -m Mesh relative refinement value (default is %(default)s)")
220 group_fastercap.add_argument("--ooc", dest="fastercap_ooc_condition",
221 type=float, default=2,
222 help="FasterCap -f out-of-core free memory to link memory condition "
223 "(0 = don't go OOC, default is %(default)s)")
224 group_fastercap.add_argument("--auto_precond", dest="fastercap_auto_preconditioner",
225 type=true_or_false, default=True,
226 help=f"FasterCap -ap Automatic preconditioner usage (default is %(default)s)")
227 group_fastercap.add_argument("--galerkin", dest="fastercap_galerkin_scheme",
228 action='store_true', default=False,
229 help=f"FasterCap -g Use Galerkin scheme (default is %(default)s)")
230 group_fastercap.add_argument("--jacobi", dest="fastercap_jacobi_preconditioner",
231 action='store_true', default=False,
232 help="FasterCap -pj Use Jacobi preconditioner (default is %(default)s)")
234 group_magic = main_parser.add_argument_group("MAGIC options")
236 default_magicrc_path = env.default_magicrc_path
237 if default_magicrc_path:
238 magicrc_help = f"Path to magicrc configuration file (default is '{default_magicrc_path}')"
239 else:
240 magicrc_help = "Path to magicrc configuration file "\
241 "(default not available, PDK and PDK_ROOT must be set!)"
243 group_magic.add_argument('--magicrc', dest='magicrc_path', default=default_magicrc_path,
244 help=magicrc_help)
245 group_magic.add_argument("--magic_mode", dest='magic_pex_mode',
246 default=MagicPEXMode.DEFAULT, type=MagicPEXMode, choices=list(MagicPEXMode),
247 help=render_enum_help(topic='magic_mode', enum_cls=MagicPEXMode))
248 group_magic.add_argument("--magic_cthresh", dest="magic_cthresh",
249 type=float, default=0.01,
250 help="Threshold (in fF) for ignored parasitic capacitances (default is %(default)s). "
251 "(MAGIC command: ext2spice cthresh <value>)")
252 group_magic.add_argument("--magic_rthresh", dest="magic_rthresh",
253 type=int, default=100,
254 help="Threshold (in Ω) for ignored parasitic resistances (default is %(default)s). "
255 "(MAGIC command: ext2spice rthresh <value>)")
256 group_magic.add_argument("--magic_tolerance", dest="magic_tolerance",
257 type=float, default=1,
258 help="Set ratio between resistor and device tolerance (default is %(default)s). "
259 "(MAGIC command: extresist tolerance <value>)")
260 group_magic.add_argument("--magic_halo", dest="magic_halo",
261 type=float, default=None,
262 help="Custom sidewall halo distance (in µm) "
263 "(MAGIC command: extract halo <value>) (default is no custom halo)")
264 group_magic.add_argument("--magic_short", dest='magic_short_mode',
265 default=MagicShortMode.DEFAULT, type=MagicShortMode, choices=list(MagicShortMode),
266 help=render_enum_help(topic='magic_short', enum_cls=MagicShortMode))
267 group_magic.add_argument("--magic_merge", dest='magic_merge_mode',
268 default=MagicMergeMode.DEFAULT, type=MagicMergeMode, choices=list(MagicMergeMode),
269 help=render_enum_help(topic='magic_merge', enum_cls=MagicMergeMode))
271 group_25d = main_parser.add_argument_group("2.5D options")
272 group_25d.add_argument("--mode", dest='pex_mode',
273 default=PEXMode.DEFAULT, type=PEXMode, choices=list(PEXMode),
274 help=render_enum_help(topic='mode', enum_cls=PEXMode))
275 group_25d.add_argument("--halo", dest="halo",
276 type=float, default=None,
277 help="Custom sidewall halo distance (in µm) to override tech info "
278 "(default is no custom halo)")
279 group_25d.add_argument("--scale", dest="scale_ratio_to_fit_halo",
280 type=true_or_false, default=True,
281 help=f"Scale fringe ratios, so that halo distance is 100%% (default is %(default)s)")
283 if arg_list is None:
284 arg_list = sys.argv[1:]
285 args = main_parser.parse_args(arg_list)
287 # environmental variables and their defaults
288 args.fastcap_exe_path = env[EnvVar.FASTCAP_EXE]
289 args.fastercap_exe_path = env[EnvVar.FASTERCAP_EXE]
290 args.klayout_exe_path = env[EnvVar.KLAYOUT_EXE]
291 args.magic_exe_path = env[EnvVar.MAGIC_EXE]
293 return args
295 @staticmethod
296 def validate_args(args: argparse.Namespace):
297 found_errors = False
299 pdk_config: PDKConfig = args.pdk.config
300 args.tech_pbjson_path = pdk_config.tech_pb_json_path
301 args.lvs_script_path = pdk_config.pex_lvs_script_path
303 def input_file_stem(path: str):
304 # could be *.gds, or *.gds.gz, so remove all extensions
305 return os.path.basename(path).split(sep='.')[0]
307 if not os.path.isfile(args.klayout_exe_path):
308 path = shutil.which(args.klayout_exe_path)
309 if not path:
310 error(f"Can't locate KLayout executable at {args.klayout_exe_path}")
311 found_errors = True
313 if not os.path.isfile(args.tech_pbjson_path):
314 error(f"Can't read technology file at path {args.tech_pbjson_path}")
315 found_errors = True
317 if not os.path.isfile(args.lvs_script_path):
318 error(f"Can't locate LVS script path at {args.lvs_script_path}")
319 found_errors = True
321 rule('Input Layout')
323 # check engines VS input possiblities
324 match (args.run_magic, args.run_fastcap, args.run_fastercap, args.run_2_5D,
325 args.gds_path, args.lvsdb_path):
326 case (True, _, _, _, None, _):
327 error(f"Running PEX engine MAGIC requires --gds (--lvsdb not possible)")
328 found_errors = True
329 case (False, False, False, False, _, _): # at least one engine must be activated
330 error("No PEX engines activated")
331 engine_help = """
332 | Argument | Description |
333 | ------------ | ------------------------------- |
334 | --2.5D | Run KPEX/2.5D analytical engine |
335 | --fastercap | Run KPEX/FastCap 3D engine |
336 | --fastercap | Run KPEX/FasterCap 3D engine |
337 | --magic | Run MAGIC wrapper engine |
338 """
339 subproc(f"\n\nPlease activate one or more engines using the arguments:")
340 rich.print(rich.markdown.Markdown(engine_help, style='argparse.text'))
341 found_errors = True
342 case (_, _, _, _, None, None):
343 error(f"Neither GDS nor LVSDB was provided")
344 found_errors = True
346 # check if we find magicrc
347 if args.run_magic:
348 if args.magicrc_path is None:
349 error(f"magicrc not available, requires any those:\n"
350 f"\t• set environmental variables PDK_ROOT / PDK\n"
351 f"\t• pass argument --magicrc")
352 found_errors = True
353 else:
354 result = validate_files([args.magicrc_path])
355 for f in result.failures:
356 error(f"Invalid magicrc: {f.reason} at {str(f.path)}")
357 found_errors = True
359 # input mode: LVS or existing LVSDB?
360 if args.gds_path:
361 info(f"GDS input file passed, running in LVS mode")
362 args.input_mode = InputMode.GDS
363 if not os.path.isfile(args.gds_path):
364 error(f"Can't read GDS file (LVS input) at path {args.gds_path}")
365 found_errors = True
366 else:
367 args.layout = kdb.Layout()
368 args.layout.read(args.gds_path)
370 top_cells = args.layout.top_cells()
372 if args.cell_name: # explicit user-specified cell name
373 args.effective_cell_name = args.cell_name
375 found_cell: Optional[kdb.Cell] = None
376 for cell in args.layout.cells('*'):
377 if cell.name == args.effective_cell_name:
378 found_cell = cell
379 break
380 if not found_cell:
381 error(f"Could not find cell {args.cell_name} in GDS {args.gds_path}")
382 found_errors = True
384 is_only_top_cell = len(top_cells) == 1 and top_cells[0].name == args.cell_name
385 if is_only_top_cell:
386 info(f"Found cell {args.cell_name} in GDS {args.gds_path} (only top cell)")
387 else: # there are other cells => extract the top cell to a tmp layout
388 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}"
389 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id)
390 os.makedirs(args.output_dir_path, exist_ok=True)
391 args.effective_gds_path = os.path.join(args.output_dir_path,
392 f"{args.cell_name}_exported.gds.gz")
393 info(f"Found cell {args.cell_name} in GDS {args.gds_path}, "
394 f"but it is not the only top cell, "
395 f"so layout is exported to: {args.effective_gds_path}")
397 found_cell.write(args.effective_gds_path)
398 else: # find top cell
399 if len(top_cells) == 1:
400 args.effective_cell_name = top_cells[0].name
401 info(f"No explicit top cell specified, using top cell '{args.effective_cell_name}'")
402 else:
403 args.effective_cell_name = 'TOP'
404 error(f"Could not determine the default top cell in GDS {args.gds_path}, "
405 f"there are multiple: {', '.join([c.name for c in top_cells])}. "
406 f"Use --cell to specify the cell")
407 found_errors = True
409 if not hasattr(args, 'effective_gds_path'):
410 args.effective_gds_path = args.gds_path
411 elif args.lvsdb_path is not None:
412 info(f"LVSDB input file passed, bypassing LVS")
413 args.input_mode = InputMode.LVSDB
414 if not os.path.isfile(args.lvsdb_path):
415 error(f"Can't read KLayout LVSDB file at path {args.lvsdb_path}")
416 found_errors = True
417 else:
418 lvsdb = kdb.LayoutVsSchematic()
419 lvsdb.read(args.lvsdb_path)
420 top_cell: kdb.Cell = lvsdb.internal_top_cell()
421 args.effective_cell_name = top_cell.name
423 if hasattr(args, 'effective_cell_name'):
424 run_dir_id: str
425 match args.input_mode:
426 case InputMode.GDS:
427 run_dir_id = f"{input_file_stem(args.gds_path)}__{args.effective_cell_name}"
428 case InputMode.LVSDB:
429 run_dir_id = f"{input_file_stem(args.lvsdb_path)}__{args.effective_cell_name}"
430 case _:
431 raise NotImplementedError(f"Unknown input mode {args.input_mode}")
433 args.output_dir_path = os.path.join(args.output_dir_base_path, run_dir_id)
434 os.makedirs(args.output_dir_path, exist_ok=True)
435 if args.input_mode == InputMode.GDS:
436 if args.schematic_path:
437 args.effective_schematic_path = args.schematic_path
438 if not os.path.isfile(args.schematic_path):
439 error(f"Can't read schematic (LVS input) at path {args.schematic_path}")
440 found_errors = True
441 else:
442 info(f"LVS input schematic not specified (argument --schematic), using dummy schematic")
443 args.effective_schematic_path = os.path.join(args.output_dir_path,
444 f"{args.effective_cell_name}_dummy_schematic.spice")
445 with open(args.effective_schematic_path, 'w', encoding='utf-8') as f:
446 f.writelines([
447 f".subckt {args.effective_cell_name} VDD VSS\n",
448 '.ends\n',
449 '.end\n'
450 ])
452 try:
453 args.log_level = LogLevel[args.log_level.upper()]
454 except KeyError:
455 error(f"Requested log level {args.log_level.lower()} does not exist, "
456 f"{render_enum_help(topic='log_level', enum_cls=LogLevel, print_default=False)}")
457 found_errors = True
459 try:
460 pattern_string: str = args.dielectric_filter
461 args.dielectric_filter = MultipleChoicePattern(pattern=pattern_string)
462 except ValueError as e:
463 error("Failed to parse --diel arg", e)
464 found_errors = True
466 if args.cache_dir_path is None:
467 args.cache_dir_path = os.path.join(args.output_dir_base_path, '.kpex_cache')
469 if found_errors:
470 raise ArgumentValidationError("Argument validation failed")
472 def create_netlist_printer(self,
473 args: argparse.Namespace,
474 extraction_engine: ExtractionEngine):
475 printer = NetlistPrinter(extraction_engine=extraction_engine,
476 pdk=args.pdk)
477 return printer
479 def build_fastercap_input(self,
480 args: argparse.Namespace,
481 pex_context: KLayoutExtractionContext,
482 tech_info: TechInfo) -> str:
483 rule('Process stackup')
484 fastercap_input_builder = FasterCapInputBuilder(pex_context=pex_context,
485 tech_info=tech_info,
486 k_void=args.k_void,
487 delaunay_amax=args.delaunay_amax,
488 delaunay_b=args.delaunay_b)
489 gen: FasterCapModelGenerator = fastercap_input_builder.build()
491 rule('FasterCap Input File Generation')
492 faster_cap_input_dir_path = os.path.join(args.output_dir_path, 'FasterCap_Input_Files')
493 os.makedirs(faster_cap_input_dir_path, exist_ok=True)
495 lst_file = gen.write_fastcap(output_dir_path=faster_cap_input_dir_path, prefix='FasterCap_Input_')
497 rule('STL File Generation')
498 geometry_dir_path = os.path.join(args.output_dir_path, 'Geometries')
499 os.makedirs(geometry_dir_path, exist_ok=True)
500 gen.dump_stl(output_dir_path=geometry_dir_path, prefix='')
502 if args.geometry_check:
503 rule('Geometry Validation')
504 gen.check()
506 return lst_file
509 def run_fastercap_extraction(self,
510 args: argparse.Namespace,
511 pex_context: KLayoutExtractionContext,
512 lst_file: str):
513 rule('FasterCap Execution')
514 info(f"Configure number of OpenMP threads (environmental variable OMP_NUM_THREADS) as {args.num_threads}")
515 os.environ['OMP_NUM_THREADS'] = f"{args.num_threads}"
517 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Output.txt")
518 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Raw.csv")
519 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Result_Matrix_Avg.csv")
520 expanded_netlist_path = os.path.join(args.output_dir_path,
521 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.cir")
522 expanded_netlist_csv_path = os.path.join(args.output_dir_path,
523 f"{args.effective_cell_name}_FasterCap_Expanded_Netlist.csv")
524 reduced_netlist_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FasterCap_Reduced_Netlist.cir")
526 run_fastercap(exe_path=args.fastercap_exe_path,
527 lst_file_path=lst_file,
528 log_path=log_path,
529 tolerance=args.fastercap_tolerance,
530 d_coeff=args.fastercap_d_coeff,
531 mesh_refinement_value=args.fastercap_mesh_refinement_value,
532 ooc_condition=args.fastercap_ooc_condition,
533 auto_preconditioner=args.fastercap_auto_preconditioner,
534 galerkin_scheme=args.fastercap_galerkin_scheme,
535 jacobi_preconditioner=args.fastercap_jacobi_preconditioner)
537 cap_matrix = fastercap_parse_capacitance_matrix(log_path)
538 cap_matrix.write_csv(raw_csv_path)
540 cap_matrix = cap_matrix.averaged_off_diagonals()
541 cap_matrix.write_csv(avg_csv_path)
543 netlist_expander = NetlistExpander()
544 expanded_netlist = netlist_expander.expand(
545 extracted_netlist=pex_context.lvsdb.netlist(),
546 top_cell_name=pex_context.annotated_top_cell.name,
547 cap_matrix=cap_matrix,
548 blackbox_devices=args.blackbox_devices
549 )
551 # create a nice CSV for reports, useful for spreadsheets
552 netlist_csv_writer = NetlistCSVWriter()
553 netlist_csv_writer.write_csv(netlist=expanded_netlist,
554 top_cell_name=pex_context.annotated_top_cell.name,
555 output_path=expanded_netlist_csv_path)
557 rule("Extended netlist (CSV format):")
558 with open(expanded_netlist_csv_path, 'r') as f:
559 for line in f.readlines():
560 subproc(line[:-1]) # abusing subproc, simply want verbatim
561 rule()
563 info(f"Wrote expanded netlist CSV to: {expanded_netlist_csv_path}")
565 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTERCAP)
566 netlist_printer.write(expanded_netlist, expanded_netlist_path)
567 info(f"Wrote expanded netlist to: {expanded_netlist_path}")
569 # FIXME: should this be already reduced?
570 if args.output_spice_path:
571 netlist_printer.write(expanded_netlist, args.output_spice_path)
572 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
574 netlist_reducer = NetlistReducer()
575 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist,
576 top_cell_name=pex_context.annotated_top_cell.name)
577 netlist_printer.write(reduced_netlist, reduced_netlist_path)
578 info(f"Wrote reduced netlist to: {reduced_netlist_path}")
580 self._fastercap_extracted_csv_path = expanded_netlist_csv_path
582 def run_magic_extraction(self,
583 args: argparse.Namespace):
584 if args.input_mode != InputMode.GDS:
585 error(f"MAGIC engine only works with GDS input mode"
586 f" (currently {args.input_mode})")
587 return
589 magic_run_dir = os.path.join(args.output_dir_path, f"magic_{args.magic_pex_mode}")
590 magic_log_path = os.path.join(magic_run_dir,
591 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Output.txt")
592 magic_script_path = os.path.join(magic_run_dir,
593 f"{args.effective_cell_name}_MAGIC_{args.magic_pex_mode}_Script.tcl")
595 output_netlist_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}.pex.spice")
596 report_db_path = os.path.join(magic_run_dir, f"{args.effective_cell_name}_MAGIC_report.rdb.gz")
598 os.makedirs(magic_run_dir, exist_ok=True)
600 prepare_magic_script(gds_path=args.effective_gds_path,
601 cell_name=args.effective_cell_name,
602 run_dir_path=magic_run_dir,
603 script_path=magic_script_path,
604 output_netlist_path=output_netlist_path,
605 pex_mode=args.magic_pex_mode,
606 c_threshold=args.magic_cthresh,
607 r_threshold=args.magic_rthresh,
608 tolerance=args.magic_tolerance,
609 halo=args.magic_halo,
610 short_mode=args.magic_short_mode,
611 merge_mode=args.magic_merge_mode)
613 run_magic(exe_path=args.magic_exe_path,
614 magicrc_path=args.magicrc_path,
615 script_path=magic_script_path,
616 log_path=magic_log_path)
618 magic_pex_run = parse_magic_pex_run(Path(magic_run_dir))
620 layout = kdb.Layout()
621 layout.read(args.effective_gds_path)
623 report = rdb.ReportDatabase('')
624 magic_log_analyzer = MagicLogAnalyzer(magic_pex_run=magic_pex_run,
625 report=report,
626 dbu=layout.dbu)
627 magic_log_analyzer.analyze()
628 report.save(report_db_path)
630 rule("Paths")
631 subproc(f"Report DB saved at: {report_db_path}")
632 subproc(f"SPICE netlist saved at: {output_netlist_path}")
634 if os.path.exists(output_netlist_path):
635 if args.output_spice_path and os.path.exists(output_netlist_path):
636 shutil.copy(output_netlist_path, args.output_spice_path)
637 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
639 rule("MAGIC PEX SPICE netlist")
640 with open(output_netlist_path, 'r') as f:
641 subproc(f.read())
642 rule()
644 def run_fastcap_extraction(self,
645 args: argparse.Namespace,
646 pex_context: KLayoutExtractionContext,
647 lst_file: str):
648 rule('FastCap2 Execution')
650 log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Output.txt")
651 raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Raw.csv")
652 avg_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Avg.csv")
653 expanded_netlist_path = os.path.join(args.output_dir_path,
654 f"{args.effective_cell_name}_FastCap2_Expanded_Netlist.cir")
655 reduced_netlist_path = os.path.join(args.output_dir_path,
656 f"{args.effective_cell_name}_FastCap2_Reduced_Netlist.cir")
658 run_fastcap(exe_path=args.fastcap_exe_path,
659 lst_file_path=lst_file,
660 log_path=log_path)
662 cap_matrix = fastcap_parse_capacitance_matrix(log_path)
663 cap_matrix.write_csv(raw_csv_path)
665 cap_matrix = cap_matrix.averaged_off_diagonals()
666 cap_matrix.write_csv(avg_csv_path)
668 netlist_expander = NetlistExpander()
669 expanded_netlist = netlist_expander.expand(
670 extracted_netlist=pex_context.lvsdb.netlist(),
671 top_cell_name=pex_context.annotated_top_cell.name,
672 cap_matrix=cap_matrix,
673 blackbox_devices=args.blackbox_devices
674 )
676 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.FASTCAP2)
677 netlist_printer.write(expanded_netlist, expanded_netlist_path)
678 info(f"Wrote expanded netlist to: {expanded_netlist_path}")
680 # FIXME: should this be already reduced?
681 if args.output_spice_path:
682 netlist_printer.write(expanded_netlist, args.output_spice_path)
683 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
685 netlist_reducer = NetlistReducer()
686 reduced_netlist = netlist_reducer.reduce(netlist=expanded_netlist,
687 top_cell_name=pex_context.annotated_top_cell.name)
688 netlist_printer.write(reduced_netlist, reduced_netlist_path)
690 info(f"Wrote reduced netlist to: {reduced_netlist_path}")
692 def run_kpex_2_5d_engine(self,
693 args: argparse.Namespace,
694 pex_context: KLayoutExtractionContext,
695 tech_info: TechInfo,
696 report_path: str,
697 netlist_csv_path: Optional[str],
698 expanded_netlist_path: Optional[str]):
699 # TODO: make this separatly configurable
700 # for now we use 0
701 args.rcx25d_delaunay_amax = 0
702 args.rcx25d_delaunay_b = 0.5
704 extractor = RCX25Extractor(pex_context=pex_context,
705 pex_mode=args.pex_mode,
706 delaunay_amax=args.rcx25d_delaunay_amax,
707 delaunay_b=args.rcx25d_delaunay_b,
708 scale_ratio_to_fit_halo=args.scale_ratio_to_fit_halo,
709 tech_info=tech_info,
710 report_path=report_path)
711 extraction_results = extractor.extract()
713 if netlist_csv_path is not None:
714 # TODO: merge this with klayout_pex/klayout/netlist_csv.py
716 with open(netlist_csv_path, 'w', encoding='utf-8') as f:
717 summary = extraction_results.summarize()
719 f.write('Device;Net1;Net2;Capacitance [fF];Resistance [Ω]\n')
720 for idx, (key, cap_value) in enumerate(sorted(summary.capacitances.items())):
721 f.write(f"C{idx + 1};{key.net1};{key.net2};{round(cap_value, 3)};\n")
722 for idx, (key, res_value) in enumerate(sorted(summary.resistances.items())):
723 f.write(f"R{idx + 1};{key.net1};{key.net2};;{round(res_value, 3)}\n")
725 rule('kpex/2.5D extracted netlist (CSV format)')
726 with open(netlist_csv_path, 'r') as f:
727 for line in f.readlines():
728 subproc(line[:-1]) # abusing subproc, simply want verbatim
730 rule('Extracted netlist CSV')
731 subproc(f"{netlist_csv_path}")
733 if expanded_netlist_path is not None:
734 rule('kpex/2.5D extracted netlist (SPICE format)')
735 netlist_expander = RCX25NetlistExpander()
736 expanded_netlist = netlist_expander.expand(
737 extracted_netlist=pex_context.lvsdb.netlist(),
738 top_cell_name=pex_context.annotated_top_cell.name,
739 extraction_results=extraction_results,
740 blackbox_devices=args.blackbox_devices
741 )
743 netlist_printer = self.create_netlist_printer(args, ExtractionEngine.K25D)
744 netlist_printer.write(expanded_netlist, expanded_netlist_path)
745 subproc(f"Wrote expanded netlist to: {expanded_netlist_path}")
747 # FIXME: should this be already reduced?
748 if args.output_spice_path:
749 netlist_printer.write(expanded_netlist, args.output_spice_path)
750 info(f"Copied expanded SPICE netlist to: {args.output_spice_path}")
752 # NOTE: there was a KLayout bug that some of the categories were lost,
753 # so that the marker browser could not load the report file
754 try:
755 report = rdb.ReportDatabase('')
756 report.load(report_path) # try loading rdb
757 except Exception as e:
758 rule("Repair broken marker DB")
759 warning(f"Detected KLayout bug: RDB can't be loaded due to exception {e}")
760 repair_rdb(report_path)
762 return extraction_results
764 def setup_logging(self, args: argparse.Namespace):
765 def register_log_file_handler(log_path: str,
766 formatter: Optional[logging.Formatter]) -> logging.Handler:
767 handler = logging.FileHandler(log_path)
768 handler.setLevel(LogLevel.SUBPROCESS)
769 if formatter:
770 handler.setFormatter(formatter)
771 register_additional_handler(handler)
772 return handler
774 def reregister_log_file_handler(handler: logging.Handler,
775 log_path: str,
776 formatter: Optional[logging.Formatter]):
777 deregister_additional_handler(handler)
778 handler.flush()
779 handler.close()
780 os.makedirs(args.output_dir_path, exist_ok=True)
781 new_path = os.path.join(args.output_dir_path, os.path.basename(log_path))
782 if os.path.exists(new_path):
783 ctime = os.path.getctime(new_path)
784 dt = datetime.fromtimestamp(ctime)
785 timestamp = dt.strftime('%Y-%m-%d_%H-%M-%S')
786 backup_path = f"{new_path[:-4]}_{timestamp}.bak.log"
787 shutil.move(new_path, backup_path)
788 log_path = shutil.move(log_path, new_path)
789 register_log_file_handler(log_path, formatter)
791 # setup preliminary logger
792 cli_log_path_plain = os.path.join(args.output_dir_base_path, f"kpex_plain.log")
793 cli_log_path_formatted = os.path.join(args.output_dir_base_path, f"kpex.log")
794 formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
795 file_handler_plain = register_log_file_handler(cli_log_path_plain, None)
796 file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter)
797 try:
798 self.validate_args(args)
799 except ArgumentValidationError:
800 if hasattr(args, 'output_dir_path'):
801 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None)
802 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter)
803 sys.exit(1)
804 reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None)
805 reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter)
807 set_log_level(args.log_level)
809 @staticmethod
810 def modification_date(filename: str) -> datetime:
811 t = os.path.getmtime(filename)
812 return datetime.fromtimestamp(t)
814 def create_lvsdb(self, args: argparse.Namespace) -> kdb.LayoutVsSchematic:
815 lvsdb = kdb.LayoutVsSchematic()
817 match args.input_mode:
818 case InputMode.LVSDB:
819 lvsdb.read(args.lvsdb_path)
820 case InputMode.GDS:
821 lvs_log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_lvs.log")
822 lvsdb_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}.lvsdb.gz")
823 lvsdb_cache_path = os.path.join(args.cache_dir_path, args.pdk,
824 os.path.splitroot(os.path.abspath(args.gds_path))[-1],
825 f"{args.effective_cell_name}.lvsdb.gz")
827 lvs_needed = True
829 if args.cache_lvs:
830 if not os.path.exists(lvsdb_cache_path):
831 info(f"Cache miss: extracted LVSDB does not exist")
832 subproc(lvsdb_cache_path)
833 elif self.modification_date(lvsdb_cache_path) <= self.modification_date(args.gds_path):
834 info(f"Cache miss: extracted LVSDB is older than the input GDS")
835 subproc(lvsdb_cache_path)
836 else:
837 warning(f"Cache hit: Reusing cached LVSDB")
838 subproc(lvsdb_cache_path)
839 lvs_needed = False
841 if lvs_needed:
842 lvs_runner = LVSRunner()
843 lvs_runner.run_klayout_lvs(exe_path=args.klayout_exe_path,
844 lvs_script=args.lvs_script_path,
845 gds_path=args.effective_gds_path,
846 schematic_path=args.effective_schematic_path,
847 log_path=lvs_log_path,
848 lvsdb_path=lvsdb_path,
849 verbose=args.klayout_lvs_verbose)
850 if args.cache_lvs:
851 cache_dir_path = os.path.dirname(lvsdb_cache_path)
852 if not os.path.exists(cache_dir_path):
853 os.makedirs(cache_dir_path, exist_ok=True)
854 shutil.copy(lvsdb_path, lvsdb_cache_path)
856 lvsdb.read(lvsdb_path)
857 return lvsdb
859 def main(self, argv: List[str]):
860 if '-v' not in argv and \
861 '--version' not in argv and \
862 '-h' not in argv and \
863 '--help' not in argv:
864 rule('Command line arguments')
865 subproc(' '.join(map(shlex.quote, sys.argv)))
867 env = Env.from_os_environ()
868 args = self.parse_args(arg_list=argv[1:], env=env)
870 os.makedirs(args.output_dir_base_path, exist_ok=True)
871 self.setup_logging(args)
873 tech_info = TechInfo.from_json(args.tech_pbjson_path,
874 dielectric_filter=args.dielectric_filter)
876 if args.halo is not None:
877 tech_info.tech.process_parasitics.side_halo = args.halo
879 if args.run_magic:
880 rule('MAGIC')
881 self.run_magic_extraction(args)
883 # no need to run LVS etc if only running magic engine
884 if not (args.run_fastcap or args.run_fastercap or args.run_2_5D):
885 return
887 rule('Prepare LVSDB')
888 lvsdb = self.create_lvsdb(args)
890 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=args.effective_cell_name,
891 lvsdb=lvsdb,
892 tech=tech_info,
893 blackbox_devices=args.blackbox_devices)
894 rule('Non-empty layers in LVS database')
895 for gds_pair, layer_info in pex_context.extracted_layers.items():
896 names = [l.lvs_layer_name for l in layer_info.source_layers]
897 info(f"{gds_pair} -> ({' '.join(names)})")
899 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_extracted.oas")
900 pex_context.annotated_layout.write(gds_path)
902 gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_internal.oas")
903 pex_context.lvsdb.internal_layout().write(gds_path)
905 def dump_layers(cell: str,
906 layers: List[KLayoutExtractedLayerInfo],
907 layout_dump_path: str):
908 layout = kdb.Layout()
909 layout.dbu = lvsdb.internal_layout().dbu
911 top_cell = layout.create_cell(cell)
912 for ulyr in layers:
913 li = kdb.LayerInfo(*ulyr.gds_pair)
914 li.name = ulyr.lvs_layer_name
915 layer = layout.insert_layer(li)
916 layout.insert(top_cell.cell_index(), layer, ulyr.region.dup())
918 layout.write(layout_dump_path)
920 if len(pex_context.unnamed_layers) >= 1:
921 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_unnamed_LVS_layers.gds.gz")
922 dump_layers(cell=args.effective_cell_name,
923 layers=pex_context.unnamed_layers,
924 layout_dump_path=layout_dump_path)
926 if len(pex_context.extracted_layers) >= 1:
927 layout_dump_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_nonempty_LVS_layers.gds.gz")
928 nonempty_layers = [l \
929 for layers in pex_context.extracted_layers.values() \
930 for l in layers.source_layers]
931 dump_layers(cell=args.effective_cell_name,
932 layers=nonempty_layers,
933 layout_dump_path=layout_dump_path)
934 else:
935 error("No extracted layers found")
936 sys.exit(1)
938 if args.run_fastcap or args.run_fastercap:
939 lst_file = self.build_fastercap_input(args=args,
940 pex_context=pex_context,
941 tech_info=tech_info)
942 if args.run_fastercap:
943 self.run_fastercap_extraction(args=args,
944 pex_context=pex_context,
945 lst_file=lst_file)
946 if args.run_fastcap:
947 self.run_fastcap_extraction(args=args,
948 pex_context=pex_context,
949 lst_file=lst_file)
951 if args.run_2_5D:
952 rule("kpex/2.5D PEX Engine")
953 report_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_k25d_pex_report.rdb.gz")
954 netlist_csv_path = os.path.abspath(os.path.join(args.output_dir_path,
955 f"{args.effective_cell_name}_k25d_pex_netlist.csv"))
956 netlist_spice_path = os.path.abspath(os.path.join(args.output_dir_path,
957 f"{args.effective_cell_name}_k25d_pex_netlist.spice"))
959 self._rcx25_extraction_results = self.run_kpex_2_5d_engine( # NOTE: store for test case
960 args=args,
961 pex_context=pex_context,
962 tech_info=tech_info,
963 report_path=report_path,
964 netlist_csv_path=netlist_csv_path,
965 expanded_netlist_path=netlist_spice_path
966 )
968 self._rcx25_extracted_csv_path = netlist_csv_path
970 @property
971 def rcx25_extraction_results(self) -> ExtractionResults:
972 if not hasattr(self, '_rcx25_extraction_results'):
973 raise Exception('rcx25_extraction_results is not initialized, was run_kpex_2_5d_engine called?')
974 return self._rcx25_extraction_results
976 @property
977 def rcx25_extracted_csv_path(self) -> str:
978 if not hasattr(self, '_rcx25_extracted_csv_path'):
979 raise Exception('rcx25_extracted_csv_path is not initialized, was run_kpex_2_5d_engine called?')
980 return self._rcx25_extracted_csv_path
982 @property
983 def fastercap_extracted_csv_path(self) -> str:
984 if not hasattr(self, '_fastercap_extracted_csv_path'):
985 raise Exception('fastercap_extracted_csv_path is not initialized, was run_fastercap_extraction called?')
986 return self._fastercap_extracted_csv_path
989if __name__ == "__main__":
990 cli = KpexCLI()
991 cli.main(sys.argv)