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

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

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

30 

31from __future__ import annotations 

32 

33from dataclasses import dataclass 

34from enum import StrEnum 

35import os 

36from typing import * 

37 

38from .rc_network import RCNetwork 

39from .rc_reduction import ReductionOptions, ReductionStatistics, reduce_network 

40from .spice_rc_netlist import SpiceRCNetlist 

41 

42 

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' 

53 

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

68 

69 

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

74 

75 

76class UnknownNodesError(Exception): 

77 """Nodes to keep that are in none of the RC networks""" 

78 

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

86 

87 

88@dataclass(frozen=True) 

89class NetworkSize: 

90 resistors: int 

91 capacitors: int 

92 nodes: int 

93 """nodes with at least one resistor or capacitor""" 

94 

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

100 

101 def __str__(self) -> str: 

102 return f"{self.resistors} R, {self.capacitors} C, {self.nodes} nodes" 

103 

104 

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 

115 

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

123 

124 

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

131 

132 

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) 

140 

141 

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

149 

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] 

158 

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) 

165 

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

184 

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)