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