Coverage for klayout_pex/rcx25/netlist_expander.py: 98%

110 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 .extraction_results import ExtractionResults 

36from ..device_models import DeviceModels 

37from ..klayout.netlist_expander import remove_devices 

38from ..klayout.parasitic_device_classes import PARASITIC_CAPACITOR_CLASS_NAME, PARASITIC_RESISTOR_CLASS_NAME 

39 

40 

41# the net of the capacitances to the substrate in the extraction results (TechInfo.internal_substrate_layer_name) 

42SUBSTRATE = 'VSUBS' 

43 

44 

45class RCX25NetlistExpander: 

46 @staticmethod 

47 def is_whiteboxed(device_class: kdb.DeviceClass, device_models: DeviceModels) -> bool: 

48 """ 

49 :return: whether white-box mode (--blackbox n) removes the devices of the class, 

50 as their capacitances are extracted from their plates and fingers (e.g. MIM and MOM caps) 

51 

52 NOTE: the kind of their device model tells, not the LVS device class: 

53 e.g. sky130A reads VPP caps with 4 terminals as MOS4, 

54 while the MOS capacitors and varactors are capacitor classes, 

55 whose capacitance (of the gate oxide) the extraction doesn't have 

56 """ 

57 return device_class.name in device_models.metal_capacitor_class_names 

58 

59 @staticmethod 

60 def expand(extracted_netlist: kdb.Netlist, 

61 top_cell_name: str, 

62 extraction_results: ExtractionResults, 

63 blackbox_devices: bool, 

64 device_models: DeviceModels, 

65 substrate_net_name: Optional[str] = None, 

66 removed_device_ids: Collection[int] = ()) -> kdb.Netlist: 

67 """ 

68 :param device_models: the device models of the tech info, which tell the whiteboxed devices 

69 :param substrate_net_name: the net the capacitances to the substrate go to (VSUBS if not given), 

70 created if the netlist has none, and a port if it has no pin 

71 :param removed_device_ids: the devices to leave out, as a model has them (of cut cells) 

72 """ 

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

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

75 

76 remove_devices(extracted_netlist.circuit_by_name(top_cell_name), top_circuit, removed_device_ids) 

77 

78 if not blackbox_devices: 

79 # NOTE: removing a device ends the iteration over the circuit's devices, 

80 # so iterate over a list of them 

81 for d in list(top_circuit.each_device()): 

82 if RCX25NetlistExpander.is_whiteboxed(d.device_class(), device_models): 

83 info(f"Removing whiteboxed device {d.name or d.expanded_name()}") 

84 top_circuit.remove_device(d) 

85 

86 # create capacitor device class 

87 cap = kdb.DeviceClassCapacitor() 

88 # cap.name = 'KPEX_CAP' 

89 cap.name = PARASITIC_CAPACITOR_CLASS_NAME 

90 cap.description = "Extracted by KPEX/2.5D" 

91 expanded_netlist.add(cap) 

92 

93 # create resistor device class 

94 res = kdb.DeviceClassResistor() 

95 # res.name = 'KPEX_RES' 

96 res.name = PARASITIC_RESISTOR_CLASS_NAME 

97 res.description = "Extracted by KPEX/2.5D" 

98 expanded_netlist.add(res) 

99 

100 fc_gnd_net = top_circuit.create_net('FC_GND') # create GROUND net 

101 

102 summary = extraction_results.summarize() 

103 cap_items = sorted(summary.capacitances.items()) 

104 res_items = sorted(summary.resistances.items()) 

105 

106 # build table: name -> net 

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

108 

109 def add_net_if_needed(net_name: str): 

110 if net_name in name2net: 

111 return 

112 name2net[net_name] = top_circuit.create_net(net_name) 

113 

114 # NOTE: the capacitances to the substrate are on VSUBS (TechInfo.internal_substrate_layer_name), 

115 # they go to the substrate net, rather than to a net that connects to nothing, 

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

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

118 if any(SUBSTRATE in (key.net1, key.net2) for key, _ in cap_items): 

119 substrate_net_name = substrate_net_name or SUBSTRATE 

120 substrate_net = name2net.get(substrate_net_name) 

121 if substrate_net is None: 

122 substrate_net = top_circuit.create_net(substrate_net_name) 

123 name2net[substrate_net_name] = substrate_net 

124 if substrate_net.pin_count() == 0: 

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

126 name2net[SUBSTRATE] = substrate_net 

127 

128 # add additional nets for new nodes (e.g. created during R extraction of vias) 

129 for key, _ in cap_items: 

130 add_net_if_needed(key.net1) 

131 add_net_if_needed(key.net2) 

132 for key, _ in res_items: 

133 add_net_if_needed(key.net1) 

134 add_net_if_needed(key.net2) 

135 

136 # connect each device terminal to its node of the resistor network (#211 §6), 

137 # a terminal without one stays on its net, which a node of the resistor network carries 

138 for key, node_name in sorted(summary.device_terminal_nodes.items()): 

139 device = top_circuit.device_by_id(key.device_id) 

140 if device is None: # e.g. removed, as whiteboxed 

141 continue 

142 add_net_if_needed(node_name) 

143 device.connect_terminal(key.terminal_id, name2net[node_name]) 

144 

145 # the pin of a net of several labels (e.g. A,B) becomes a port per label, in its place (#211 §11) 

146 if summary.label_ports: 

147 ports: List[Tuple[str, Optional[kdb.Net]]] = [] 

148 nets_with_pin: Set[str] = set() 

149 for pin in top_circuit.each_pin(): 

150 net = top_circuit.net_for_pin(pin.id()) 

151 port_names = summary.label_ports.get(net.expanded_name(), None) if net else None 

152 if port_names is None: 

153 ports.append((pin.name(), net)) 

154 continue 

155 nets_with_pin.add(net.expanded_name()) 

156 # NOTE: a net that only exclusion blocks give other ports stays a port, as what instantiates this 

157 # connects it, also if its label has no pin shape, unless only a cut cell names it, 

158 # and the substrate net also if no label names it, as its node has its capacitances 

159 if pin.name() not in port_names and \ 

160 set(port_names) <= set(summary.exclusion_ports.get(net.expanded_name(), ())) and \ 

161 (net.expanded_name() == substrate_net_name 

162 or not any(results.exclusions is not None 

163 and results.exclusions.is_only_named_by_cut_cells(net.expanded_name()) 

164 for results in extraction_results.cell_extraction_results.values())): 

165 ports.append((pin.name(), net)) 

166 for port_name in port_names: 

167 add_net_if_needed(port_name) 

168 ports.append((port_name, name2net[port_name])) 

169 # NOTE: a net without a label has its ports at exclusion markers only 

170 for net_name, port_names in sorted(summary.exclusion_ports.items()): 

171 if net_name not in nets_with_pin: 

172 for port_name in summary.label_ports[net_name]: 

173 add_net_if_needed(port_name) 

174 ports.append((port_name, name2net[port_name])) 

175 for pin in list(top_circuit.each_pin()): 

176 top_circuit.remove_pin(pin.id()) 

177 for port_name, net in ports: 

178 pin = top_circuit.create_pin(port_name) 

179 if net is not None: 

180 top_circuit.connect_pin(pin, net) 

181 

182 for idx, (key, cap_value_femto) in enumerate(cap_items): 

183 net1 = name2net[key.net1] 

184 net2 = name2net[key.net2] 

185 if net1 == net2 and SUBSTRATE in (key.net1, key.net2): 

186 continue # e.g. the metal of the substrate net itself (VSS), shorted 

187 

188 cap_value_farad = cap_value_femto / 1e15 

189 

190 c: kdb.Device = top_circuit.create_device(cap, f"ext_{idx+1}") 

191 c.connect_terminal('A', net1) 

192 c.connect_terminal('B', net2) 

193 c.set_parameter('C', cap_value_farad) 

194 if net1 == net2: 

195 warning(f"Invalid attempt to create cap {c.name} between " 

196 f"same net {net1} with value {'%.12g' % cap_value_femto} fF") 

197 

198 for idx, (key, res_value) in enumerate(res_items): 

199 net1 = name2net[key.net1] 

200 net2 = name2net[key.net2] 

201 

202 r: kdb.Device = top_circuit.create_device(res, f"ext_{idx+1}") 

203 r.connect_terminal('A', net1) 

204 r.connect_terminal('B', net2) 

205 r.set_parameter('R', res_value) 

206 if net1 == net2: 

207 warning(f"Invalid attempt to create resistor {r.name} between " 

208 f"same net {net1} with value {'%.12g' % res_value}") 

209 

210 return expanded_netlist