Coverage for tests/klayout/netlist_expander_test.py: 100%
88 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-2025 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
27import os
28import tempfile
29import unittest
31import klayout.db as kdb
33from klayout_pex.device_models import DeviceModels
34from klayout_pex.fastercap.output_interpreter import FasterCapOutputInterpreter
35from klayout_pex.klayout.lvsdb_extractor import KLayoutExtractionContext
36from klayout_pex.klayout.netlist_expander import NetlistExpander
37from klayout_pex.log import (
38 debug,
39)
40from klayout_pex.common.capacitance_matrix import CapacitanceMatrix
41from klayout_pex.tech_info import TechInfo
42import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
45@allure.parent_suite("Unit Tests")
46@allure.tag("Netlist", "Netlist Expansion")
47class Test(unittest.TestCase):
48 @property
49 def klayout_testdata_dir(self) -> str:
50 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
51 'testdata', 'fastercap'))
53 @property
54 def tech_info_json_path(self) -> str:
55 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
56 'klayout_pex_protobuf', 'sky130A_tech.pb.json'))
58 def test_netlist_expansion(self):
59 exp = NetlistExpander()
61 cell_name = 'nmos_diode2'
63 lvsdb = kdb.LayoutVsSchematic()
64 lvsdb_path = os.path.join(self.klayout_testdata_dir, f"{cell_name}.lvsdb.gz")
65 lvsdb.read(lvsdb_path)
67 csv_path = os.path.join(self.klayout_testdata_dir, f"{cell_name}_FasterCap_Result_Matrix.csv")
69 cap_matrix = CapacitanceMatrix.parse_csv(csv_path, separator=';')
71 tech = TechInfo.from_json(self.tech_info_json_path,
72 dielectric_filter=None)
74 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=cell_name,
75 lvsdb=lvsdb,
76 tech=tech,
77 blackbox_devices=False)
78 expanded_netlist = exp.expand(extracted_netlist=pex_context.lvsdb.netlist(),
79 top_cell_name=pex_context.annotated_top_cell.name,
80 cap_matrix=cap_matrix,
81 cap_matrix_interpreter=FasterCapOutputInterpreter(),
82 blackbox_devices=False,
83 device_models=tech.device_models,
84 substrate_net_name='VSUBS') # NOTE: the substrate conductor of the matrix
85 out_path = tempfile.mktemp(prefix=f"{cell_name}_expanded_netlist_", suffix=".cir")
86 spice_writer = kdb.NetlistSpiceWriter()
87 expanded_netlist.write(out_path, spice_writer)
88 debug(f"Wrote expanded netlist to: {out_path}")
90 allure.attach.file(csv_path, attachment_type=allure.attachment_type.CSV)
91 allure.attach.file(out_path, attachment_type=allure.attachment_type.TEXT)
94def device_models(*metal_capacitor_class_names: str) -> DeviceModels:
95 info = device_models_pb2.DeviceModelsInfo()
96 for name in metal_capacitor_class_names:
97 info.device_model_mappings.add(lvs_device_class_name=name,
98 kind=device_models_pb2.DeviceModelMapping.KIND_METAL_CAPACITOR)
99 return DeviceModels(info)
102def substrate_test_netlist() -> kdb.Netlist:
103 """
104 The nets D and B, with a pin each
105 """
106 netlist = kdb.Netlist()
107 circuit = kdb.Circuit()
108 circuit.name = 'chip'
109 netlist.add(circuit)
110 for name in ('D', 'B'):
111 circuit.connect_pin(circuit.create_pin(name), circuit.create_net(name))
112 return netlist
115# the substrate on the net B, as FasterCapInputBuilder names it after the substrate net:
116# B to D 1.5 fF, and the rest of the diagonal (to infinity) of D 0.25 fF
117CAP_MATRIX_SUBSTRATE_ON_B = CapacitanceMatrix(conductor_names=['g1_B', 'g2_D'],
118 rows=[[2.0e-15, -1.5e-15],
119 [-1.5e-15, 1.75e-15]])
121# the substrate a conductor of its own: to D 1 fF and to B 2 fF, D to B 0.5 fF,
122# and the rest of the diagonal (to infinity): D 0.25 fF, B 0.1 fF
123CAP_MATRIX_SUBSTRATE_PORT = CapacitanceMatrix(conductor_names=['g1_sky130_gnd', 'g2_D', 'g3_B'],
124 rows=[[3.0e-15, -1.0e-15, -2.0e-15],
125 [-1.0e-15, 1.75e-15, -0.5e-15],
126 [-2.0e-15, -0.5e-15, 2.6e-15]])
129def capacitances_by_net_pair(expanded: kdb.Netlist) -> dict:
130 caps = {}
131 for d in expanded.circuit_by_name('chip').each_device():
132 pair = frozenset((d.net_for_terminal('A').name, d.net_for_terminal('B').name))
133 caps[pair] = round(caps.get(pair, 0.0) + d.parameter('C') * 1e15, 6)
134 return caps
137@allure.parent_suite("Unit Tests")
138@allure.tag("Netlist", "Netlist Expansion")
139class SubstrateTest(unittest.TestCase):
140 def test_capacitances_to_the_substrate_are_on_its_net(self):
141 expanded = NetlistExpander.expand(substrate_test_netlist(), 'chip', CAP_MATRIX_SUBSTRATE_ON_B,
142 FasterCapOutputInterpreter(), blackbox_devices=True,
143 device_models=device_models(), substrate_net_name='B')
144 circuit = expanded.circuit_by_name('chip')
145 # NOTE: the rest of the diagonal of D goes to the substrate B too
146 self.assertEqual({frozenset(('D', 'B')): 1.75}, capacitances_by_net_pair(expanded))
147 self.assertEqual(['D', 'B'], [p.name() for p in circuit.each_pin()])
148 for name in ('VSUBS', 'FC_GND', 'GND'):
149 self.assertIsNone(circuit.net_by_name(name))
151 def test_substrate_without_net_is_a_port(self):
152 expanded = NetlistExpander.expand(substrate_test_netlist(), 'chip', CAP_MATRIX_SUBSTRATE_PORT,
153 FasterCapOutputInterpreter(), blackbox_devices=True,
154 device_models=device_models(), substrate_net_name='sky130_gnd')
155 circuit = expanded.circuit_by_name('chip')
156 self.assertEqual({frozenset(('sky130_gnd', 'D')): 1.25,
157 frozenset(('sky130_gnd', 'B')): 2.1,
158 frozenset(('D', 'B')): 0.5},
159 capacitances_by_net_pair(expanded))
160 self.assertEqual(['D', 'B', 'sky130_gnd'], [p.name() for p in circuit.each_pin()])
163@allure.parent_suite("Unit Tests")
164@allure.tag("Netlist", "Netlist Expansion")
165class WhiteboxTest(unittest.TestCase):
166 def test_whitebox_removes_the_metal_capacitors_only(self):
167 # White-box mode removed all devices, e.g. the transistors too
168 netlist = substrate_test_netlist()
169 circuit = netlist.circuit_by_name('chip')
170 for device_class, class_name, device_name in ((kdb.DeviceClassCapacitor(), 'mim', 'C1'),
171 (kdb.DeviceClassMOS4Transistor(), 'nmos', 'M1')):
172 device_class.name = class_name
173 netlist.add(device_class)
174 circuit.create_device(device_class, device_name)
176 expanded = NetlistExpander.expand(netlist, 'chip', CAP_MATRIX_SUBSTRATE_ON_B,
177 FasterCapOutputInterpreter(), blackbox_devices=False,
178 device_models=device_models('mim'), substrate_net_name='B')
179 self.assertEqual(['M1'], [d.name for d in expanded.circuit_by_name('chip').each_device()
180 if d.device_class().name in ('mim', 'nmos')])