Coverage for tests/klayout/netlist_expander_test.py: 100%

88 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 allure 

27import os 

28import tempfile 

29import unittest 

30 

31import klayout.db as kdb 

32 

33from klayout_pex.device_models import DeviceModels 

34from klayout_pex.fastercap.output_interpreter import FasterCapOutputInterpreter 

35from klayout_pex.klayout.lvsdb_extractor import KLayoutExtractionContext 

36from klayout_pex.klayout.netlist_expander import NetlistExpander 

37from klayout_pex.log import ( 

38 debug, 

39) 

40from klayout_pex.common.capacitance_matrix import CapacitanceMatrix 

41from klayout_pex.tech_info import TechInfo 

42import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

43 

44 

45@allure.parent_suite("Unit Tests") 

46@allure.tag("Netlist", "Netlist Expansion") 

47class Test(unittest.TestCase): 

48 @property 

49 def klayout_testdata_dir(self) -> str: 

50 return os.path.realpath(os.path.join(__file__, '..', '..', '..', 

51 'testdata', 'fastercap')) 

52 

53 @property 

54 def tech_info_json_path(self) -> str: 

55 return os.path.realpath(os.path.join(__file__, '..', '..', '..', 

56 'klayout_pex_protobuf', 'sky130A_tech.pb.json')) 

57 

58 def test_netlist_expansion(self): 

59 exp = NetlistExpander() 

60 

61 cell_name = 'nmos_diode2' 

62 

63 lvsdb = kdb.LayoutVsSchematic() 

64 lvsdb_path = os.path.join(self.klayout_testdata_dir, f"{cell_name}.lvsdb.gz") 

65 lvsdb.read(lvsdb_path) 

66 

67 csv_path = os.path.join(self.klayout_testdata_dir, f"{cell_name}_FasterCap_Result_Matrix.csv") 

68 

69 cap_matrix = CapacitanceMatrix.parse_csv(csv_path, separator=';') 

70 

71 tech = TechInfo.from_json(self.tech_info_json_path, 

72 dielectric_filter=None) 

73 

74 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=cell_name, 

75 lvsdb=lvsdb, 

76 tech=tech, 

77 blackbox_devices=False) 

78 expanded_netlist = exp.expand(extracted_netlist=pex_context.lvsdb.netlist(), 

79 top_cell_name=pex_context.annotated_top_cell.name, 

80 cap_matrix=cap_matrix, 

81 cap_matrix_interpreter=FasterCapOutputInterpreter(), 

82 blackbox_devices=False, 

83 device_models=tech.device_models, 

84 substrate_net_name='VSUBS') # NOTE: the substrate conductor of the matrix 

85 out_path = tempfile.mktemp(prefix=f"{cell_name}_expanded_netlist_", suffix=".cir") 

86 spice_writer = kdb.NetlistSpiceWriter() 

87 expanded_netlist.write(out_path, spice_writer) 

88 debug(f"Wrote expanded netlist to: {out_path}") 

89 

90 allure.attach.file(csv_path, attachment_type=allure.attachment_type.CSV) 

91 allure.attach.file(out_path, attachment_type=allure.attachment_type.TEXT) 

92 

93 

94def device_models(*metal_capacitor_class_names: str) -> DeviceModels: 

95 info = device_models_pb2.DeviceModelsInfo() 

96 for name in metal_capacitor_class_names: 

97 info.device_model_mappings.add(lvs_device_class_name=name, 

98 kind=device_models_pb2.DeviceModelMapping.KIND_METAL_CAPACITOR) 

99 return DeviceModels(info) 

100 

101 

102def substrate_test_netlist() -> kdb.Netlist: 

103 """ 

104 The nets D and B, with a pin each 

105 """ 

106 netlist = kdb.Netlist() 

107 circuit = kdb.Circuit() 

108 circuit.name = 'chip' 

109 netlist.add(circuit) 

110 for name in ('D', 'B'): 

111 circuit.connect_pin(circuit.create_pin(name), circuit.create_net(name)) 

112 return netlist 

113 

114 

115# the substrate on the net B, as FasterCapInputBuilder names it after the substrate net: 

116# B to D 1.5 fF, and the rest of the diagonal (to infinity) of D 0.25 fF 

117CAP_MATRIX_SUBSTRATE_ON_B = CapacitanceMatrix(conductor_names=['g1_B', 'g2_D'], 

118 rows=[[2.0e-15, -1.5e-15], 

119 [-1.5e-15, 1.75e-15]]) 

120 

121# the substrate a conductor of its own: to D 1 fF and to B 2 fF, D to B 0.5 fF, 

122# and the rest of the diagonal (to infinity): D 0.25 fF, B 0.1 fF 

123CAP_MATRIX_SUBSTRATE_PORT = CapacitanceMatrix(conductor_names=['g1_sky130_gnd', 'g2_D', 'g3_B'], 

124 rows=[[3.0e-15, -1.0e-15, -2.0e-15], 

125 [-1.0e-15, 1.75e-15, -0.5e-15], 

126 [-2.0e-15, -0.5e-15, 2.6e-15]]) 

127 

128 

129def capacitances_by_net_pair(expanded: kdb.Netlist) -> dict: 

130 caps = {} 

131 for d in expanded.circuit_by_name('chip').each_device(): 

132 pair = frozenset((d.net_for_terminal('A').name, d.net_for_terminal('B').name)) 

133 caps[pair] = round(caps.get(pair, 0.0) + d.parameter('C') * 1e15, 6) 

134 return caps 

135 

136 

137@allure.parent_suite("Unit Tests") 

138@allure.tag("Netlist", "Netlist Expansion") 

139class SubstrateTest(unittest.TestCase): 

140 def test_capacitances_to_the_substrate_are_on_its_net(self): 

141 expanded = NetlistExpander.expand(substrate_test_netlist(), 'chip', CAP_MATRIX_SUBSTRATE_ON_B, 

142 FasterCapOutputInterpreter(), blackbox_devices=True, 

143 device_models=device_models(), substrate_net_name='B') 

144 circuit = expanded.circuit_by_name('chip') 

145 # NOTE: the rest of the diagonal of D goes to the substrate B too 

146 self.assertEqual({frozenset(('D', 'B')): 1.75}, capacitances_by_net_pair(expanded)) 

147 self.assertEqual(['D', 'B'], [p.name() for p in circuit.each_pin()]) 

148 for name in ('VSUBS', 'FC_GND', 'GND'): 

149 self.assertIsNone(circuit.net_by_name(name)) 

150 

151 def test_substrate_without_net_is_a_port(self): 

152 expanded = NetlistExpander.expand(substrate_test_netlist(), 'chip', CAP_MATRIX_SUBSTRATE_PORT, 

153 FasterCapOutputInterpreter(), blackbox_devices=True, 

154 device_models=device_models(), substrate_net_name='sky130_gnd') 

155 circuit = expanded.circuit_by_name('chip') 

156 self.assertEqual({frozenset(('sky130_gnd', 'D')): 1.25, 

157 frozenset(('sky130_gnd', 'B')): 2.1, 

158 frozenset(('D', 'B')): 0.5}, 

159 capacitances_by_net_pair(expanded)) 

160 self.assertEqual(['D', 'B', 'sky130_gnd'], [p.name() for p in circuit.each_pin()]) 

161 

162 

163@allure.parent_suite("Unit Tests") 

164@allure.tag("Netlist", "Netlist Expansion") 

165class WhiteboxTest(unittest.TestCase): 

166 def test_whitebox_removes_the_metal_capacitors_only(self): 

167 # White-box mode removed all devices, e.g. the transistors too 

168 netlist = substrate_test_netlist() 

169 circuit = netlist.circuit_by_name('chip') 

170 for device_class, class_name, device_name in ((kdb.DeviceClassCapacitor(), 'mim', 'C1'), 

171 (kdb.DeviceClassMOS4Transistor(), 'nmos', 'M1')): 

172 device_class.name = class_name 

173 netlist.add(device_class) 

174 circuit.create_device(device_class, device_name) 

175 

176 expanded = NetlistExpander.expand(netlist, 'chip', CAP_MATRIX_SUBSTRATE_ON_B, 

177 FasterCapOutputInterpreter(), blackbox_devices=False, 

178 device_models=device_models('mim'), substrate_net_name='B') 

179 self.assertEqual(['M1'], [d.name for d in expanded.circuit_by_name('chip').each_device() 

180 if d.device_class().name in ('mim', 'nmos')])