Coverage for tests/klayout/lvsdb_extractor_test.py: 100%
101 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#
25from __future__ import annotations
27import os
28import tempfile
29from typing import *
30import unittest
31from unittest import mock
33import allure
34import klayout.db as kdb
36from klayout_pex.klayout.lvsdb_extractor import (
37 KLayoutExtractedLayerInfo,
38 KLayoutExtractionContext,
39 KLayoutMergedExtractedLayerInfo,
40 LVSDBError
41)
42from klayout_pex.tech_info import TechInfo
43import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
46@allure.parent_suite("Unit Tests")
47@allure.tag("LVS", "LVSDB")
48class Test(unittest.TestCase):
49 # NOTE: the IHP LVS script replaced the extracted rfnmos by a newly created
50 # sg13_lv_nmos (RF MOS model mapping), which has no device abstract,
51 # and left the abstract of the extracted one as a second top cell
52 cell_name = 'rfnmos_w1u_l0u72'
54 @property
55 def lvsdb_path(self) -> str:
56 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
57 'testdata', 'klayout', 'lvs',
58 f"{self.cell_name}_broken_rfmos_model_mapping.lvsdb.gz"))
60 @property
61 def tech_info_json_path(self) -> str:
62 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
63 'klayout_pex_protobuf', 'ihp-sg13g2_tech.pb.json'))
65 def tech_info(self, renamed_layers_derived_from: Optional[str] = None) -> TechInfo:
66 tech = TechInfo.parse_tech_def(jsonpb_path=self.tech_info_json_path)
67 # NOTE: e.g. like gf180mcuD, whose tech info names the contacts differently than its LVS deck
68 for layer in tech.lvs_computed_layers:
69 if layer.original_layer_name == renamed_layers_derived_from:
70 layer.layer_info.name += '_renamed'
71 return TechInfo(tech=tech, dielectric_filter=None)
73 def prepare_extraction(self, tech: Optional[TechInfo] = None) -> KLayoutExtractionContext:
74 lvsdb = kdb.LayoutVsSchematic()
75 lvsdb.read(self.lvsdb_path)
76 return KLayoutExtractionContext.prepare_extraction(top_cell=self.cell_name,
77 lvsdb=lvsdb,
78 tech=tech or self.tech_info(),
79 blackbox_devices=False)
81 def test_unnamed_layer_partly_covered_by_derived_layers_is_a_warning(self):
82 # the contacts to the taps (e.g. the guard ring) have no layer in the tech info
83 with mock.patch('klayout_pex.klayout.lvsdb_extractor.warning') as warning:
84 pex_context = self.prepare_extraction()
85 self.assertIn("The extraction leaves out these LVS layers, as the tech info doesn't know them:\n"
86 " - cont_drw (Cont): 36 of 46 shapes are not within cont_nsd_con, cont_psd_con, cont_poly_con",
87 [call.args[0] for call in warning.call_args_list])
88 self.assertEqual([], pex_context.unmodeled_layers)
90 def test_unnamed_layer_without_derived_layers_is_unmodeled(self):
91 pex_context = self.prepare_extraction(tech=self.tech_info(renamed_layers_derived_from='Cont'))
92 self.assertEqual(['cont_drw (Cont): the tech info has no layer derived from it'],
93 pex_context.unmodeled_layers)
95 def test_leftover_top_cells_are_ignored_with_a_warning(self):
96 with mock.patch('klayout_pex.klayout.lvsdb_extractor.warning') as warning:
97 pex_context = self.prepare_extraction()
98 messages = [call.args[0] for call in warning.call_args_list]
99 self.assertTrue(any('D$rfnmos' in m for m in messages), messages)
100 self.assertFalse(pex_context.top_cell_bbox().empty())
102 def test_a_device_without_layout_geometry_is_an_error(self):
103 pex_context = self.prepare_extraction()
104 with self.assertRaises(LVSDBError) as cm:
105 pex_context.devices_by_name
106 self.assertIn('$1 (sg13_lv_nmos)', str(cm.exception))
109@allure.parent_suite("Unit Tests")
110@allure.tag("LVS", "LVSDB", "Net Names")
111class NetNameTest(unittest.TestCase):
112 def test_nets_get_the_unique_names_of_the_spice_writer(self):
113 # NOTE: nets can share a name, e.g. the metal islands of a supply, joined only in the parent (#211 §10)
114 netlist = kdb.Netlist()
115 circuit = kdb.Circuit()
116 circuit.name = 'TOP'
117 netlist.add(circuit)
118 for name in ('D', 'S', 'D', 'D$1', 'D', ''):
119 circuit.connect_pin(circuit.create_pin(''), circuit.create_net(name))
121 with tempfile.TemporaryDirectory() as tmp_dir:
122 spice_path = os.path.join(tmp_dir, 'netlist.cir')
123 writer = kdb.NetlistSpiceWriter()
124 writer.use_net_names = True
125 netlist.write(spice_path, writer)
126 with open(spice_path) as f:
127 subckt_line = next(line for line in f if line.startswith('.SUBCKT'))
128 spice_names = [name.replace('\\', '') for name in subckt_line.split()[2:]]
130 KLayoutExtractionContext.make_net_names_unique(netlist)
132 self.assertEqual(['D', 'S', 'D$1', 'D$1$1', 'D$2', '$0'], [n.expanded_name() for n in circuit.each_net()])
133 self.assertEqual(spice_names, [n.expanded_name() for n in circuit.each_net()])
135 def test_nets_without_label_keep_their_shapes(self):
136 # NOTE: KLayout 0.30.4 (CI) reads the nets without a label from the LVSDB without a name
137 # (newer versions with the expanded one, e.g. $2), but the extraction finds the shapes of a net
138 # by the name on them, so the nets $2 and $3 of the nfet had no shapes and no resistor network
139 testdata_dir = os.path.realpath(os.path.join(__file__, '..', '..', '..', 'testdata', 'klayout', 'lvs'))
140 lvsdb = kdb.LayoutVsSchematic()
141 lvsdb.read(os.path.join(testdata_dir, 'nfet_li1_redux_reordered_lvs_layers.lvsdb.gz'))
142 for net in lvsdb.netlist().circuit_by_name('nfet_li1_redux').each_net():
143 if net.name.startswith('$'):
144 net.name = ''
145 tech = TechInfo(tech=TechInfo.parse_tech_def(jsonpb_path=os.path.realpath(os.path.join(
146 __file__, '..', '..', '..', 'klayout_pex_protobuf', 'sky130A_tech.pb.json'))),
147 dielectric_filter=None)
149 with mock.patch('klayout_pex.klayout.lvsdb_extractor.warning'):
150 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell='nfet_li1_redux',
151 lvsdb=lvsdb,
152 tech=tech,
153 blackbox_devices=False)
155 circuit = pex_context.lvsdb.netlist().circuit_by_name('nfet_li1_redux')
156 self.assertEqual(['G', '$2', '$3', 'sky130_gnd'], [n.name for n in circuit.each_net()])
157 for net in circuit.each_net():
158 if net.name in ('$2', '$3'):
159 shape_count = sum(pex_context.shapes_of_net(gds_pair, net).count()
160 for gds_pair in pex_context.extracted_layers)
161 self.assertGreater(shape_count, 0, net.name)
163@allure.parent_suite("Unit Tests")
164@allure.tag("LVS", "LVSDB", "Pins")
165class PinsTest(unittest.TestCase):
166 def test_the_pins_of_a_pin_layer_with_several_lvs_layers(self):
167 # NOTE: e.g. gf180mcuD, which has no pin layers (the pins are labels on the drawn layers),
168 # and splits Metal4 into the bottom plates of the MIM caps and the rest
169 tech = tech_pb2.Technology(name='test')
170 tech.layers.add(name='Metal4',
171 drw_gds_pair=tech_pb2.GDSPair(layer=46, datatype=0),
172 pin_gds_pair=tech_pb2.GDSPair(layer=46, datatype=0))
174 def extracted_layer(lvs_layer_name: str, box: kdb.Box, net_name: str) -> KLayoutExtractedLayerInfo:
175 region = kdb.Region()
176 region.insert(kdb.PolygonWithProperties(kdb.Polygon(box), {'net': net_name}))
177 return KLayoutExtractedLayerInfo(index=0, lvs_layer_name=lvs_layer_name, gds_pair=(46, 0), region=region)
179 source_layers = [extracted_layer('metal4_n_cap', kdb.Box(0, 0, 100, 100), 'BOT'),
180 extracted_layer('metal4_cap', kdb.Box(100, 0, 300, 100), 'BOT'),
181 extracted_layer('metal4_n_cap', kdb.Box(400, 0, 500, 100), 'OTHER')]
182 pex_context = KLayoutExtractionContext(
183 lvsdb=None, tech=TechInfo(tech=tech, dielectric_filter=None), dbu=0.001,
184 layer_index_map={}, lvsdb_regions={}, cell_mapping=None,
185 annotated_top_cell=None, annotated_layout=None,
186 extracted_layers={(46, 0): KLayoutMergedExtractedLayerInfo(source_layers=source_layers, gds_pair=(46, 0))},
187 unnamed_layers=[], unmodeled_layers=[], blackbox_devices=False
188 )
190 pins = pex_context.pins_of_layer((46, 0))
192 self.assertEqual([('BOT', kdb.Box(0, 0, 100, 100)), ('BOT', kdb.Box(100, 0, 300, 100)),
193 ('OTHER', kdb.Box(400, 0, 500, 100))],
194 sorted((p.property('net'), p.bbox()) for p in pins.each()))