Coverage for klayout_pex/netlist/reduce_subcommand.py: 96%

68 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX 

7# (see https://github.com/iic-jku/klayout-pex). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21# SPDX-License-Identifier: GPL-3.0-or-later 

22# -------------------------------------------------------------------------------- 

23# 

24 

25""" 

26``netlist reduce``: reduce the RC parasitics of a SPICE netlist (#214), see :mod:`.netlist_reduction` 

27""" 

28 

29from __future__ import annotations 

30 

31import argparse 

32import os 

33from typing import * 

34 

35import rich.console 

36import rich.markdown 

37import rich.text 

38from rich_argparse import RichHelpFormatter 

39 

40from ..log import error, info, rule 

41from ..pex25d.diagnostics import ExitCode 

42from ..util.argparse_helpers import add_argument_with_former_name, positive_spice_seconds, true_or_false 

43from ..util.unit_formatter import format_spice_number 

44from .netlist_reduction import UnknownNodesError, reduce_spice_netlist, reduced_netlist_path 

45from .rc_reduction import TIE_RESISTANCE, ReductionOptions 

46from .spice_rc_netlist import NetlistReadError 

47 

48 

49DEFAULTS = ReductionOptions() 

50 

51EPILOG_MARKDOWN = f""" 

52The reduction runs on every `.SUBCKT` (and on the top level) of the netlist on its own, in this order: 

53 

541. *floating*: merge each floating island (nodes connected by resistors only among themselves, none of them kept) 

55 into one node 

562. *TICER*: eliminate the quick nodes, whose time constant (capacitance sum / conductance sum) is at most 

57 `--ticer-tau`, and which have at most `--ticer-max-degree` resistors. The neighbours get the star-mesh 

58 transform of the node: exact at DC, and the capacitance between the nets is conserved. 

59 The error per node is about 2π f τ at frequency f, e.g. below 1 % up to 16 GHz with τ = 0.1 ps; 

60 the errors of many nodes can add up. Losses are not kept: the series resistance (the Q) of a capacitor 

61 shrinks or vanishes, unless τ is small against 1 / (2π f Q). 

623. *floating*: eliminate the capacitor-only nodes with at most `--floating-max-degree` capacitors exactly 

63 (a plate between two nets becomes their series capacitance), and tie the rest to ground (node `0`) 

64 by {format_spice_number(TIE_RESISTANCE)}Ω, so that DC has a solution. The ties of a netlist reduced before 

65 (`Rtie_<n>`) are dropped first, so that it can be reduced again, e.g. with a higher degree limit 

66 

67Never eliminated: the `.SUBCKT` ports, the ground nodes `0` and `gnd`, `.GLOBAL` nodes, every node of a 

68line that is no plain R or C (device and subcircuit terminals, sources, control statements), and the nodes 

69given with `--keep`. Plain R and C elements have two nodes and a number as value; all other lines are 

70written as they are. The reduced R and C elements are named `Rred_<n>` and `Cred_<n>`, the ties `Rtie_<n>`. 

71 

72| Exit code | Meaning | 

73| --------- | ------- | 

74| 0 | success | 

75| 1 | the netlist can't be split into subcircuits (`.SUBCKT` and `.ENDS` do not match) | 

76| 2 | the command line is wrong, or a file could not be read or written | 

77""" 

78 

79 

80def _degree(text: str) -> int: 

81 """argparse type: an integer ≥ 1""" 

82 try: 

83 value = int(text) 

84 except ValueError: 

85 raise argparse.ArgumentTypeError(f"must be an integer ≥ 1, got '{text}'") 

86 if value < 1: 

87 raise argparse.ArgumentTypeError(f"must be an integer ≥ 1, got '{text}'") 

88 return value 

89 

90 

91def add_reduce_subcommand(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser: 

92 parser = subparsers.add_parser( 

93 "reduce", 

94 help="Reduce the RC parasitics of a SPICE netlist (TICER, capacitor-only nodes)", 

95 description="Reduce the resistors and capacitors of a SPICE netlist, e.g. the 2.5D RC output of kpex: " 

96 "TICER eliminates the nodes with small time constants, " 

97 "the capacitor-only nodes are eliminated exactly.", 

98 formatter_class=RichHelpFormatter, 

99 epilog=rich.console.Group(rich.text.Text('Passes, kept nodes and exit codes:', style='argparse.groups'), 

100 rich.markdown.Markdown(EPILOG_MARKDOWN, style='argparse.text'))) 

101 parser.add_argument("input_netlist_path", metavar="INPUT_NETLIST", help="Path to the SPICE netlist to reduce") 

102 add_argument_with_former_name(parser, "--out-netlist", "--output", "-o", dest="output_netlist_path", 

103 metavar="OUTPUT_NETLIST", default=None, 

104 help="Path of the reduced netlist (default is INPUT_NETLIST with '_reduced' " 

105 "before its extension)") 

106 parser.add_argument("--keep", dest="keep_nodes", metavar="NODE", action='append', default=[], 

107 help="Never eliminate this node (case-insensitive, may be given more than once)") 

108 

109 group_ticer = parser.add_argument_group("TICER") 

110 group_ticer.add_argument("--ticer", dest="ticer", type=true_or_false, default=DEFAULTS.ticer, 

111 help="Eliminate the quick nodes (default is %(default)s)") 

112 add_argument_with_former_name(group_ticer, "--ticer-tau", "--ticer_tau", 

113 dest="ticer_tau", metavar="TAU", type=positive_spice_seconds, 

114 default=DEFAULTS.ticer_tau, 

115 help=f"Time constant threshold in s, a SPICE number like '0.1p' or '1e-13' " 

116 f"(default is {DEFAULTS.ticer_tau * 1e12:g}p)") 

117 add_argument_with_former_name(group_ticer, "--ticer-max-degree", "--ticer_max_degree", 

118 dest="ticer_max_degree", metavar="N", type=_degree, 

119 default=DEFAULTS.ticer_max_degree, 

120 help="Keep nodes with more resistors than this, to bound the fill-in " 

121 "(an integer ≥ 1, default is %(default)s)") 

122 

123 group_floating = parser.add_argument_group("Floating nodes") 

124 group_floating.add_argument("--floating", dest="floating", type=true_or_false, 

125 default=DEFAULTS.floating, 

126 help="Merge floating islands, eliminate capacitor-only nodes " 

127 "(default is %(default)s)") 

128 add_argument_with_former_name(group_floating, "--floating-max-degree", "--floating_max_degree", 

129 dest="floating_max_degree", metavar="N", type=_degree, 

130 default=DEFAULTS.floating_max_degree, 

131 help="Tie capacitor-only nodes with more capacitors than this to ground instead " 

132 "(an integer ≥ 1, default is %(default)s)") 

133 return parser 

134 

135 

136def run_reduce_subcommand(args: argparse.Namespace) -> ExitCode: 

137 """ 

138 Validate the arguments (before reading anything), reduce, and report. 

139 

140 :return: the exit code (see the epilog) 

141 """ 

142 try: 

143 options = ReductionOptions(ticer=args.ticer, 

144 ticer_tau=args.ticer_tau, 

145 ticer_max_degree=args.ticer_max_degree, 

146 floating=args.floating, 

147 floating_max_degree=args.floating_max_degree) 

148 except ValueError as e: 

149 error(f"Invalid reduction options: {e}") 

150 return ExitCode.USAGE 

151 

152 input_path = args.input_netlist_path 

153 if not os.path.isfile(input_path): 

154 error(f"Input netlist not found: {input_path}") 

155 return ExitCode.USAGE 

156 output_path = args.output_netlist_path or reduced_netlist_path(input_path) 

157 if os.path.exists(output_path) and os.path.samefile(input_path, output_path): 

158 error(f"The output would overwrite the input netlist: {output_path}") 

159 return ExitCode.USAGE 

160 

161 rule('Netlist reduction') 

162 try: 

163 reduction = reduce_spice_netlist(input_path=input_path, 

164 output_path=output_path, 

165 options=options, 

166 keep=args.keep_nodes) 

167 except UnknownNodesError as e: 

168 error(str(e)) 

169 return ExitCode.USAGE 

170 except NetlistReadError as e: 

171 error(f"Invalid netlist: {e}") 

172 return ExitCode.DIAGNOSTIC_ERRORS 

173 except OSError as e: 

174 error(f"Netlist reduction failed: {e}") 

175 return ExitCode.USAGE 

176 

177 for scope in reduction.scopes: 

178 info(scope.describe()) 

179 info(f"Wrote reduced netlist to {reduction.output_path}") 

180 return ExitCode.OK