Coverage for klayout_pex/netlist/netlist_cli.py: 92%
145 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
27from datetime import datetime
28import os
29import os.path
31import rich.console
32import rich.markdown
33import rich.text
34from rich_argparse import RichHelpFormatter
35import shlex
36import shutil
37import sys
38import tempfile
39from typing import *
41from klayout_pex.log import (
42 set_log_level,
43 register_additional_handler,
44 deregister_additional_handler,
45 # console,
46 # debug,
47 info,
48 warning,
49 subproc,
50 error,
51 rule
52)
53from klayout_pex.netlist.netlist_convert import add_convert_subcommand, run_convert
54from klayout_pex.netlist.reduce_subcommand import add_reduce_subcommand, run_reduce_subcommand
55from klayout_pex.pex25d.diagnostics import ExitCode
56from klayout_pex.util.argparse_helpers import (
57 SectionedHelpFormatter,
58 add_argument_with_former_name,
59 add_help_subcommand,
60 add_log_level_argument,
61 add_subcommand_sections,
62 handle_help_subcommand,
63 warn_deprecated_options,
64)
65from klayout_pex.version import __version__
68# ------------------------------------------------------------------------------------
70PROGRAM_NAME = "netlist"
72SUBCOMMAND_SECTIONS: Dict[str, Tuple[str, ...]] = {
73 'Inspect': ('svg',),
74 'Transform': ('reduce', 'convert'),
75 'Help': ('help',),
76}
77"""How ``netlist --help`` groups the subcommands; every subcommand is in exactly one section."""
80SVG_EPILOG_MARKDOWN = """\
81| Exit code | Meaning |
82| --------- | ------- |
83| 0 | success |
84| 1 | the netlist can't be drawn: it can't be read, it has no top circuit, or netlistsvg failed |
85| 2 | the command line is wrong, e.g. the input netlist, netlistsvg or the directory of the output is missing, `--cell` names no circuit, or the netlist has several top circuits and no `--cell` |
86"""
89def svg_output_path(args: argparse.Namespace) -> str:
90 """The SVG that netlist svg writes: --out-svg, else the input path with the extension .svg"""
91 return args.output_svg_path or os.path.splitext(args.input_netlist_path)[0] + '.svg'
94class ArgumentValidationError(Exception):
95 pass
98class NetlistCLI:
99 def parse_args(self, arg_list: List[str] = None) -> argparse.Namespace:
101 main_parser = argparse.ArgumentParser(prog=PROGRAM_NAME,
102 description=f"{PROGRAM_NAME}: "
103 f"Netlist tool for KLayout-PEX",
104 add_help=False,
105 formatter_class=SectionedHelpFormatter)
107 group_special = main_parser.add_argument_group("Special Options")
108 group_special.add_argument("--help", "-h", action='help', help="show this help message and exit")
109 group_special.add_argument("--version", "-v", action='version', version=f'{PROGRAM_NAME} {__version__}')
110 add_log_level_argument(group_special)
112 subparsers = main_parser.add_subparsers(dest="command", metavar='<subcommand>')
114 parser_netlistsvg = subparsers.add_parser("svg",
115 help="Draw the schematic of a SPICE netlist as SVG "
116 "(with netlistsvg)",
117 formatter_class=RichHelpFormatter,
118 epilog=rich.console.Group(
119 rich.text.Text('Exit codes:', style='argparse.groups'),
120 rich.markdown.Markdown(SVG_EPILOG_MARKDOWN,
121 style='argparse.text')))
122 parser_netlistsvg.add_argument("input_netlist_path",
123 type=str, help="Path to the input netlist file")
125 parser_netlistsvg.add_argument('--exe', dest='netlistsvg_exe_path',
126 default='netlistsvg',
127 help="Path to netlistsvg executable (default is '%(default)s')")
129 add_argument_with_former_name(parser_netlistsvg, "--out-svg", "--output", "-o", dest="output_svg_path",
130 help="Output SVG path (default is the input path with the extension .svg)",
131 default=None)
132 parser_netlistsvg.add_argument("--preview", "-p", dest="preview_svg",
133 action='store_true', default=False,
134 help="Preview SVG")
135 parser_netlistsvg.add_argument("--cell", "-c", dest="cell_name", default=None,
136 help="Cell (default is the top cell)")
138 add_reduce_subcommand(subparsers)
139 add_convert_subcommand(subparsers)
140 add_help_subcommand(subparsers)
141 add_subcommand_sections(main_parser, subparsers, SUBCOMMAND_SECTIONS)
143 if arg_list is None:
144 arg_list = sys.argv[1:]
145 args = main_parser.parse_args(arg_list)
146 handle_help_subcommand(main_parser, args)
148 self.validate_args(main_parser, args)
150 return args
152 @staticmethod
153 def validate_args(main_parser: argparse.ArgumentParser,
154 args: argparse.Namespace):
155 print("")
156 found_errors = False
158 match args.command:
159 case None:
160 error(f"Command argument missing")
161 found_errors = True
162 print("")
163 rule('Usage')
164 main_parser.print_help()
166 sys.exit(1)
168 case 'svg':
169 if not os.path.isfile(args.input_netlist_path):
170 error(f"Input netlist not found: {args.input_netlist_path}")
171 found_errors = True
172 if shutil.which(args.netlistsvg_exe_path) is None:
173 error(f"Can't locate netlistsvg executable at {args.netlistsvg_exe_path} "
174 f"(install it with 'npm install -g netlistsvg', or give its path with --exe)")
175 found_errors = True
176 output_dir = os.path.dirname(os.path.abspath(svg_output_path(args)))
177 if not os.path.isdir(output_dir):
178 error(f"The directory of the output SVG doesn't exist: {output_dir}")
179 found_errors = True
181 if found_errors:
182 raise ArgumentValidationError("Argument validation failed")
184 def run_netlistsvg(self,
185 args: argparse.Namespace) -> ExitCode:
186 """
187 :return: the exit code (see the epilog)
188 """
189 # NOTE: only this subcommand needs KLayout, the others (e.g. reduce) must not load it
190 import klayout.db as kdb
191 from klayout_pex.netlistsvg.netlist_json import NetlistJSONWriter
192 from klayout_pex.netlistsvg.netlistsvg_runner import NetlistSVGError, SKIN_PATH, run_netlistsvg
194 netlist = kdb.Netlist()
195 netlist_reader = kdb.NetlistSpiceReader()
196 try:
197 netlist.read(args.input_netlist_path, netlist_reader)
198 except RuntimeError as e:
199 error(f"Can't read the SPICE netlist {args.input_netlist_path}: {e}")
200 return ExitCode.DIAGNOSTIC_ERRORS
202 rule('netlistsvg Execution')
204 top_circuits = netlist.top_circuits()
205 top_circuit_names = [c.name for c in top_circuits]
206 n_circuits = len(top_circuit_names)
208 top_circuit: kdb.Circuit
210 if args.cell_name is not None:
211 top_circuit = netlist.circuit_by_name(args.cell_name)
212 if top_circuit is None:
213 circuit_names = ', '.join(c.name for c in netlist.each_circuit())
214 error(f"No circuit {args.cell_name} in the SPICE netlist, its circuits are: {circuit_names}")
215 return ExitCode.USAGE
216 else:
217 match n_circuits:
218 case 0:
219 error(f"No top circuit found in SPICE netlist, perhaps the SUBCKT with the ports is missing?!")
220 return ExitCode.DIAGNOSTIC_ERRORS
221 case 1:
222 top_circuit = top_circuits[0]
223 case _:
224 info(f"Multiple top circuits found in SPICE netlist: {top_circuit_names}")
225 error('Please specify the desired cell using the --cell argument')
226 return ExitCode.USAGE
228 output_svg_path = svg_output_path(args)
230 # NOTE: the netlistsvg input is an intermediate file
231 with tempfile.TemporaryDirectory() as tmp_dir:
232 yosys_json_netlist_path = os.path.join(tmp_dir, f"yosys_netlist.json")
233 json_writer = NetlistJSONWriter()
234 json_writer.write_json(netlist=netlist,
235 top_circuit=top_circuit,
236 output_path=yosys_json_netlist_path)
238 try:
239 run_netlistsvg(exe_path=shutil.which(args.netlistsvg_exe_path),
240 skin_path=SKIN_PATH,
241 yosys_json_netlist_path=yosys_json_netlist_path,
242 output_svg_path=output_svg_path,
243 layout_elk_path=None)
244 except NetlistSVGError as e:
245 error(str(e))
246 return ExitCode.DIAGNOSTIC_ERRORS
248 info(f"Wrote {output_svg_path}")
250 if args.preview_svg:
251 import webbrowser
252 webbrowser.open(f"file://{os.path.abspath(output_svg_path)}")
254 # import cairosvg
255 # from PIL import Image
256 # import io
257 # import matplotlib.pyplot as plt
258 #
259 # png_data = cairosvg.svg2png(url=output_svg_path)
260 # image = Image.open(io.BytesIO(png_data))
261 # plt.imshow(image)
262 # plt.axis('off')
263 # plt.show()
265 return ExitCode.OK
267 def setup_logging(self, args: argparse.Namespace):
268 set_log_level(args.log_level)
270 @staticmethod
271 def modification_date(filename: str) -> datetime:
272 t = os.path.getmtime(filename)
273 return datetime.fromtimestamp(t)
275 def main(self, argv: List[str]):
276 if '-v' not in argv and \
277 '--version' not in argv and \
278 '-h' not in argv and \
279 '--help' not in argv and \
280 argv[1:2] != ['help']:
281 rule('Command line arguments')
282 subproc(' '.join(map(shlex.quote, sys.argv)))
284 try:
285 args = self.parse_args(argv[1:])
286 except ArgumentValidationError:
287 sys.exit(ExitCode.USAGE)
289 self.setup_logging(args)
290 warn_deprecated_options(args)
292 match args.command:
293 case 'svg':
294 sys.exit(self.run_netlistsvg(args))
295 case 'reduce':
296 sys.exit(run_reduce_subcommand(args))
297 case 'convert':
298 sys.exit(run_convert(args))
301if __name__ == "__main__":
302 cli = NetlistCLI()
303 cli.main(sys.argv)