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

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# 

25 

26import argparse 

27from datetime import datetime 

28import os 

29import os.path 

30 

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 * 

40 

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__ 

66 

67 

68# ------------------------------------------------------------------------------------ 

69 

70PROGRAM_NAME = "netlist" 

71 

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.""" 

78 

79 

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""" 

87 

88 

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' 

92 

93 

94class ArgumentValidationError(Exception): 

95 pass 

96 

97 

98class NetlistCLI: 

99 def parse_args(self, arg_list: List[str] = None) -> argparse.Namespace: 

100 

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) 

106 

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) 

111 

112 subparsers = main_parser.add_subparsers(dest="command", metavar='<subcommand>') 

113 

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") 

124 

125 parser_netlistsvg.add_argument('--exe', dest='netlistsvg_exe_path', 

126 default='netlistsvg', 

127 help="Path to netlistsvg executable (default is '%(default)s')") 

128 

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)") 

137 

138 add_reduce_subcommand(subparsers) 

139 add_convert_subcommand(subparsers) 

140 add_help_subcommand(subparsers) 

141 add_subcommand_sections(main_parser, subparsers, SUBCOMMAND_SECTIONS) 

142 

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) 

147 

148 self.validate_args(main_parser, args) 

149 

150 return args 

151 

152 @staticmethod 

153 def validate_args(main_parser: argparse.ArgumentParser, 

154 args: argparse.Namespace): 

155 print("") 

156 found_errors = False 

157 

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() 

165 

166 sys.exit(1) 

167 

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 

180 

181 if found_errors: 

182 raise ArgumentValidationError("Argument validation failed") 

183 

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 

193 

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 

201 

202 rule('netlistsvg Execution') 

203 

204 top_circuits = netlist.top_circuits() 

205 top_circuit_names = [c.name for c in top_circuits] 

206 n_circuits = len(top_circuit_names) 

207 

208 top_circuit: kdb.Circuit 

209 

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 

227 

228 output_svg_path = svg_output_path(args) 

229 

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) 

237 

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 

247 

248 info(f"Wrote {output_svg_path}") 

249 

250 if args.preview_svg: 

251 import webbrowser 

252 webbrowser.open(f"file://{os.path.abspath(output_svg_path)}") 

253 

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() 

264 

265 return ExitCode.OK 

266 

267 def setup_logging(self, args: argparse.Namespace): 

268 set_log_level(args.log_level) 

269 

270 @staticmethod 

271 def modification_date(filename: str) -> datetime: 

272 t = os.path.getmtime(filename) 

273 return datetime.fromtimestamp(t) 

274 

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))) 

283 

284 try: 

285 args = self.parse_args(argv[1:]) 

286 except ArgumentValidationError: 

287 sys.exit(ExitCode.USAGE) 

288 

289 self.setup_logging(args) 

290 warn_deprecated_options(args) 

291 

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)) 

299 

300 

301if __name__ == "__main__": 

302 cli = NetlistCLI() 

303 cli.main(sys.argv)