Coverage for tests/klayout/netlist_printer_test.py: 100%
96 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
25from datetime import datetime
26import os
27import tempfile
28import unittest
30import google.protobuf.json_format
31import klayout.db as kdb
33from klayout_pex.device_models import DeviceModelError, DeviceModels
34from klayout_pex.klayout.netlist_printer import NetlistHeader, NetlistPrinter
35from klayout_pex.klayout.parasitic_device_classes import (
36 PARASITIC_CAPACITOR_CLASS_NAME,
37 PARASITIC_RESISTOR_CLASS_NAME,
38)
39import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
42HEADER = NetlistHeader(kpex_version='1.2.3',
43 extraction_engine='KPEX/2.5D',
44 tech='ihp_sg13g2',
45 date=datetime(2026, 9, 30, 18, 15, 1))
48def device_model_mapping(**kwargs) -> device_models_pb2.DeviceModelMapping:
49 return google.protobuf.json_format.ParseDict(kwargs, device_models_pb2.DeviceModelMapping())
52# as the IHP RF MOS: a device class only LVS knows, written as the base MOS subcircuit
53RFNMOS = device_model_mapping(lvs_device_class_name='rfnmos',
54 spice_prefix='X',
55 model_name='sg13_lv_nmos',
56 terminal_names=['D', 'G', 'S', 'B'],
57 parameters=[{'name': 'w', 'lvs_parameter_name': 'W', 'lvs_parameter_factor': 1e-6},
58 {'name': 'l', 'lvs_parameter_name': 'L', 'lvs_parameter_factor': 1e-6},
59 {'name': 'rfmode', 'constant': 1}])
61# as the IHP antenna diode: a model of w and l, which LVS extracts by area and perimeter
62DANTENNA = device_model_mapping(lvs_device_class_name='dantenna',
63 spice_prefix='X',
64 terminal_names=['A', 'C'],
65 parameters=[{'name': 'w', 'lvs_parameter_factor': 1e-6,
66 'lvs_area_perimeter_side': {'area_parameter_name': 'A',
67 'perimeter_parameter_name': 'P',
68 'side': 'SIDE_LONG'}},
69 {'name': 'l', 'lvs_parameter_factor': 1e-6,
70 'lvs_area_perimeter_side': {'area_parameter_name': 'A',
71 'perimeter_parameter_name': 'P',
72 'side': 'SIDE_SHORT'}}])
75def empty_netlist() -> kdb.Netlist:
76 netlist = kdb.Netlist()
77 circuit = kdb.Circuit()
78 circuit.name = 'TOP'
79 netlist.add(circuit)
80 return netlist
83def netlist_with_parasitics() -> kdb.Netlist:
84 """
85 A netlist with the parasitics KPEX adds
86 """
87 netlist = empty_netlist()
88 circuit = netlist.circuit_by_name('TOP')
89 a = circuit.create_net('a')
90 b = circuit.create_net('b')
92 cap = kdb.DeviceClassCapacitor()
93 cap.name = PARASITIC_CAPACITOR_CLASS_NAME
94 netlist.add(cap)
95 c = circuit.create_device(cap, 'ext_1')
96 c.connect_terminal('A', a)
97 c.connect_terminal('B', b)
98 c.set_parameter('C', 1e-15)
100 res = kdb.DeviceClassResistor()
101 res.name = PARASITIC_RESISTOR_CLASS_NAME
102 netlist.add(res)
103 r = circuit.create_device(res, 'ext_1')
104 r.connect_terminal('A', a)
105 r.connect_terminal('B', b)
106 r.set_parameter('R', 12.5)
108 return netlist
111def lvs_netlist() -> kdb.Netlist:
112 """
113 An LVS netlist with a single rfnmos, expanded by parasitics, as KPEX does
114 """
115 netlist = netlist_with_parasitics()
116 circuit = netlist.circuit_by_name('TOP')
118 mos = kdb.DeviceClassMOS4Transistor()
119 mos.name = 'rfnmos'
120 netlist.add(mos)
122 # NOTE: a device class without devices
123 unused = kdb.DeviceClassResistor()
124 unused.name = 'rppd'
125 netlist.add(unused)
127 d = circuit.create_device(mos, 'M1')
128 for t, net_name in (('S', 'source'), ('G', 'gate'), ('D', 'drain'), ('B', 'bulk')):
129 d.connect_terminal(t, circuit.create_net(net_name))
130 d.set_parameter('W', 1.0)
131 d.set_parameter('L', 0.72)
133 return netlist
136@allure.parent_suite("Unit Tests")
137@allure.tag("Netlist", "SPICE")
138class Test(unittest.TestCase):
139 @staticmethod
140 def printer(*device_model_mappings: device_models_pb2.DeviceModelMapping) -> NetlistPrinter:
141 device_models = device_models_pb2.DeviceModelsInfo(device_model_mappings=device_model_mappings)
142 return NetlistPrinter(header=HEADER,
143 device_models=DeviceModels(device_models))
145 @staticmethod
146 def lines(printer: NetlistPrinter, netlist: kdb.Netlist) -> list[str]:
147 with tempfile.TemporaryDirectory() as tmp_dir:
148 path = os.path.join(tmp_dir, 'netlist.cir')
149 printer.write(netlist, path)
150 with open(path) as f:
151 return [l.strip() for l in f if l.strip()]
153 @classmethod
154 def device_lines(cls, printer: NetlistPrinter, netlist: kdb.Netlist) -> list[str]:
155 return [l for l in cls.lines(printer, netlist) if l[0] not in '*.']
157 def test_header_is_written_as_comments(self):
158 self.assertEqual([
159 '*********************************************************',
160 '*** NGSPICE file created by KLayout-PEX 1.2.3',
161 '*** -----------------------------------------------------',
162 '*** Extraction Engine: KPEX/2.5D',
163 '*** Technology: ihp_sg13g2',
164 '*** Date: 2026-09-30 18:15:01',
165 '*********************************************************',
166 ], self.lines(self.printer(), empty_netlist())[:7])
168 def test_parasitics_are_written_without_a_model(self):
169 self.assertEqual([
170 'Cext_1 a b 1f',
171 'Rext_1 a b 12.5',
172 ], self.device_lines(self.printer(), netlist_with_parasitics()))
174 def test_lvs_device_is_written_with_its_device_model_mapping(self):
175 self.assertEqual([
176 'Cext_1 a b 1f',
177 'Rext_1 a b 12.5',
178 'XM1 drain gate source bulk sg13_lv_nmos w=1e-06 l=7.2e-07 rfmode=1',
179 ], self.device_lines(self.printer(RFNMOS), lvs_netlist()))
181 def test_lvs_device_without_device_model_mapping_is_an_error(self):
182 # NOTE: the parasitics KPEX added need no device model mapping
183 with self.assertRaises(DeviceModelError) as ctx:
184 self.device_lines(self.printer(), lvs_netlist())
185 self.assertEqual("The device model mappings of the tech info are missing or invalid "
186 "for these LVS device classes:\n"
187 " - rfnmos (1 devices): no mapping",
188 str(ctx.exception))
190 def test_rectangle_sides_are_written_for_area_and_perimeter(self):
191 netlist = empty_netlist()
192 circuit = netlist.circuit_by_name('TOP')
193 diode = kdb.DeviceClassDiode()
194 diode.name = 'dantenna'
195 netlist.add(diode)
196 d = circuit.create_device(diode, 'D1')
197 d.connect_terminal('A', circuit.create_net('sub'))
198 d.connect_terminal('C', circuit.create_net('pin'))
199 d.set_parameter('A', 0.48 * 0.78) # µm²
200 d.set_parameter('P', 2 * (0.48 + 0.78)) # µm
201 self.assertEqual([
202 'XD1 sub pin dantenna w=7.8e-07 l=4.8e-07',
203 ], self.device_lines(self.printer(DANTENNA), netlist))