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

97 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 23:32 +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.parasitic_device_classes import PARASITIC_CAPACITOR_CLASS_NAME, PARASITIC_RESISTOR_CLASS_NAME 

38 

39 

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

41SUBSTRATE = 'VSUBS' 

42 

43 

44class RCX25NetlistExpander: 

45 @staticmethod 

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

47 """ 

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

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

50 

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

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

53 while the MOS capacitors and varactors are capacitor classes, 

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

55 """ 

56 return device_class.name in device_models.metal_capacitor_class_names 

57 

58 @staticmethod 

59 def expand(extracted_netlist: kdb.Netlist, 

60 top_cell_name: str, 

61 extraction_results: ExtractionResults, 

62 blackbox_devices: bool, 

63 device_models: DeviceModels, 

64 substrate_net_name: Optional[str] = None) -> kdb.Netlist: 

65 """ 

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

67 :param substrate_net_name: the net the capacitances to the substrate go to, 

68 a port of its own if the netlist has none (VSUBS if not given) 

69 """ 

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

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

72 

73 if not blackbox_devices: 

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

75 # so iterate over a list of them 

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

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

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

79 top_circuit.remove_device(d) 

80 

81 # create capacitor device class 

82 cap = kdb.DeviceClassCapacitor() 

83 # cap.name = 'KPEX_CAP' 

84 cap.name = PARASITIC_CAPACITOR_CLASS_NAME 

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

86 expanded_netlist.add(cap) 

87 

88 # create resistor device class 

89 res = kdb.DeviceClassResistor() 

90 # res.name = 'KPEX_RES' 

91 res.name = PARASITIC_RESISTOR_CLASS_NAME 

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

93 expanded_netlist.add(res) 

94 

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

96 

97 summary = extraction_results.summarize() 

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

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

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 def add_net_if_needed(net_name: str): 

105 if net_name in name2net: 

106 return 

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

108 

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

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

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

112 substrate_net_name = substrate_net_name or SUBSTRATE 

113 if substrate_net_name not in name2net: 

114 substrate_net = top_circuit.create_net(substrate_net_name) 

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

116 name2net[substrate_net_name] = substrate_net 

117 name2net[SUBSTRATE] = name2net[substrate_net_name] 

118 

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

120 for key, _ in cap_items: 

121 add_net_if_needed(key.net1) 

122 add_net_if_needed(key.net2) 

123 for key, _ in res_items: 

124 add_net_if_needed(key.net1) 

125 add_net_if_needed(key.net2) 

126 

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

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

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

130 device = top_circuit.device_by_id(key.device_id) 

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

132 continue 

133 add_net_if_needed(node_name) 

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

135 

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

137 if summary.label_ports: 

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

139 for pin in top_circuit.each_pin(): 

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

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

142 if port_names is None: 

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

144 continue 

145 for port_name in port_names: 

146 add_net_if_needed(port_name) 

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

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

149 top_circuit.remove_pin(pin.id()) 

150 for port_name, net in ports: 

151 pin = top_circuit.create_pin(port_name) 

152 if net is not None: 

153 top_circuit.connect_pin(pin, net) 

154 

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

156 net1 = name2net[key.net1] 

157 net2 = name2net[key.net2] 

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

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

160 

161 cap_value_farad = cap_value_femto / 1e15 

162 

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

164 c.connect_terminal('A', net1) 

165 c.connect_terminal('B', net2) 

166 c.set_parameter('C', cap_value_farad) 

167 if net1 == net2: 

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

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

170 

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

172 net1 = name2net[key.net1] 

173 net2 = name2net[key.net2] 

174 

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

176 r.connect_terminal('A', net1) 

177 r.connect_terminal('B', net2) 

178 r.set_parameter('R', res_value) 

179 if net1 == net2: 

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

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

182 

183 return expanded_netlist