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

96 statements  

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

24import allure 

25from datetime import datetime 

26import os 

27import tempfile 

28import unittest 

29 

30import google.protobuf.json_format 

31import klayout.db as kdb 

32 

33from klayout_pex.device_models import DeviceModelError, DeviceModels 

34from klayout_pex.klayout.netlist_printer import NetlistHeader, NetlistPrinter 

35from klayout_pex.klayout.parasitic_device_classes import ( 

36 PARASITIC_CAPACITOR_CLASS_NAME, 

37 PARASITIC_RESISTOR_CLASS_NAME, 

38) 

39import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

40 

41 

42HEADER = NetlistHeader(kpex_version='1.2.3', 

43 extraction_engine='KPEX/2.5D', 

44 tech='ihp_sg13g2', 

45 date=datetime(2026, 9, 30, 18, 15, 1)) 

46 

47 

48def device_model_mapping(**kwargs) -> device_models_pb2.DeviceModelMapping: 

49 return google.protobuf.json_format.ParseDict(kwargs, device_models_pb2.DeviceModelMapping()) 

50 

51 

52# as the IHP RF MOS: a device class only LVS knows, written as the base MOS subcircuit 

53RFNMOS = device_model_mapping(lvs_device_class_name='rfnmos', 

54 spice_prefix='X', 

55 model_name='sg13_lv_nmos', 

56 terminal_names=['D', 'G', 'S', 'B'], 

57 parameters=[{'name': 'w', 'lvs_parameter_name': 'W', 'lvs_parameter_factor': 1e-6}, 

58 {'name': 'l', 'lvs_parameter_name': 'L', 'lvs_parameter_factor': 1e-6}, 

59 {'name': 'rfmode', 'constant': 1}]) 

60 

61# as the IHP antenna diode: a model of w and l, which LVS extracts by area and perimeter 

62DANTENNA = device_model_mapping(lvs_device_class_name='dantenna', 

63 spice_prefix='X', 

64 terminal_names=['A', 'C'], 

65 parameters=[{'name': 'w', 'lvs_parameter_factor': 1e-6, 

66 'lvs_area_perimeter_side': {'area_parameter_name': 'A', 

67 'perimeter_parameter_name': 'P', 

68 'side': 'SIDE_LONG'}}, 

69 {'name': 'l', 'lvs_parameter_factor': 1e-6, 

70 'lvs_area_perimeter_side': {'area_parameter_name': 'A', 

71 'perimeter_parameter_name': 'P', 

72 'side': 'SIDE_SHORT'}}]) 

73 

74 

75def empty_netlist() -> kdb.Netlist: 

76 netlist = kdb.Netlist() 

77 circuit = kdb.Circuit() 

78 circuit.name = 'TOP' 

79 netlist.add(circuit) 

80 return netlist 

81 

82 

83def netlist_with_parasitics() -> kdb.Netlist: 

84 """ 

85 A netlist with the parasitics KPEX adds 

86 """ 

87 netlist = empty_netlist() 

88 circuit = netlist.circuit_by_name('TOP') 

89 a = circuit.create_net('a') 

90 b = circuit.create_net('b') 

91 

92 cap = kdb.DeviceClassCapacitor() 

93 cap.name = PARASITIC_CAPACITOR_CLASS_NAME 

94 netlist.add(cap) 

95 c = circuit.create_device(cap, 'ext_1') 

96 c.connect_terminal('A', a) 

97 c.connect_terminal('B', b) 

98 c.set_parameter('C', 1e-15) 

99 

100 res = kdb.DeviceClassResistor() 

101 res.name = PARASITIC_RESISTOR_CLASS_NAME 

102 netlist.add(res) 

103 r = circuit.create_device(res, 'ext_1') 

104 r.connect_terminal('A', a) 

105 r.connect_terminal('B', b) 

106 r.set_parameter('R', 12.5) 

107 

108 return netlist 

109 

110 

111def lvs_netlist() -> kdb.Netlist: 

112 """ 

113 An LVS netlist with a single rfnmos, expanded by parasitics, as KPEX does 

114 """ 

115 netlist = netlist_with_parasitics() 

116 circuit = netlist.circuit_by_name('TOP') 

117 

118 mos = kdb.DeviceClassMOS4Transistor() 

119 mos.name = 'rfnmos' 

120 netlist.add(mos) 

121 

122 # NOTE: a device class without devices 

123 unused = kdb.DeviceClassResistor() 

124 unused.name = 'rppd' 

125 netlist.add(unused) 

126 

127 d = circuit.create_device(mos, 'M1') 

128 for t, net_name in (('S', 'source'), ('G', 'gate'), ('D', 'drain'), ('B', 'bulk')): 

129 d.connect_terminal(t, circuit.create_net(net_name)) 

130 d.set_parameter('W', 1.0) 

131 d.set_parameter('L', 0.72) 

132 

133 return netlist 

134 

135 

136@allure.parent_suite("Unit Tests") 

137@allure.tag("Netlist", "SPICE") 

138class Test(unittest.TestCase): 

139 @staticmethod 

140 def printer(*device_model_mappings: device_models_pb2.DeviceModelMapping) -> NetlistPrinter: 

141 device_models = device_models_pb2.DeviceModelsInfo(device_model_mappings=device_model_mappings) 

142 return NetlistPrinter(header=HEADER, 

143 device_models=DeviceModels(device_models)) 

144 

145 @staticmethod 

146 def lines(printer: NetlistPrinter, netlist: kdb.Netlist) -> list[str]: 

147 with tempfile.TemporaryDirectory() as tmp_dir: 

148 path = os.path.join(tmp_dir, 'netlist.cir') 

149 printer.write(netlist, path) 

150 with open(path) as f: 

151 return [l.strip() for l in f if l.strip()] 

152 

153 @classmethod 

154 def device_lines(cls, printer: NetlistPrinter, netlist: kdb.Netlist) -> list[str]: 

155 return [l for l in cls.lines(printer, netlist) if l[0] not in '*.'] 

156 

157 def test_header_is_written_as_comments(self): 

158 self.assertEqual([ 

159 '*********************************************************', 

160 '*** NGSPICE file created by KLayout-PEX 1.2.3', 

161 '*** -----------------------------------------------------', 

162 '*** Extraction Engine: KPEX/2.5D', 

163 '*** Technology: ihp_sg13g2', 

164 '*** Date: 2026-09-30 18:15:01', 

165 '*********************************************************', 

166 ], self.lines(self.printer(), empty_netlist())[:7]) 

167 

168 def test_parasitics_are_written_without_a_model(self): 

169 self.assertEqual([ 

170 'Cext_1 a b 1f', 

171 'Rext_1 a b 12.5', 

172 ], self.device_lines(self.printer(), netlist_with_parasitics())) 

173 

174 def test_lvs_device_is_written_with_its_device_model_mapping(self): 

175 self.assertEqual([ 

176 'Cext_1 a b 1f', 

177 'Rext_1 a b 12.5', 

178 'XM1 drain gate source bulk sg13_lv_nmos w=1e-06 l=7.2e-07 rfmode=1', 

179 ], self.device_lines(self.printer(RFNMOS), lvs_netlist())) 

180 

181 def test_lvs_device_without_device_model_mapping_is_an_error(self): 

182 # NOTE: the parasitics KPEX added need no device model mapping 

183 with self.assertRaises(DeviceModelError) as ctx: 

184 self.device_lines(self.printer(), lvs_netlist()) 

185 self.assertEqual("The device model mappings of the tech info are missing or invalid " 

186 "for these LVS device classes:\n" 

187 " - rfnmos (1 devices): no mapping", 

188 str(ctx.exception)) 

189 

190 def test_rectangle_sides_are_written_for_area_and_perimeter(self): 

191 netlist = empty_netlist() 

192 circuit = netlist.circuit_by_name('TOP') 

193 diode = kdb.DeviceClassDiode() 

194 diode.name = 'dantenna' 

195 netlist.add(diode) 

196 d = circuit.create_device(diode, 'D1') 

197 d.connect_terminal('A', circuit.create_net('sub')) 

198 d.connect_terminal('C', circuit.create_net('pin')) 

199 d.set_parameter('A', 0.48 * 0.78) # µm² 

200 d.set_parameter('P', 2 * (0.48 + 0.78)) # µm 

201 self.assertEqual([ 

202 'XD1 sub pin dantenna w=7.8e-07 l=4.8e-07', 

203 ], self.device_lines(self.printer(DANTENNA), netlist))