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
« 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#
25"""
26``netlist reduce``: reduce the RC parasitics of a SPICE netlist (#214), see :mod:`.netlist_reduction`
27"""
29from __future__ import annotations
31import argparse
32import os
33from typing import *
35import rich.console
36import rich.markdown
37import rich.text
38from rich_argparse import RichHelpFormatter
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
49DEFAULTS = ReductionOptions()
51EPILOG_MARKDOWN = f"""
52The reduction runs on every `.SUBCKT` (and on the top level) of the netlist on its own, in this order:
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
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>`.
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"""
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
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)")
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)")
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
136def run_reduce_subcommand(args: argparse.Namespace) -> ExitCode:
137 """
138 Validate the arguments (before reading anything), reduce, and report.
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
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
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
177 for scope in reduction.scopes:
178 info(scope.describe())
179 info(f"Wrote reduced netlist to {reduction.output_path}")
180 return ExitCode.OK