Coverage for tests/rcx25/netlist_checks_test.py: 100%
137 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#
24from __future__ import annotations
26import allure
27from typing import *
28import unittest
30import klayout.db as kdb
32from klayout_pex.device_models import DeviceModels
33from klayout_pex.klayout.parasitic_device_classes import (
34 PARASITIC_CAPACITOR_CLASS_NAME,
35 PARASITIC_RESISTOR_CLASS_NAME,
36)
37from klayout_pex.rcx25.extraction_results import DeviceTerminalKey, ExtractionSummary, NetCoupleKey
38from klayout_pex.rcx25.netlist_checks import check_rc_netlist
39import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
42def lvs_netlist(mim_cap: bool = False) -> kdb.Netlist:
43 """
44 An nmos M1, with a pin on D, G and S
46 :param mim_cap: with a MIM cap C1 between G and S, before M1
47 """
48 netlist = kdb.Netlist()
49 circuit = kdb.Circuit()
50 circuit.name = 'chip'
51 netlist.add(circuit)
52 nmos = kdb.DeviceClassMOS4Transistor()
53 nmos.name = 'nmos'
54 netlist.add(nmos)
55 if mim_cap:
56 mim = kdb.DeviceClassCapacitor()
57 mim.name = 'mim'
58 netlist.add(mim)
59 c1 = circuit.create_device(mim, 'C1')
60 m1 = circuit.create_device(nmos, 'M1')
61 for cluster_id, terminal in enumerate(('D', 'G', 'S', 'B'), start=1):
62 net = circuit.create_net(terminal)
63 net.cluster_id = cluster_id # like extracted nets
64 m1.connect_terminal(terminal, net)
65 if terminal != 'B':
66 circuit.connect_pin(circuit.create_pin(terminal), net)
67 if mim_cap:
68 c1.connect_terminal('A', circuit.net_by_name('G'))
69 c1.connect_terminal('B', circuit.net_by_name('S'))
70 return netlist
73class RCNetlist:
74 """
75 The LVS netlist, with parasitic resistors and capacitors added, like the netlist expansion does
76 """
77 def __init__(self, lvs: kdb.Netlist):
78 self.netlist = lvs.dup()
79 self.circuit = self.netlist.circuit_by_name('chip')
80 self.res = kdb.DeviceClassResistor()
81 self.res.name = PARASITIC_RESISTOR_CLASS_NAME
82 self.netlist.add(self.res)
83 self.cap = kdb.DeviceClassCapacitor()
84 self.cap.name = PARASITIC_CAPACITOR_CLASS_NAME
85 self.netlist.add(self.cap)
87 def net(self, name: str) -> kdb.Net:
88 return self.circuit.net_by_name(name) or self.circuit.create_net(name)
90 def add(self, device_class: kdb.DeviceClass, net_a: str, net_b: str, parameter: str, value: float):
91 device = self.circuit.create_device(device_class, '')
92 device.connect_terminal('A', self.net(net_a))
93 device.connect_terminal('B', self.net(net_b))
94 device.set_parameter(parameter, value)
96 def add_resistor(self, net_a: str, net_b: str, resistance: float = 10.0):
97 self.add(self.res, net_a, net_b, 'R', resistance)
99 def add_capacitor(self, net_a: str, net_b: str, capacitance_femto: float):
100 self.add(self.cap, net_a, net_b, 'C', capacitance_femto / 1e15)
102 def connect(self, device_name: str, terminal: str, net_name: str):
103 self.circuit.device_by_name(device_name).connect_terminal(terminal, self.net(net_name))
106@allure.parent_suite("Unit Tests")
107@allure.tag("RCX25", "Netlist Checks")
108class Test(unittest.TestCase):
109 @staticmethod
110 def check(rc: RCNetlist,
111 capacitances: Dict[Tuple[str, str], float],
112 device_terminal_nodes: Optional[Dict[str, str]] = None,
113 substrate_net_name: Optional[str] = None,
114 blackbox_devices: bool = False) -> List[str]:
115 """
116 :param device_terminal_nodes: the node of the port of each terminal of M1 (e.g. D),
117 or of another device (e.g. C1.A)
118 """
119 def key(terminal: str) -> DeviceTerminalKey:
120 device_name, terminal = terminal.split('.') if '.' in terminal else ('M1', terminal)
121 # NOTE: the device IDs of the summary are the ones of the LVS netlist
122 device = rc.lvs.circuit_by_name('chip').device_by_name(device_name)
123 return DeviceTerminalKey(device.id(), device.device_class().terminal_id(terminal))
125 summary = ExtractionSummary(
126 capacitances={NetCoupleKey(*k): c for k, c in capacitances.items()},
127 resistances={},
128 device_terminal_nodes={key(terminal): node for terminal, node in (device_terminal_nodes or {}).items()}
129 )
130 # NOTE: the MIM cap is a metal capacitor, which white-box mode removes
131 device_models = device_models_pb2.DeviceModelsInfo()
132 device_models.device_model_mappings.add(lvs_device_class_name='mim',
133 kind=device_models_pb2.DeviceModelMapping.KIND_METAL_CAPACITOR)
134 return check_rc_netlist(lvs_netlist=rc.lvs, rc_netlist=rc.netlist, top_cell_name='chip', summary=summary,
135 blackbox_devices=blackbox_devices, device_models=DeviceModels(device_models),
136 substrate_net_name=substrate_net_name)
138 def rc_netlist(self, mim_cap: bool = False) -> RCNetlist:
139 lvs = lvs_netlist(mim_cap)
140 rc = RCNetlist(lvs)
141 rc.lvs = lvs
142 return rc
144 def test_consistent_netlist(self):
145 # the drain is on the end of the drain wire, the wire has a capacitance to the gate
146 rc = self.rc_netlist()
147 rc.add_resistor('D', 'D.$1.li1')
148 rc.add_resistor('D.$1.li1', 'D.P0.nsdm')
149 rc.connect('M1', 'D', 'D.P0.nsdm')
150 rc.add_capacitor('D.$1.li1', 'G', 1.0)
151 self.assertEqual([], self.check(rc, {('D.$1.li1', 'G'): 1.0}, {'D': 'D.P0.nsdm'}))
153 def test_devices_after_a_whiteboxed_one(self):
154 # the whiteboxed C1 is removed, the devices after it keep their IDs, the ones of the LVS netlist
155 # NOTE: a copy of the RC netlist numbers them anew, which would make M1 the C1 of the LVS netlist
156 rc = self.rc_netlist(mim_cap=True)
157 rc.circuit.remove_device(rc.circuit.device_by_name('C1'))
158 rc.add_resistor('D', 'D.P0.nsdm')
159 rc.add_resistor('G', 'G.P0.metal1')
160 rc.connect('M1', 'D', 'D.P0.nsdm')
161 self.assertEqual([], self.check(rc, {}, {'D': 'D.P0.nsdm', 'C1.A': 'G.P0.metal1'}))
163 def test_blackboxed_device_is_kept(self):
164 rc = self.rc_netlist(mim_cap=True)
165 self.assertEqual([], self.check(rc, {}, blackbox_devices=True))
167 def test_lost_device(self):
168 # black-box mode keeps the MIM cap, as its plates are not extracted
169 rc = self.rc_netlist(mim_cap=True)
170 rc.circuit.remove_device(rc.circuit.device_by_name('C1'))
171 self.assertEqual(['the RC netlist lacks 1 of the 1 devices of class mim: C1'],
172 self.check(rc, {}, blackbox_devices=True))
174 def test_whiteboxed_device_is_counted_twice(self):
175 # like the whiteboxed devices after the first one, which the netlist expansion didn't remove,
176 # but whose capacitances it extracted from their plates and fingers
177 rc = self.rc_netlist(mim_cap=True)
178 self.assertEqual(['the RC netlist keeps 1 of the 1 whiteboxed devices of class mim, '
179 'which counts their capacitances twice: C1'],
180 self.check(rc, {}))
182 def test_floating_resistor_network(self):
183 # like #211 §6, on a net without a pin: the drain on the net, its wire floating
184 rc = self.rc_netlist()
185 rc.add_resistor('D.$1.li1', 'D.P0.nsdm')
186 self.assertEqual(['resistors of the nodes D.$1.li1, D.P0.nsdm touch no net',
187 'terminal D of device M1 is not on the node D.P0.nsdm of its port'],
188 self.check(rc, {}, {'D': 'D.P0.nsdm'}))
190 def test_terminal_not_on_its_port(self):
191 # like #211 §6, on a net with a pin: the drain on the pin, not the end of its wire
192 rc = self.rc_netlist()
193 rc.add_resistor('D', 'D.P0.nsdm')
194 self.assertEqual(['terminal D of device M1 is not on the node D.P0.nsdm of its port'],
195 self.check(rc, {}, {'D': 'D.P0.nsdm'}))
197 def test_terminal_on_another_net(self):
198 rc = self.rc_netlist()
199 rc.add_resistor('D', 'D.$1.li1')
200 rc.add_resistor('S', 'S.$1.li1')
201 rc.connect('M1', 'D', 'S.$1.li1')
202 self.assertEqual(['terminal D of device M1 is not on the resistor network of its net D'],
203 self.check(rc, {}))
205 def test_resistors_joining_nets(self):
206 rc = self.rc_netlist()
207 rc.add_resistor('D', 'S')
208 problems = self.check(rc, {})
209 self.assertIn('resistors join the nets D and S', problems)
210 self.assertIn('terminal S of device M1 is not on the resistor network of its net S', problems)
212 def test_port_touching_nothing(self):
213 # like #211 §11: the pin of D replaced by a port on a node of nothing
214 rc = self.rc_netlist()
215 pin_d = rc.circuit.pin_by_name('D')
216 rc.circuit.disconnect_pin(pin_d.id())
217 rc.circuit.connect_pin(pin_d.id(), rc.net('X'))
218 self.assertEqual(['port D touches nothing', 'net D has no port'], self.check(rc, {}))
220 def test_lost_capacitance(self):
221 rc = self.rc_netlist()
222 rc.add_capacitor('D', 'G', 1.0)
223 self.assertEqual(['the capacitance between D and G is 1 fF, but 1.5 fF in the summary',
224 'the capacitance between D and S is 0 fF, but 0.5 fF in the summary'],
225 self.check(rc, {('D', 'G'): 1.5, ('D', 'S'): 0.5}))
227 def test_nets_of_the_same_name(self):
228 # like #211 §10: the islands of D, joined only in the parent
229 rc = self.rc_netlist()
230 rc.circuit.create_net('D')
231 self.assertEqual(['nets have the same name: D'], self.check(rc, {}))
233 def test_capacitances_to_the_substrate_are_on_its_net(self):
234 # the summary has them on VSUBS, the netlist on the substrate net B, but the one of B itself, shorted
235 rc = self.rc_netlist()
236 rc.add_capacitor('D', 'B', 1.0)
237 self.assertEqual([], self.check(rc, {('D', 'VSUBS'): 1.0, ('B', 'VSUBS'): 2.0}, substrate_net_name='B'))