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
« 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 os
27from typing import *
28import unittest
29from unittest import mock
31import allure
32import klayout.db as kdb
33import klayout.pex as klp
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
43REPO_DIR = os.path.realpath(os.path.join(__file__, '..', '..', '..'))
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'))
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'))
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)
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))
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")
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")
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")
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
114 rex_tech = r_extractor(tech).prepare_r_extractor_tech_pb(RExtractorTech())
116 self.assertEqual([0.11], [c.resistance for c in rex_tech.conductors if c.layer.lvs_layer_name == 'metal1_n_mom'])
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'
128 rex_tech = r_extractor(tech).prepare_r_extractor_tech_pb(RExtractorTech())
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])
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')
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))
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()
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])
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)
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}))
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}))
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}))
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}
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()}
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()})
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()})
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()})