Coverage for tests/device_models_test.py: 100%
63 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#
24import allure
25import math
26import unittest
28import google.protobuf.json_format
29import klayout.db as kdb
31from klayout_pex.device_models import DeviceModelError, DeviceModels, rectangle_sides
32import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
35def device_models(*mappings: dict) -> DeviceModels:
36 return DeviceModels(google.protobuf.json_format.ParseDict({'device_model_mappings': list(mappings)},
37 device_models_pb2.DeviceModelsInfo()))
40def netlist() -> kdb.Netlist:
41 """
42 A netlist with an nmos and a resistor, and a capacitor device class without devices
43 """
44 netlist = kdb.Netlist()
45 circuit = kdb.Circuit()
46 circuit.name = 'TOP'
47 netlist.add(circuit)
49 mos = kdb.DeviceClassMOS4Transistor()
50 mos.name = 'nmos'
51 netlist.add(mos)
52 circuit.create_device(mos, 'M1')
54 res = kdb.DeviceClassResistor()
55 res.name = 'rppd'
56 netlist.add(res)
57 circuit.create_device(res, 'R1')
58 circuit.create_device(res, 'R2')
60 cap = kdb.DeviceClassCapacitor()
61 cap.name = 'cap_cmim'
62 netlist.add(cap)
64 return netlist
67NMOS = {'lvs_device_class_name': 'nmos', 'spice_prefix': 'X', 'terminal_names': ['D', 'G', 'S', 'B']}
68RPPD = {'lvs_device_class_name': 'rppd', 'spice_prefix': 'X', 'terminal_names': ['A', 'B']}
71@allure.parent_suite('Unit Tests')
72@allure.tag('TechInfo', 'Device Models')
73class Test(unittest.TestCase):
74 def test_mapping_by_lvs_device_class_name(self):
75 self.assertEqual(['nmos', 'rppd'], sorted(device_models(NMOS, RPPD).mapping_by_lvs_device_class_name))
77 def test_device_classes_with_mappings_pass(self):
78 # NOTE: cap_cmim has no mapping, but no devices either
79 device_models(NMOS, RPPD).check_mappings(netlist())
81 def test_missing_mappings_are_reported(self):
82 with self.assertRaises(DeviceModelError) as ctx:
83 device_models(NMOS).check_mappings(netlist())
84 self.assertEqual("The device model mappings of the tech info are missing or invalid "
85 "for these LVS device classes:\n"
86 " - rppd (2 devices): no mapping",
87 str(ctx.exception))
89 def test_ignored_device_classes_need_no_mapping(self):
90 device_models(NMOS).check_mappings(netlist(), ignored_device_class_names={'rppd'})
92 def test_invalid_mappings_are_reported(self):
93 invalid_nmos = {'lvs_device_class_name': 'nmos',
94 'terminal_names': ['D', 'G', 'S', 'X'],
95 'parameters': [{'name': 'w', 'lvs_parameter_name': 'w'},
96 {'name': 'rfmode'}]}
97 with self.assertRaises(DeviceModelError) as ctx:
98 device_models(invalid_nmos, RPPD).check_mappings(netlist())
99 self.assertEqual("The device model mappings of the tech info are missing or invalid "
100 "for these LVS device classes:\n"
101 " - nmos: no SPICE prefix\n"
102 " - nmos: no LVS terminal 'X' (terminals are S, G, D, B)\n"
103 " - nmos: LVS terminal 'B' is missing in the terminal names\n"
104 " - nmos: parameter 'w': no LVS parameter 'w' (parameters are L, W, AS, AD, PS, PD)\n"
105 " - nmos: parameter 'rfmode': no value",
106 str(ctx.exception))
108 def test_invalid_rectangle_sides_are_reported(self):
109 rppd = {'lvs_device_class_name': 'rppd', 'spice_prefix': 'X', 'terminal_names': ['A', 'B'],
110 'parameters': [{'name': 'w', 'lvs_area_perimeter_side': {'area_parameter_name': 'A',
111 'perimeter_parameter_name': 'X',
112 'side': 'SIDE_LONG'}},
113 {'name': 'l', 'lvs_area_perimeter_side': {'area_parameter_name': 'A',
114 'perimeter_parameter_name': 'P'}}]}
115 with self.assertRaises(DeviceModelError) as ctx:
116 device_models(NMOS, rppd).check_mappings(netlist())
117 self.assertEqual("The device model mappings of the tech info are missing or invalid "
118 "for these LVS device classes:\n"
119 " - rppd: parameter 'w': no LVS parameter 'X' (parameters are R, L, W, A, P)\n"
120 " - rppd: parameter 'l': no side of the rectangle",
121 str(ctx.exception))
123 def test_rectangle_sides(self):
124 self.assertEqual((2.0, 1.0), rectangle_sides(area=2.0, perimeter=6.0))
125 self.assertEqual((3.0, 3.0), rectangle_sides(area=9.0, perimeter=12.0))
126 self.assertEqual((5.0, 0.0), rectangle_sides(area=0.0, perimeter=10.0))
128 def test_rectangle_sides_of_a_long_narrow_rectangle_are_exact(self):
129 long_side, short_side = rectangle_sides(area=1e4 * 1e-4, perimeter=2 * (1e4 + 1e-4))
130 self.assertAlmostEqual(1e4, long_side, delta=1e-12 * 1e4)
131 self.assertAlmostEqual(1e-4, short_side, delta=1e-12 * 1e-4)
133 def test_shape_without_rectangle_is_the_square_of_its_area(self):
134 # NOTE: a regular octagon of side 1 has a perimeter of 8, less than 4·√area
135 area = 2 * (1 + math.sqrt(2))
136 self.assertEqual((math.sqrt(area), math.sqrt(area)), rectangle_sides(area=area, perimeter=8.0))