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
« 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#
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 *
31import klayout.db as kdb
33import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
36class DeviceModelError(Exception):
37 """
38 The device model mappings are missing or invalid for devices of a netlist
39 """
40 pass
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
60class DeviceModels:
61 """Helper class for Protocol Buffer device_models_pb2.DeviceModelsInfo"""
63 LVSDeviceClassName = str
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 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}
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.
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)]
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 )
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")
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")
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