Coverage for tests/device_models_test.py: 100%

63 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 

25import math 

26import unittest 

27 

28import google.protobuf.json_format 

29import klayout.db as kdb 

30 

31from klayout_pex.device_models import DeviceModelError, DeviceModels, rectangle_sides 

32import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

33 

34 

35def device_models(*mappings: dict) -> DeviceModels: 

36 return DeviceModels(google.protobuf.json_format.ParseDict({'device_model_mappings': list(mappings)}, 

37 device_models_pb2.DeviceModelsInfo())) 

38 

39 

40def netlist() -> kdb.Netlist: 

41 """ 

42 A netlist with an nmos and a resistor, and a capacitor device class without devices 

43 """ 

44 netlist = kdb.Netlist() 

45 circuit = kdb.Circuit() 

46 circuit.name = 'TOP' 

47 netlist.add(circuit) 

48 

49 mos = kdb.DeviceClassMOS4Transistor() 

50 mos.name = 'nmos' 

51 netlist.add(mos) 

52 circuit.create_device(mos, 'M1') 

53 

54 res = kdb.DeviceClassResistor() 

55 res.name = 'rppd' 

56 netlist.add(res) 

57 circuit.create_device(res, 'R1') 

58 circuit.create_device(res, 'R2') 

59 

60 cap = kdb.DeviceClassCapacitor() 

61 cap.name = 'cap_cmim' 

62 netlist.add(cap) 

63 

64 return netlist 

65 

66 

67NMOS = {'lvs_device_class_name': 'nmos', 'spice_prefix': 'X', 'terminal_names': ['D', 'G', 'S', 'B']} 

68RPPD = {'lvs_device_class_name': 'rppd', 'spice_prefix': 'X', 'terminal_names': ['A', 'B']} 

69 

70 

71@allure.parent_suite('Unit Tests') 

72@allure.tag('TechInfo', 'Device Models') 

73class Test(unittest.TestCase): 

74 def test_mapping_by_lvs_device_class_name(self): 

75 self.assertEqual(['nmos', 'rppd'], sorted(device_models(NMOS, RPPD).mapping_by_lvs_device_class_name)) 

76 

77 def test_device_classes_with_mappings_pass(self): 

78 # NOTE: cap_cmim has no mapping, but no devices either 

79 device_models(NMOS, RPPD).check_mappings(netlist()) 

80 

81 def test_missing_mappings_are_reported(self): 

82 with self.assertRaises(DeviceModelError) as ctx: 

83 device_models(NMOS).check_mappings(netlist()) 

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

85 "for these LVS device classes:\n" 

86 " - rppd (2 devices): no mapping", 

87 str(ctx.exception)) 

88 

89 def test_ignored_device_classes_need_no_mapping(self): 

90 device_models(NMOS).check_mappings(netlist(), ignored_device_class_names={'rppd'}) 

91 

92 def test_invalid_mappings_are_reported(self): 

93 invalid_nmos = {'lvs_device_class_name': 'nmos', 

94 'terminal_names': ['D', 'G', 'S', 'X'], 

95 'parameters': [{'name': 'w', 'lvs_parameter_name': 'w'}, 

96 {'name': 'rfmode'}]} 

97 with self.assertRaises(DeviceModelError) as ctx: 

98 device_models(invalid_nmos, RPPD).check_mappings(netlist()) 

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

100 "for these LVS device classes:\n" 

101 " - nmos: no SPICE prefix\n" 

102 " - nmos: no LVS terminal 'X' (terminals are S, G, D, B)\n" 

103 " - nmos: LVS terminal 'B' is missing in the terminal names\n" 

104 " - nmos: parameter 'w': no LVS parameter 'w' (parameters are L, W, AS, AD, PS, PD)\n" 

105 " - nmos: parameter 'rfmode': no value", 

106 str(ctx.exception)) 

107 

108 def test_invalid_rectangle_sides_are_reported(self): 

109 rppd = {'lvs_device_class_name': 'rppd', 'spice_prefix': 'X', 'terminal_names': ['A', 'B'], 

110 'parameters': [{'name': 'w', 'lvs_area_perimeter_side': {'area_parameter_name': 'A', 

111 'perimeter_parameter_name': 'X', 

112 'side': 'SIDE_LONG'}}, 

113 {'name': 'l', 'lvs_area_perimeter_side': {'area_parameter_name': 'A', 

114 'perimeter_parameter_name': 'P'}}]} 

115 with self.assertRaises(DeviceModelError) as ctx: 

116 device_models(NMOS, rppd).check_mappings(netlist()) 

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

118 "for these LVS device classes:\n" 

119 " - rppd: parameter 'w': no LVS parameter 'X' (parameters are R, L, W, A, P)\n" 

120 " - rppd: parameter 'l': no side of the rectangle", 

121 str(ctx.exception)) 

122 

123 def test_rectangle_sides(self): 

124 self.assertEqual((2.0, 1.0), rectangle_sides(area=2.0, perimeter=6.0)) 

125 self.assertEqual((3.0, 3.0), rectangle_sides(area=9.0, perimeter=12.0)) 

126 self.assertEqual((5.0, 0.0), rectangle_sides(area=0.0, perimeter=10.0)) 

127 

128 def test_rectangle_sides_of_a_long_narrow_rectangle_are_exact(self): 

129 long_side, short_side = rectangle_sides(area=1e4 * 1e-4, perimeter=2 * (1e4 + 1e-4)) 

130 self.assertAlmostEqual(1e4, long_side, delta=1e-12 * 1e4) 

131 self.assertAlmostEqual(1e-4, short_side, delta=1e-12 * 1e-4) 

132 

133 def test_shape_without_rectangle_is_the_square_of_its_area(self): 

134 # NOTE: a regular octagon of side 1 has a perimeter of 8, less than 4·√area 

135 area = 2 * (1 + math.sqrt(2)) 

136 self.assertEqual((math.sqrt(area), math.sqrt(area)), rectangle_sides(area=area, perimeter=8.0))