Coverage for klayout_pex/device_totals.py: 100%
57 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#
25"""
26Totals of the devices of an LVS device class that don't depend on how the devices are split
27(into fingers, by m, into series or parallel segments), which --check compares between the schematic
28and the LVS netlist (#345):
29 - the area: W·L, if the device model mapping writes model parameters of the LVS parameters W and L
30 (in any case), or else A, if it writes those of the area (A, or the rectangle of an area and perimeter)
31 - the width W of a MOS
32 - the multiplier, if the mapping writes the model's m of an LVS parameter (e.g. NE of a BJT)
33each times the multiplier: that LVS parameter, else (or if a device of the schematic doesn't give it)
34the m of a device of the schematic (SPICE's multiplier, 1 if not given).
36The totals are in the units of the LVS parameters (e.g. µm² for W·L of a MOS in µm), a device of the schematic
37gets its LVS parameters by the mapping in reverse (see DeviceModels.lvs_parameter_values).
38A class whose mapping writes none of them has no totals (e.g. taps, or caps of a fixed size).
39"""
41from __future__ import annotations
42from dataclasses import dataclass
43import math
44from typing import *
46import klayout.db as kdb
48from .device_models import DeviceModels
49import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
52@dataclass(frozen=True)
53class DeviceTotalParameters:
54 """
55 The LVS parameters of the totals of a device class (see the module docs)
56 """
57 width: Optional[str] = None
58 length: Optional[str] = None
59 area: Optional[str] = None
60 multiplier: Optional[str] = None
61 mos: bool = False
63 @staticmethod
64 def of(device_class: Optional[kdb.DeviceClass],
65 mapping: device_models_pb2.DeviceModelMapping) -> DeviceTotalParameters:
66 """
67 :param device_class: None if only the names of the LVS parameters matter
68 """
69 names: Dict[str, str] = {}
70 area: Optional[str] = None
71 for p in mapping.parameters:
72 match p.WhichOneof('value'):
73 case 'lvs_parameter_name':
74 names.setdefault(p.lvs_parameter_name.upper(), p.lvs_parameter_name)
75 case 'lvs_area_perimeter_side':
76 area = p.lvs_area_perimeter_side.area_parameter_name
77 return DeviceTotalParameters(width=names.get('W'),
78 length=names.get('L'),
79 area=area or names.get('A'),
80 multiplier=DeviceModels.multiplier_lvs_parameter_name(mapping),
81 mos=isinstance(device_class, kdb.DeviceClassMOS3Transistor))
83 @staticmethod
84 def model_parameter_names(mapping: device_models_pb2.DeviceModelMapping) -> Set[str]:
85 """
86 The parameters of a device of a schematic that its totals depend on, in lowercase, with the m of SPICE,
87 e.g. {'w', 'l', 'm'} of a MOS, but not the feed of an IHP MOM cap
88 """
89 totals = DeviceTotalParameters.of(None, mapping)
90 lvs_parameter_names = {totals.width, totals.length, totals.area, totals.multiplier}
91 names = {'m'}
92 for p in mapping.parameters:
93 match p.WhichOneof('value'):
94 case 'lvs_parameter_name' if p.lvs_parameter_name in lvs_parameter_names:
95 names.add(p.name.lower())
96 case 'lvs_area_perimeter_side':
97 names.add(p.name.lower())
98 return names
100 @property
101 def quantity_names(self) -> List[str]:
102 """
103 e.g. ['W·L', 'W'] of a MOS
104 """
105 return list(self.quantities({}))
107 def quantities(self,
108 lvs_parameters: Mapping[str, float],
109 m: float = 1.0) -> Dict[str, Optional[float]]:
110 """
111 The quantities of a device whose totals are compared, e.g. {'W·L': 0.13, 'W': 1.0} of a MOS
113 :param lvs_parameters: of the device, by name
114 :param m: the multiplier of SPICE, if the mapping writes no multiplier, or the device doesn't give it
115 :return: None for a quantity the device has no value for
116 (e.g. a parameter the schematic leaves to the model's default)
117 """
118 multiplier = lvs_parameters.get(self.multiplier, m) if self.multiplier else m
120 def product(*names: str) -> Optional[float]:
121 values = [lvs_parameters.get(n) for n in names]
122 if None in values:
123 return None
124 return math.prod(values) * multiplier
126 quantities: Dict[str, Optional[float]] = {}
127 if self.width and self.length:
128 quantities[f"{self.width}·{self.length}"] = product(self.width, self.length)
129 elif self.area:
130 quantities[self.area] = product(self.area)
131 if self.mos and self.width:
132 quantities[self.width] = product(self.width)
133 if self.multiplier:
134 quantities[self.multiplier] = multiplier
135 return quantities