Coverage for klayout_pex/netlist/netlist_reduction.py: 100%
93 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"""
26Reduction of the RC parasitics of a SPICE netlist (``netlist reduce``, #214):
27each scope (.SUBCKT and top level) is reduced on its own, see :mod:`.rc_reduction` for the passes
28and :mod:`.spice_rc_netlist` for what is read, protected and written.
29"""
31from __future__ import annotations
33from dataclasses import dataclass
34from enum import StrEnum
35import os
36from typing import *
38from .rc_network import RCNetwork
39from .rc_reduction import ReductionOptions, ReductionStatistics, reduce_network
40from .spice_rc_netlist import SpiceRCNetlist
43class ReductionMode(StrEnum):
44 """
45 The reduction kpex runs on the netlist of its 2.5D engine (--reduce), with the passes of ``netlist reduce``
46 """
47 NONE = 'none'
48 FLOATING = 'floating'
49 """merge the floating islands, eliminate the capacitor-only nodes (exactly), tie the rest to ground"""
50 TICER = 'ticer'
51 """the same, with TICER before the capacitor-only nodes"""
52 DEFAULT = 'none'
54 def options(self, ticer_tau: Optional[float] = None) -> Optional[ReductionOptions]:
55 """
56 :param ticer_tau: TICER's time constant threshold [s], None for the default
57 :return: the options of the passes, None for no reduction
58 """
59 match self:
60 case ReductionMode.NONE:
61 return None
62 case ReductionMode.FLOATING:
63 return ReductionOptions(ticer=False)
64 case ReductionMode.TICER:
65 return ReductionOptions() if ticer_tau is None else ReductionOptions(ticer_tau=ticer_tau)
66 case _:
67 raise NotImplementedError()
70def reduced_netlist_path(input_path: str) -> str:
71 """'<dir>/<name>.spice' → '<dir>/<name>_reduced.spice'"""
72 base, extension = os.path.splitext(input_path)
73 return f"{base}_reduced{extension}"
76class UnknownNodesError(Exception):
77 """Nodes to keep that are in none of the RC networks"""
79 def __init__(self, unknown_names: List[str], known_names: List[str]):
80 self.unknown_names = unknown_names
81 self.known_names = known_names
82 listed = known_names[:20]
83 more = f", … ({len(known_names) - len(listed)} more)" if len(known_names) > len(listed) else ''
84 super().__init__(f"No RC network has the node(s) to keep {', '.join(unknown_names)}; "
85 f"the nodes are: {', '.join(listed)}{more}")
88@dataclass(frozen=True)
89class NetworkSize:
90 resistors: int
91 capacitors: int
92 nodes: int
93 """nodes with at least one resistor or capacitor"""
95 @classmethod
96 def of(cls, network: RCNetwork) -> NetworkSize:
97 return cls(resistors=network.resistor_count(),
98 capacitors=network.capacitor_count(),
99 nodes=network.connected_node_count())
101 def __str__(self) -> str:
102 return f"{self.resistors} R, {self.capacitors} C, {self.nodes} nodes"
105@dataclass(frozen=True)
106class ScopeReduction:
107 scope: str
108 """'.SUBCKT <name>' or 'top level'"""
109 resistors_read: int
110 capacitors_read: int
111 before: NetworkSize
112 """after adding up parallel elements"""
113 after: NetworkSize
114 statistics: ReductionStatistics
116 def describe(self) -> str:
117 statistics = self.statistics
118 return (f"{self.scope}: {self.before} → {self.after} "
119 f"(TICER eliminated {statistics.quick_nodes} nodes; "
120 f"{statistics.floating_islands} floating islands merged, "
121 f"{statistics.floating_island_nodes} nodes less; "
122 f"{statistics.capacitor_only_nodes} capacitor-only nodes eliminated, {statistics.tied_nodes} tied)")
125@dataclass(frozen=True)
126class NetlistReduction:
127 output_path: str
128 """the path written"""
129 scopes: List[ScopeReduction]
130 """the scopes with R or C elements, in the order of the netlist"""
133def _describe_options(options: ReductionOptions) -> str:
134 passes = []
135 if options.ticer:
136 passes.append(f"TICER tau={options.ticer_tau * 1e12:g}ps max degree {options.ticer_max_degree}")
137 if options.floating:
138 passes.append(f"floating max degree {options.floating_max_degree}")
139 return ', '.join(passes)
142def reduce_spice_netlist(input_path: str,
143 output_path: str,
144 options: ReductionOptions,
145 keep: Sequence[str] = ()) -> NetlistReduction:
146 """
147 Reduce the RC network of every scope of a SPICE netlist and write the result
148 (creating the output's directory if needed).
150 :param keep: nodes to keep in addition to the protected ones (case-insensitive),
151 each must be a node of at least one network
152 :raises OSError: if the input can't be read or the output can't be written
153 :raises NetlistReadError: if the netlist's .SUBCKT and .ENDS don't match
154 :raises UnknownNodesError: if a node to keep is in none of the networks (nothing is written then)
155 """
156 netlist = SpiceRCNetlist.read(input_path)
157 scopes = [s for s in netlist.scopes if s.resistors_read or s.capacitors_read]
159 unknown = [name for name in keep
160 if all(s.network.find_node(name) is None for s in scopes)]
161 if unknown:
162 known = sorted({s.network.name(n) for s in scopes for n in s.network.nodes()},
163 key=RCNetwork.name_key)
164 raise UnknownNodesError(unknown_names=unknown, known_names=known)
166 reductions: List[ScopeReduction] = []
167 for scope in scopes:
168 network = scope.network
169 for name in keep:
170 node = network.find_node(name)
171 if node is not None:
172 network.protect(node)
173 before = NetworkSize.of(network)
174 statistics = reduce_network(network, options)
175 after = NetworkSize.of(network)
176 scope.comment_lines.append(f"* netlist reduce ({_describe_options(options)}): "
177 f"{before} -> {after}")
178 reductions.append(ScopeReduction(scope=scope.title,
179 resistors_read=scope.resistors_read,
180 capacitors_read=scope.capacitors_read,
181 before=before,
182 after=after,
183 statistics=statistics))
185 output_directory = os.path.dirname(output_path)
186 if output_directory:
187 os.makedirs(output_directory, exist_ok=True)
188 return NetlistReduction(output_path=netlist.write(output_path), scopes=reductions)