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

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# 

24 

25from __future__ import annotations 

26 

27import os 

28import tempfile 

29from typing import * 

30import unittest 

31from unittest import mock 

32 

33import allure 

34import klayout.db as kdb 

35 

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 

44 

45 

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' 

53 

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")) 

59 

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')) 

64 

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) 

72 

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) 

80 

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) 

89 

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) 

94 

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()) 

101 

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)) 

107 

108 

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)) 

120 

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:]] 

129 

130 KLayoutExtractionContext.make_net_names_unique(netlist) 

131 

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()]) 

134 

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) 

148 

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) 

154 

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) 

162 

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)) 

173 

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) 

178 

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 ) 

189 

190 pins = pex_context.pins_of_layer((46, 0)) 

191 

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()))