Coverage for klayout_pex/klayout/netlist_expander.py: 97%

76 statements  

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

1# 

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

3# SPDX-FileCopyrightText: 2024-2025 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# 

24from __future__ import annotations 

25 

26import re 

27from typing import * 

28 

29import klayout.db as kdb 

30 

31from ..log import ( 

32 info, 

33 warning, 

34) 

35from ..common.capacitance_matrix import CapacitanceMatrix 

36from ..device_models import DeviceModels 

37from klayout_pex.klayout.capacitance_matrix_interpreter import CapacitanceMatrixInterpreter 

38from .parasitic_device_classes import PARASITIC_CAPACITOR_CLASS_NAME 

39from ..util.unit_formatter import format_spice_number 

40 

41if TYPE_CHECKING: 

42 from ..rcx25.exclusion import Exclusions 

43 

44 

45def remove_devices(extracted_circuit: kdb.Circuit, copied_circuit: kdb.Circuit, device_ids: Collection[int]): 

46 """ 

47 :param extracted_circuit: the circuit with the IDs of the devices 

48 :param copied_circuit: its copy, which leaves out the devices of the IDs after this 

49 

50 NOTE: by their order, as the copy numbers its devices anew, which closes the gaps of the IDs 

51 """ 

52 if device_ids: 

53 for original, device in list(zip(extracted_circuit.each_device(), copied_circuit.each_device())): 

54 if original.id() in device_ids: 

55 copied_circuit.remove_device(device) 

56 

57 

58class NetlistExpander: 

59 @staticmethod 

60 def expand(extracted_netlist: kdb.Netlist, 

61 top_cell_name: str, 

62 cap_matrix: CapacitanceMatrix, 

63 cap_matrix_interpreter: CapacitanceMatrixInterpreter, 

64 blackbox_devices: bool, 

65 device_models: DeviceModels, 

66 substrate_net_name: str, 

67 exclusions: Optional[Exclusions] = None) -> kdb.Netlist: 

68 """ 

69 :param device_models: the device models of the tech info, whose metal capacitors white-box mode removes 

70 :param substrate_net_name: the conductor of the substrate (see FasterCapInputBuilder), 

71 created if the netlist has no such net, and a port if it has no pin 

72 :param exclusions: what a model has the parasitics of, whose conductors the matrix has per block 

73 """ 

74 expanded_netlist: kdb.Netlist = extracted_netlist.dup() 

75 top_circuit: kdb.Circuit = expanded_netlist.circuit_by_name(top_cell_name) 

76 

77 if exclusions is not None: 

78 remove_devices(extracted_netlist.circuit_by_name(top_cell_name), top_circuit, 

79 exclusions.removed_device_blocks) 

80 

81 if not blackbox_devices: 

82 # NOTE: only the metal capacitors (e.g. MIM and MOM caps), whose capacitance is extracted 

83 # from their plates and fingers (see RCX25NetlistExpander.is_whiteboxed), not e.g. the transistors 

84 metal_capacitor_class_names = device_models.metal_capacitor_class_names 

85 # NOTE: Store devices before modifying container 

86 devices_to_remove: List[kdb.Device] = [d for d in top_circuit.each_device() 

87 if d.device_class().name in metal_capacitor_class_names] 

88 for d in devices_to_remove: 

89 name = d.name or d.expanded_name() 

90 info(f"Removing whiteboxed device {name}") 

91 top_circuit.remove_device(d) 

92 

93 # create capacitor class 

94 cap = kdb.DeviceClassCapacitor() 

95 cap.name = PARASITIC_CAPACITOR_CLASS_NAME 

96 cap.description = "Extracted by kpex/FasterCap PEX" 

97 expanded_netlist.add(cap) 

98 

99 nets: List[kdb.Net] = [] 

100 

101 # build table: name -> net 

102 name2net: Dict[str, kdb.Net] = {n.expanded_name(): n for n in top_circuit.each_net()} 

103 

104 signal_names = [cap_matrix_interpreter.signal_name_from_conductor_name(nc) 

105 for nc in cap_matrix.conductor_names] 

106 

107 if exclusions is not None: 

108 cap_matrix = exclusions.capacitance_matrix_of_nets(cap_matrix, signal_names, substrate_net_name) 

109 signal_names = cap_matrix.conductor_names 

110 

111 # NOTE: the substrate conductor is the substrate net, or a net of its own (e.g. a metal test pattern), 

112 # and the substrate net is a port, also if the LVS netlist has it without pin 

113 # (e.g. IHP's global net sub! in a metal test pattern without taps) 

114 if substrate_net_name in signal_names: 

115 substrate_net = name2net.get(substrate_net_name) 

116 if substrate_net is None: 

117 substrate_net = top_circuit.create_net(substrate_net_name) 

118 name2net[substrate_net_name] = substrate_net 

119 if substrate_net.pin_count() == 0: 

120 top_circuit.connect_pin(top_circuit.create_pin(substrate_net_name), substrate_net) 

121 

122 # find nets for the matrix axes 

123 for nn in signal_names: 

124 n = name2net.get(nn) 

125 if n is None: 

126 raise Exception(f"No net found with name {nn}, net names are: {list(name2net.keys())}") 

127 nets.append(n) 

128 

129 cap_threshold = 0.0 

130 

131 def add_parasitic_cap(i: int, 

132 j: int, 

133 net1: kdb.Net, 

134 net2: kdb.Net, 

135 cap_value: float): 

136 if cap_value > cap_threshold: 

137 c: kdb.Device = top_circuit.create_device(cap, f"Cext_{i}_{j}") 

138 c.connect_terminal('A', net1) 

139 c.connect_terminal('B', net2) 

140 c.set_parameter('C', cap_value) # Farad 

141 if net1 == net2: 

142 raise Exception(f"Invalid attempt to create cap {c.name} between " 

143 f"same net {net1} with value format_capacitance(cap_value)") 

144 elif cap_value != 0.0: # NOTE: e.g. left out, see Exclusions.capacitance_matrix_of_nets 

145 warning(f"Ignoring capacitance matrix cell [{i},{j}], " 

146 f"{format_spice_number(cap_value)} is below threshold {format_spice_number(cap_threshold)}") 

147 

148 # ------------------------------------------------------------- 

149 # Example capacitance matrix: 

150 # [C11+C12+C13 -C12 -C13] 

151 # [-C21 C21+C22+C23 -C23] 

152 # [-C31 -C32 C31+C32+C33] 

153 # ------------------------------------------------------------- 

154 # 

155 # - Diagonal elements m[i][i] contain the capacitance over GND (Cii), 

156 # but in a sum including all the other values of the row 

157 # 

158 # https://www.fastfieldsolvers.com/Papers/The_Maxwell_Capacitance_Matrix_WP110301_R03.pdf 

159 # 

160 ground_index = signal_names.index(substrate_net_name) if substrate_net_name in signal_names else 0 

161 

162 for i in range(0, cap_matrix.dimension): 

163 row = cap_matrix[i] 

164 cap_ii = row[i] 

165 for j in range(0, cap_matrix.dimension): 

166 if i == j: 

167 continue 

168 cap_value = -row[j] # off-diagonals are always stored as negative values 

169 cap_ii -= cap_value # subtract summands to filter out Cii 

170 if j > i: 

171 add_parasitic_cap(i=i, j=j, 

172 net1=nets[i], net2=nets[j], 

173 cap_value=cap_value) 

174 # NOTE: the rest of the diagonal element (the capacitance to infinity) goes to the substrate 

175 if i != ground_index: 

176 add_parasitic_cap(i=i, j=i, 

177 net1=nets[i], net2=nets[ground_index], 

178 cap_value=cap_ii) 

179 

180 return expanded_netlist