Coverage for klayout_pex/device_models.py: 100%

126 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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, defaultdict 

27from functools import cached_property 

28import math 

29from typing import * 

30 

31import klayout.db as kdb 

32 

33from .netlist.spice_netlist import SpiceElement 

34from .types import LVSDeviceClassName 

35import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

36 

37 

38class DeviceModelError(Exception): 

39 """ 

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

41 """ 

42 pass 

43 

44 

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

46 """ 

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

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

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

50 """ 

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

52 half_sum = perimeter / 4 

53 discriminant = half_sum * half_sum - area 

54 if discriminant <= 0: 

55 side = math.sqrt(area) 

56 return side, side 

57 long_side = half_sum + math.sqrt(discriminant) 

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

59 return long_side, area / long_side 

60 

61 

62class DeviceModels: 

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

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 mappings_by_spice_model(self) -> Dict[Tuple[str, str], List[device_models_pb2.DeviceModelMapping]]: 

74 """ 

75 The mappings by the SPICE prefix and the model they write, in uppercase (SPICE reads names regardless of case), 

76 e.g. ('X', 'SG13_LV_NMOS') for sg13_lv_nmos and rfnmos 

77 """ 

78 mappings: Dict[Tuple[str, str], List[device_models_pb2.DeviceModelMapping]] = defaultdict(list) 

79 for m in self.device_models.device_model_mappings: 

80 mappings[(m.spice_prefix.upper(), (m.model_name or m.lvs_device_class_name).upper())].append(m) 

81 return mappings 

82 

83 def lvs_device_class_name(self, element: SpiceElement) -> Optional[LVSDeviceClassName]: 

84 """ 

85 The LVS device class of a device of a schematic, the reverse of the mappings: 

86 a model the mappings write with their constant parameters 

87 (e.g. X1 d g s b sg13_lv_nmos w=1u l=0.13u rfmode=1 is an rfnmos, without rfmode an sg13_lv_nmos), 

88 or else a model named as an LVS device class (e.g. M1 d g s b sky130_fd_pr__nfet_01v8, as LVS reads it) 

89 

90 :param element: with its parameters as numbers (e.g. of SpiceHierarchy) 

91 :return: None, if neither, e.g. a device whose class has no mapping 

92 """ 

93 if element.model is None: 

94 return None 

95 

96 candidates = [m for m in self.mappings_by_spice_model.get((element.kind, element.model.upper()), []) 

97 if all(element.parameters.get(name) == value 

98 for name, value in self.constant_parameters(m).items())] 

99 if candidates: 

100 # NOTE: the one with the most constants, e.g. rfnmos, which an sg13_lv_nmos with rfmode=1 matches too 

101 return max(candidates, key=lambda m: len(self.constant_parameters(m))).lvs_device_class_name 

102 for name in self.mapping_by_lvs_device_class_name: 

103 if name.upper() == element.model.upper(): 

104 return name 

105 return None 

106 

107 def mappings_of_model(self, element: SpiceElement) -> List[device_models_pb2.DeviceModelMapping]: 

108 """ 

109 The mappings a device of a schematic may be of, by its model alone (see lvs_device_class_name) 

110 

111 :param element: whose parameters may be expressions yet 

112 """ 

113 if element.model is None: 

114 return [] 

115 model = element.model.upper() 

116 return [*self.mappings_by_spice_model.get((element.kind, model), []), 

117 *(m for name, m in self.mapping_by_lvs_device_class_name.items() if name.upper() == model)] 

118 

119 @staticmethod 

120 def constant_parameters(mapping: device_models_pb2.DeviceModelMapping) -> Dict[str, float]: 

121 """ 

122 The parameters the mapping writes as constants, in lowercase, e.g. {'rfmode': 1.0} of an rfnmos 

123 """ 

124 return {p.name.lower(): p.constant for p in mapping.parameters if p.WhichOneof('value') == 'constant'} 

125 

126 @staticmethod 

127 def lvs_parameter_values(mapping: device_models_pb2.DeviceModelMapping, 

128 element: SpiceElement) -> Dict[str, float]: 

129 """ 

130 The LVS parameters of a device of a schematic, the reverse of the mapping's parameters 

131 (e.g. W = 1, in µm, of w=1u of an IHP MOS), floored as by its model (see lvs_parameter_floor_pitch), 

132 and the area and perimeter of the rectangle of a model's sides 

133 

134 :param element: with its parameters as numbers (e.g. of SpiceHierarchy) 

135 :return: by LVS parameter name, without those whose model parameters the device doesn't give 

136 (e.g. left to the model's default) 

137 """ 

138 values: Dict[str, float] = {} 

139 sides: List[Tuple[device_models_pb2.LVSAreaPerimeterSide, float]] = [] 

140 for p in mapping.parameters: 

141 value = element.parameters.get(p.name.lower()) 

142 if value is None: 

143 continue 

144 factor = p.lvs_parameter_factor if p.HasField('lvs_parameter_factor') else 1.0 

145 match p.WhichOneof('value'): 

146 case 'lvs_parameter_name': 

147 value /= factor 

148 if p.HasField('lvs_parameter_floor_pitch'): 

149 # NOTE: with the epsilon of such a model, so that a multiple stays one 

150 value = math.floor(value / p.lvs_parameter_floor_pitch + 1e-6) * p.lvs_parameter_floor_pitch 

151 values[p.lvs_parameter_name] = value 

152 case 'lvs_area_perimeter_side': 

153 sides.append((p.lvs_area_perimeter_side, value / factor)) 

154 if len(sides) == 2: 

155 (side, a), (_, b) = sides 

156 values[side.area_parameter_name] = a * b 

157 values[side.perimeter_parameter_name] = 2 * (a + b) 

158 return values 

159 

160 @staticmethod 

161 def multiplier_lvs_parameter_name(mapping: device_models_pb2.DeviceModelMapping) -> Optional[str]: 

162 """ 

163 The LVS parameter the mapping writes as the multiplier m of the model (e.g. NE of a BJT), 

164 None if it writes no m 

165 """ 

166 return next((p.lvs_parameter_name for p in mapping.parameters 

167 if p.name.lower() == 'm' and p.WhichOneof('value') == 'lvs_parameter_name'), None) 

168 

169 @cached_property 

170 def vacask_subcircuits_with_m(self) -> Set[str]: 

171 """ 

172 The models whose subcircuits in the PDK's VACASK models declare the multiplier m 

173 (VACASK rejects m on the others) 

174 """ 

175 return {m.model_name or m.lvs_device_class_name for m in self.device_models.device_model_mappings 

176 if m.vacask_declares_m} 

177 

178 @cached_property 

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

180 """ 

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

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

183 """ 

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

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

186 

187 def check_mappings(self, 

188 netlist: kdb.Netlist, 

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

190 """ 

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

192 

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

194 """ 

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

196 for c in netlist.each_circuit() 

197 for d in c.each_device()) 

198 problems: List[str] = [] 

199 for dc in netlist.each_device_class(): 

200 if dc.name in ignored_device_class_names: 

201 continue 

202 device_count = device_count_by_class_name[dc.name] 

203 if device_count == 0: 

204 continue 

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

206 if mapping is None: 

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

208 continue 

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

210 

211 if problems: 

212 raise DeviceModelError( 

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

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

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

216 ) 

217 

218 @staticmethod 

219 def mapping_problems(dc: kdb.DeviceClass, 

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

221 problems: List[str] = [] 

222 if not mapping.spice_prefix: 

223 problems.append("no SPICE prefix") 

224 

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

226 for t in mapping.terminal_names: 

227 if t not in lvs_terminal_names: 

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

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

230 for t in lvs_terminal_names: 

231 if t not in mapping.terminal_names: 

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

233 

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

235 for p in mapping.parameters: 

236 match p.WhichOneof('value'): 

237 case 'lvs_parameter_name': 

238 if p.lvs_parameter_name not in lvs_parameter_names: 

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

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

241 case 'lvs_area_perimeter_side': 

242 side = p.lvs_area_perimeter_side 

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

244 if lvs_parameter_name not in lvs_parameter_names: 

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

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

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

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

249 case 'constant': 

250 pass 

251 case _: 

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

253 return problems