Coverage for tests/rcx25/r_extractor_test.py: 100%

140 statements  

« 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 

25 

26import os 

27from typing import * 

28import unittest 

29from unittest import mock 

30 

31import allure 

32import klayout.db as kdb 

33import klayout.pex as klp 

34 

35from klayout_pex.klayout.lvsdb_extractor import KLayoutExtractionContext 

36from klayout_pex.rcx25.r.r_extractor import RExtractionTechError, RExtractor 

37from klayout_pex.tech_info import TechInfo 

38from klayout_pex_protobuf.kpex.klayout.r_extractor_tech_pb2 import RExtractorTech 

39import klayout_pex_protobuf.kpex.r.r_network_pb2 as r_network_pb2 

40import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

41 

42 

43REPO_DIR = os.path.realpath(os.path.join(__file__, '..', '..', '..')) 

44 

45 

46def ihp_sg13g2_tech() -> tech_pb2.Technology: 

47 return TechInfo.parse_tech_def(jsonpb_path=os.path.join(REPO_DIR, 'klayout_pex_protobuf', 'ihp-sg13g2_tech.pb.json')) 

48 

49 

50def sky130a_tech() -> tech_pb2.Technology: 

51 return TechInfo.parse_tech_def(jsonpb_path=os.path.join(REPO_DIR, 'klayout_pex_protobuf', 'sky130A_tech.pb.json')) 

52 

53 

54def r_extractor(tech: tech_pb2.Technology, 

55 top_cell: str = 'rfnmos_w1u_l0u72', 

56 lvsdb_file_name: str = 'rfnmos_w1u_l0u72.lvsdb.gz', 

57 skip_simplify: bool = False) -> RExtractor: 

58 lvsdb = kdb.LayoutVsSchematic() 

59 lvsdb.read(os.path.join(REPO_DIR, 'testdata', 'klayout', 'lvs', lvsdb_file_name)) 

60 pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=top_cell, 

61 lvsdb=lvsdb, 

62 tech=TechInfo(tech=tech, dielectric_filter=None), 

63 blackbox_devices=False) 

64 return RExtractor(pex_context=pex_context, 

65 substrate_algorithm=RExtractorTech.Algorithm.ALGORITHM_TESSELATION, 

66 wire_algorithm=RExtractorTech.Algorithm.ALGORITHM_SQUARE_COUNTING, 

67 delaunay_b=0.5, 

68 delaunay_amax=0.0, 

69 via_merge_distance=0.0, 

70 skip_simplify=skip_simplify) 

71 

72 

73@allure.parent_suite("Unit Tests") 

74@allure.tag("R", "Tech") 

75class RExtractorTechTest(unittest.TestCase): 

76 def assert_unmodeled_layers_error(self, tech: tech_pb2.Technology, *layers: str): 

77 with self.assertRaises(RExtractionTechError) as cm: 

78 r_extractor(tech).prepare_r_extractor_tech_pb(RExtractorTech()) 

79 self.assertEqual("The tech info can't model these layers of the layout for the resistance extraction, " 

80 "so their connections would be missing from the resistance network:\n" + 

81 '\n'.join(f" - {layer}" for layer in layers), 

82 str(cm.exception)) 

83 

84 def test_lvs_layer_without_layer_in_the_tech_info_is_an_error(self): 

85 # NOTE: like gf180mcuD, whose tech info named the contacts differently than its LVS deck 

86 tech = ihp_sg13g2_tech() 

87 for layer in tech.lvs_computed_layers: 

88 if layer.original_layer_name == 'Cont': 

89 layer.layer_info.name += '_renamed' 

90 self.assert_unmodeled_layers_error(tech, "cont_drw (Cont): the tech info has no layer derived from it") 

91 

92 def test_layer_without_model_is_an_error(self): 

93 tech = ihp_sg13g2_tech() 

94 del tech.process_parasitics.resistance.contacts[:] 

95 self.assert_unmodeled_layers_error(tech, 

96 "Cont (LVS cont_poly_con): no contact resistance for poly_con", 

97 "Cont (LVS cont_nsd_con): no contact resistance for nsd_fet") 

98 

99 def test_metal_without_sheet_resistance_is_an_error(self): 

100 # NOTE: it crashed with an AttributeError on the missing resistance 

101 tech = ihp_sg13g2_tech() 

102 layers = tech.process_parasitics.resistance.layers 

103 layers.remove(next(r for r in layers if r.layer_name == 'Metal1')) 

104 self.assert_unmodeled_layers_error(tech, "Metal1 (LVS metal1_n_mom): no sheet resistance") 

105 

106 def test_mim_cap_top_plate_is_a_conductor(self): 

107 # NOTE: the white-box extraction keeps the top plates of the MIM caps (e.g. sky130A capm, IHP cmim_top), 

108 # which stopped it with "unhandled layer purpose PURPOSE_MIM_CAP", here Metal1 stands in for one 

109 tech = ihp_sg13g2_tech() 

110 for layer in tech.lvs_computed_layers: 

111 if layer.layer_info.name == 'metal1_n_mom': 

112 layer.layer_info.purpose = tech_pb2.LayerInfo.PURPOSE_MIM_CAP 

113 

114 rex_tech = r_extractor(tech).prepare_r_extractor_tech_pb(RExtractorTech()) 

115 

116 self.assertEqual([0.11], [c.resistance for c in rex_tech.conductors if c.layer.lvs_layer_name == 'metal1_n_mom']) 

117 

118 def test_layer_modeled_by_another_layer_of_its_gds_pair_is_no_error(self): 

119 # e.g. sky130A mcon_vpp (the vias within MOM caps), on the GDS pair of mcon_con: 

120 # the resistance extraction models the layer of the GDS pair, with the shapes of both 

121 tech = ihp_sg13g2_tech() 

122 layers = {layer.layer_info.name: layer for layer in tech.lvs_computed_layers} 

123 layers['cont_poly_con'].layer_info.drw_gds_pair.CopyFrom(layers['cont_nsd_con'].layer_info.drw_gds_pair) 

124 for layer in tech.process_stack.layers: 

125 if layer.metal_layer.contact_above.name == 'cont_poly_con': 

126 layer.metal_layer.contact_above.name = 'renamed' 

127 

128 rex_tech = r_extractor(tech).prepare_r_extractor_tech_pb(RExtractorTech()) 

129 

130 self.assertIn('cont_nsd_con', [via.layer.lvs_layer_name for via in rex_tech.vias]) 

131 self.assertNotIn('cont_poly_con', [via.layer.lvs_layer_name for via in rex_tech.vias]) 

132 

133 

134@allure.parent_suite("Unit Tests") 

135@allure.tag("R", "Tech") 

136class RExtractorWellTest(unittest.TestCase): 

137 # NOTE: the IHP pwell stands in for a well, unless the tech info names the substrate (pwell_sub, on its GDS pair) 

138 @staticmethod 

139 def pwell_conductors(resistance: Optional[float], substrate: bool) -> List[Tuple[str, float]]: 

140 tech = ihp_sg13g2_tech() 

141 if not substrate: 

142 del tech.substrate.lvs_layer_names[:] 

143 if resistance is not None: 

144 tech.process_parasitics.resistance.layers.add(layer_name='PWell', resistance=resistance) 

145 rex_tech = r_extractor(tech).prepare_r_extractor_tech_pb(RExtractorTech()) 

146 return sorted((c.layer.lvs_layer_name, c.resistance) for c in rex_tech.conductors 

147 if c.layer.canonical_layer_name == 'PWell') 

148 

149 def test_well_is_a_conductor(self): 

150 # NOTE: it was left out without a word, so the device terminals on it were ideally on their nets 

151 self.assertEqual([('pwell', 3000.0), ('pwell_sub', 3000.0)], 

152 self.pwell_conductors(resistance=3000000, substrate=False)) 

153 

154 def test_substrate_is_no_conductor(self): 

155 # NOTE: the substrate is ideal, as it is 3D bulk rather than a sheet 

156 with mock.patch('klayout_pex.rcx25.r.r_extractor.warning') as warning_mock: 

157 self.assertEqual([], self.pwell_conductors(resistance=3000000, substrate=True)) 

158 warning_mock.assert_not_called() 

159 

160 def test_well_without_sheet_resistance_is_no_conductor(self): 

161 with mock.patch('klayout_pex.rcx25.r.r_extractor.warning') as warning_mock: 

162 self.assertEqual([], self.pwell_conductors(resistance=None, substrate=False)) 

163 self.assertEqual([ 

164 "The resistance network leaves out these wells, as the tech info has no sheet resistance for them, " 

165 "so the device terminals on them (e.g. the PMOS bulks) are ideally on their nets:\n" 

166 " - PWell (LVS pwell)\n" 

167 " - PWell (LVS pwell_sub)" 

168 ], [c.args[0] for c in warning_mock.call_args_list]) 

169 

170 

171def network_of_metal_and_well(joined_by_via: bool, with_metal: bool = True) -> klp.RNetwork: 

172 """ 

173 A wire (layer 0) with a pin, and a well (layer 1) with a device terminal (e.g. a PMOS bulk), 

174 joined by a contact (layer 2), or not 

175 """ 

176 tech = klp.RExtractorTech() 

177 tech.skip_simplify = True 

178 for layer in (0, 1): 

179 conductor = klp.RExtractorTechConductor() 

180 conductor.layer = layer 

181 conductor.algorithm = klp.Algorithm.SquareCounting 

182 conductor.resistance = 1.0 

183 tech.add_conductor(conductor) 

184 regions = {1: kdb.Region(kdb.Box(0, 0, 10000, 1000))} 

185 vertex_ports = {} 

186 if with_metal: 

187 regions[0] = kdb.Region(kdb.Box(9000, 0, 20000, 1000)) 

188 vertex_ports[0] = [kdb.Point(19500, 500)] 

189 if joined_by_via: 

190 via = klp.RExtractorTechVia() 

191 via.cut_layer = 2 

192 via.bottom_conductor = 1 

193 via.top_conductor = 0 

194 via.resistance = 1.0 

195 tech.add_via(via) 

196 regions[2] = kdb.Region(kdb.Box(9400, 400, 9600, 600)) 

197 polygon_ports = {1: [kdb.Polygon(kdb.Box(1000, 0, 1200, 1000))]} 

198 return klp.RNetExtractor(0.001).extract(tech, regions, vertex_ports, polygon_ports) 

199 

200 

201@allure.parent_suite("Unit Tests") 

202@allure.tag("R", "Wells") 

203class RExtractorIsolatedWellTest(unittest.TestCase): 

204 def test_well_joined_by_no_contact_is_isolated(self): 

205 # NOTE: e.g. a well joined to its net by a label, its piece of the network would join nothing 

206 network = network_of_metal_and_well(joined_by_via=False) 

207 self.assertEqual({n.object_id() for n in network.each_node() if n.layer() == 1}, 

208 RExtractor.isolated_well_node_ids(network, well_layer_ids={1})) 

209 

210 def test_well_joined_by_contact_is_not_isolated(self): 

211 network = network_of_metal_and_well(joined_by_via=True) 

212 self.assertEqual(set(), RExtractor.isolated_well_node_ids(network, well_layer_ids={1})) 

213 

214 def test_network_of_a_well_only_is_not_isolated(self): 

215 # NOTE: e.g. the well of a standard cell, without taps (sky130_fd_sc_hd__inv_1, VPB) 

216 network = network_of_metal_and_well(joined_by_via=False, with_metal=False) 

217 self.assertEqual(set(), RExtractor.isolated_well_node_ids(network, well_layer_ids={1})) 

218 

219 

220def nfet_li1_redux_networks() -> Dict[str, r_network_pb2.RNetwork]: 

221 rex = r_extractor(sky130a_tech(), 

222 top_cell='nfet_li1_redux', 

223 lvsdb_file_name='nfet_li1_redux_reordered_lvs_layers.lvsdb.gz', 

224 skip_simplify=True) # like kpex 

225 result = rex.extract(rex.prepare_request()) 

226 return {network.net_name: network for network in result.networks} 

227 

228 

229@allure.parent_suite("Unit Tests") 

230@allure.tag("R", "Device Terminals") 

231class RExtractorDeviceTerminalTest(unittest.TestCase): 

232 @staticmethod 

233 def ports_by_net() -> Dict[str, List[r_network_pb2.RNode]]: 

234 return {net_name: [n for n in network.nodes 

235 if n.node_kind == r_network_pb2.RNode.Kind.KIND_DEVICE_TERMINAL] 

236 for net_name, network in nfet_li1_redux_networks().items()} 

237 

238 def test_device_terminal_is_a_port_of_its_wire(self): 

239 # NOTE: the annotated layout has a layer for each LVS layer, e.g. poly_con and poly_vpp for poly, 

240 # and KLayout doesn't create the LVS layers in the same order from run to run: 

241 # in this LVS database, the gate terminal's layer (poly_con) comes after the other one 

242 self.assertEqual({'G': 1, '$2': 1, '$3': 1, 'sky130_gnd': 0}, 

243 {net_name: len(ports) for net_name, ports in self.ports_by_net().items()}) 

244 

245 def test_port_has_its_device_terminal(self): 

246 # NOTE: to connect the device to the node of its port (#211 §6) 

247 self.assertEqual({'G': [(1, 1, 'G')], '$2': [(1, 0, 'S')], '$3': [(1, 2, 'D')], 'sky130_gnd': []}, 

248 {net_name: [(p.device_terminal.device_id, p.device_terminal.terminal_id, p.device_terminal.name) 

249 for p in ports] 

250 for net_name, ports in self.ports_by_net().items()}) 

251 

252 

253@allure.parent_suite("Unit Tests") 

254@allure.tag("R", "Node Names") 

255class RExtractorNodeNameTest(unittest.TestCase): 

256 def test_node_names_have_the_layer_name(self): 

257 # NOTE: KLayout names a node after its layer index (e.g. $1.17), 

258 # which depends on the order of the LVS layers (see RExtractorDeviceTerminalTest) 

259 self.assertEqual({'G': ['$0.poly', '$1.li1', 'G', 'P0.poly'], 

260 '$2': ['$0.nsdm', '$1.li1', 'P0.nsdm'], 

261 '$3': ['$0.nsdm', '$1.li1', 'P0.nsdm'], 

262 'sky130_gnd': []}, 

263 {net_name: sorted(n.node_name for n in network.nodes) 

264 for net_name, network in nfet_li1_redux_networks().items()})