Coverage for klayout_pex/device_models.py: 100%

73 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# 

24 

25from __future__ import annotations # allow class type hints within same class 

26from collections import Counter 

27from functools import cached_property 

28import math 

29from typing import * 

30 

31import klayout.db as kdb 

32 

33import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

34 

35 

36class DeviceModelError(Exception): 

37 """ 

38 The device model mappings are missing or invalid for devices of a netlist 

39 """ 

40 pass 

41 

42 

43def rectangle_sides(area: float, perimeter: float) -> Tuple[float, float]: 

44 """ 

45 :return: the long and the short side of the rectangle with this area and perimeter, 

46 or the sides of the square of this area, if there is no such rectangle 

47 (perimeter² < 16 · area, e.g. an octagon) 

48 """ 

49 # NOTE: the sides are the roots of x² - perimeter/2 · x + area 

50 half_sum = perimeter / 4 

51 discriminant = half_sum * half_sum - area 

52 if discriminant <= 0: 

53 side = math.sqrt(area) 

54 return side, side 

55 long_side = half_sum + math.sqrt(discriminant) 

56 # NOTE: not half_sum - sqrt(discriminant), which cancels out for a long, narrow rectangle 

57 return long_side, area / long_side 

58 

59 

60class DeviceModels: 

61 """Helper class for Protocol Buffer device_models_pb2.DeviceModelsInfo""" 

62 

63 LVSDeviceClassName = str 

64 

65 def __init__(self, device_models: device_models_pb2.DeviceModelsInfo): 

66 self.device_models = device_models 

67 

68 @cached_property 

69 def mapping_by_lvs_device_class_name(self) -> Dict[LVSDeviceClassName, device_models_pb2.DeviceModelMapping]: 

70 return {m.lvs_device_class_name: m for m in self.device_models.device_model_mappings} 

71 

72 @cached_property 

73 def metal_capacitor_class_names(self) -> Set[LVSDeviceClassName]: 

74 """ 

75 The LVS device classes of the metal capacitors (e.g. MIM and MOM caps), 

76 whose capacitance the extraction has from their geometry, unless black-boxed 

77 """ 

78 return {m.lvs_device_class_name for m in self.device_models.device_model_mappings 

79 if m.kind == device_models_pb2.DeviceModelMapping.KIND_METAL_CAPACITOR} 

80 

81 def check_mappings(self, 

82 netlist: kdb.Netlist, 

83 ignored_device_class_names: Set[LVSDeviceClassName] = frozenset()): 

84 """ 

85 Raises DeviceModelError, if the mapping of a device class of the netlist is missing or invalid. 

86 

87 Only device classes that have devices in the netlist are checked. 

88 """ 

89 device_count_by_class_name = Counter(d.device_class().name 

90 for c in netlist.each_circuit() 

91 for d in c.each_device()) 

92 problems: List[str] = [] 

93 for dc in netlist.each_device_class(): 

94 if dc.name in ignored_device_class_names: 

95 continue 

96 device_count = device_count_by_class_name[dc.name] 

97 if device_count == 0: 

98 continue 

99 mapping = self.mapping_by_lvs_device_class_name.get(dc.name) 

100 if mapping is None: 

101 problems.append(f"{dc.name} ({device_count} devices): no mapping") 

102 continue 

103 problems += [f"{dc.name}: {p}" for p in self.mapping_problems(dc, mapping)] 

104 

105 if problems: 

106 raise DeviceModelError( 

107 "The device model mappings of the tech info are missing or invalid " 

108 "for these LVS device classes:\n" + 

109 '\n'.join(f" - {p}" for p in problems) 

110 ) 

111 

112 @staticmethod 

113 def mapping_problems(dc: kdb.DeviceClass, 

114 mapping: device_models_pb2.DeviceModelMapping) -> List[str]: 

115 problems: List[str] = [] 

116 if not mapping.spice_prefix: 

117 problems.append("no SPICE prefix") 

118 

119 lvs_terminal_names = [td.name for td in dc.terminal_definitions()] 

120 for t in mapping.terminal_names: 

121 if t not in lvs_terminal_names: 

122 problems.append(f"no LVS terminal '{t}' (terminals are {', '.join(lvs_terminal_names)})") 

123 # NOTE: a terminal left out would silently disconnect the device from its net 

124 for t in lvs_terminal_names: 

125 if t not in mapping.terminal_names: 

126 problems.append(f"LVS terminal '{t}' is missing in the terminal names") 

127 

128 lvs_parameter_names = [pd.name for pd in dc.parameter_definitions()] 

129 for p in mapping.parameters: 

130 match p.WhichOneof('value'): 

131 case 'lvs_parameter_name': 

132 if p.lvs_parameter_name not in lvs_parameter_names: 

133 problems.append(f"parameter '{p.name}': no LVS parameter '{p.lvs_parameter_name}' " 

134 f"(parameters are {', '.join(lvs_parameter_names)})") 

135 case 'lvs_area_perimeter_side': 

136 side = p.lvs_area_perimeter_side 

137 for lvs_parameter_name in (side.area_parameter_name, side.perimeter_parameter_name): 

138 if lvs_parameter_name not in lvs_parameter_names: 

139 problems.append(f"parameter '{p.name}': no LVS parameter '{lvs_parameter_name}' " 

140 f"(parameters are {', '.join(lvs_parameter_names)})") 

141 if side.side == device_models_pb2.LVSAreaPerimeterSide.SIDE_UNSPECIFIED: 

142 problems.append(f"parameter '{p.name}': no side of the rectangle") 

143 case 'constant': 

144 pass 

145 case _: 

146 problems.append(f"parameter '{p.name}': no value") 

147 return problems