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
« 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#
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 *
31import klayout.db as kdb
33from .netlist.spice_netlist import SpiceElement
34from .types import LVSDeviceClassName
35import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
38class DeviceModelError(Exception):
39 """
40 The device model mappings are missing or invalid for devices of a netlist
41 """
42 pass
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
62class DeviceModels:
63 """Helper class for Protocol Buffer device_models_pb2.DeviceModelsInfo"""
65 def __init__(self, device_models: device_models_pb2.DeviceModelsInfo):
66 self.device_models = device_models
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}
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
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)
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
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
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)
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)]
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'}
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
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
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)
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}
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}
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.
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)]
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 )
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")
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")
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