Coverage for tests/klayout/pex25d_builder_test.py: 100%

129 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-17 19:08 +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# 

24 

25from types import SimpleNamespace 

26import unittest 

27 

28import allure 

29import klayout.db as kdb 

30 

31from klayout_pex.klayout.pex25d_builder import PEX25DBuilder, BuildError 

32from klayout_pex.pex25d.protobuf import pex25d_terminal_pb2 

33from klayout_pex.pex25d.reader import read_pex25d_text 

34from klayout_pex.pex25d.resolver import resolve 

35from klayout_pex.pex25d.validator import validate 

36from klayout_pex.pex25d.writer import write_pex25d_text 

37from klayout_pex.tech_info import TechInfo 

38import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

39import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as stack_pb2 

40 

41 

42class TerminalFixture: 

43 def __init__(self): 

44 self.netlist = kdb.Netlist() 

45 self.circuit = kdb.Circuit() 

46 self.circuit.name = 'test' 

47 self.netlist.add(self.circuit) 

48 self.net = self.circuit.create_net('N') 

49 self.device_class = kdb.DeviceClassMOS4Transistor() 

50 self.device_class.name = 'nfet' 

51 self.netlist.add(self.device_class) 

52 self.geometry = {(1, 0): kdb.Region(kdb.Box(0, 0, 100, 20))} 

53 self.markers = {} 

54 self.labels = kdb.Texts() 

55 self.pins = kdb.Region(kdb.Box(0, 0, 100, 20)) 

56 tech = tech_pb2.Technology(name='test') 

57 metal = tech.layers.add(name='met1') 

58 metal.drw_gds_pair.layer = 1 

59 metal.pin_gds_pair.layer = 1 

60 metal.pin_gds_pair.datatype = 16 

61 metal.label_gds_pair.layer = 1 

62 metal.label_gds_pair.datatype = 5 

63 stack = tech.process_stack 

64 substrate = stack.layers.add(name='subs', layer_type=stack_pb2.ProcessStackInfo.LAYER_TYPE_SUBSTRATE) 

65 substrate.substrate_layer.height = 0.1 

66 substrate.substrate_layer.thickness = 0.2 

67 metal = stack.layers.add(name='met1', layer_type=stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL) 

68 metal.metal_layer.z = 1.0 

69 metal.metal_layer.thickness = 0.1 

70 air = stack.layers.add(name='air', layer_type=stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC) 

71 air.simple_dielectric_layer.dielectric_k = 1.0 

72 self.tech = TechInfo(tech, None) 

73 self.context = SimpleNamespace( 

74 dbu=0.001, top_circuit=self.circuit, 

75 shapes_of_net=lambda gds_pair, net: self.geometry.get(gds_pair), 

76 pins_of_layer=lambda pair: self.pins, 

77 labels_of_layer=lambda pair: self.labels, 

78 lvsdb=SimpleNamespace( 

79 layer_name=lambda index: 'met1', 

80 shapes_of_terminal=lambda ref: self.markers[ref.device().expanded_name()])) 

81 

82 def pin(self, label, x, y): 

83 self.labels.insert(kdb.Text(label, kdb.Trans(x, y))) 

84 

85 def device(self, name, region): 

86 device = self.circuit.create_device(self.device_class, name) 

87 device.connect_terminal('G', self.net) 

88 self.markers[name] = {1: region} 

89 

90 def build(self): 

91 return PEX25DBuilder(self.context, self.tech, 'test').build() 

92 

93 

94@allure.parent_suite('Unit Tests') 

95@allure.tag('PEX25D', 'Builder') 

96class Pex25DBuilderTerminalTest(unittest.TestCase): 

97 def test_pin_uses_its_label_not_the_whole_pin_polygon(self): 

98 fixture = TerminalFixture() 

99 fixture.pin('A', 10, 10) 

100 fixture.pin('B', 90, 10) 

101 file = fixture.build() 

102 assert [t.name for t in file.terminals] == ['pin:A', 'pin:B'] 

103 assert all(t.kind == pex25d_terminal_pb2().TERMINAL_KIND_PIN for t in file.terminals) 

104 terminal = file.terminals[0] 

105 assert terminal.conductor == 'N' 

106 assert (terminal.region.lower_left.x, terminal.region.upper_right.x) == (90, 110) 

107 assert file.shapes[0].box.upper_right.x == 1000 

108 assert validate(file, strict=True).exit_code == 0 

109 

110 def test_pin_on_a_bent_wire_resolves(self): 

111 fixture = TerminalFixture() 

112 fixture.geometry[(1, 0)] = kdb.Region(kdb.Polygon([ 

113 kdb.Point(0, 0), kdb.Point(100, 0), kdb.Point(100, 20), 

114 kdb.Point(20, 20), kdb.Point(20, 100), kdb.Point(0, 100)])) 

115 fixture.pin('A', 10, 10) 

116 file = fixture.build() 

117 scene = resolve(read_pex25d_text(write_pex25d_text(file), '<generated>')) 

118 terminal = scene.conductors[0].terminals[0] 

119 assert len(terminal.boxes) == 1 

120 assert terminal.boxes[0].upper_right.x - terminal.boxes[0].lower_left.x == 20 

121 

122 def test_repeated_labels_produce_unique_stable_names(self): 

123 fixture = TerminalFixture() 

124 fixture.pin('A', 10, 10) 

125 fixture.pin('A', 90, 10) 

126 names = [t.name for t in fixture.build().terminals] 

127 assert names == [t.name for t in fixture.build().terminals] 

128 assert len(names) == len(set(names)) == 2 

129 

130 def test_a_label_outside_the_pin_marker_is_ignored(self): 

131 fixture = TerminalFixture() 

132 fixture.pin('A', 200, 10) 

133 assert not fixture.build().terminals 

134 

135 def test_a_pin_without_interconnect_reports_a_build_error(self): 

136 fixture = TerminalFixture() 

137 fixture.pin('A', 10, 10) 

138 fixture.geometry[(1, 0)] = kdb.Region(kdb.Box(50, 0, 100, 20)) 

139 with self.assertRaisesRegex(BuildError, 'must select one conductor'): 

140 fixture.build() 

141 

142 def test_a_device_terminal_keeps_all_its_disjoint_shapes_in_one_node(self): 

143 fixture = TerminalFixture() 

144 fixture.geometry[(1, 0)] = kdb.Region(kdb.Box(10, 0, 20, 100)) 

145 fixture.geometry[(1, 0)].insert(kdb.Box(40, 0, 50, 100)) 

146 marker = kdb.Region(kdb.Box(10, 40, 20, 50)) 

147 marker.insert(kdb.Box(40, 40, 50, 50)) 

148 fixture.device('M1', marker) 

149 file = fixture.build() 

150 assert len(file.terminals) == 1 

151 terminal = file.terminals[0] 

152 assert terminal.name == 'device:M1:G' 

153 assert terminal.kind == pex25d_terminal_pb2().TERMINAL_KIND_DEVICE_TERMINAL 

154 scene = resolve(file) 

155 assert len(scene.conductors[0].terminals) == 1 

156 assert len(scene.conductors[0].terminals[0].boxes) == 2 

157 

158 def test_an_abutting_device_gets_only_a_boundary_strip(self): 

159 fixture = TerminalFixture() 

160 fixture.device('M1', kdb.Region(kdb.Box(-20, 0, 0, 20))) 

161 file = fixture.build() 

162 terminal = resolve(file).conductors[0].terminals[0] 

163 assert (terminal.boxes[0].lower_left.x, terminal.boxes[0].upper_right.x) == (0, 10) 

164 

165 def test_a_box_cannot_silently_short_interconnect_between_device_shapes(self): 

166 fixture = TerminalFixture() 

167 marker = kdb.Region(kdb.Box(10, 0, 20, 20)) 

168 marker.insert(kdb.Box(40, 0, 50, 20)) 

169 fixture.device('M1', marker) 

170 with self.assertRaisesRegex(BuildError, 'shorting additional interconnect'): 

171 fixture.build() 

172 

173 def test_device_terminal_names_cannot_collide_with_pin_names(self): 

174 fixture = TerminalFixture() 

175 fixture.pin('M1:G', 10, 10) 

176 fixture.device('M1', kdb.Region(kdb.Box(50, 0, 60, 20))) 

177 file = fixture.build() 

178 assert len({t.name for t in file.terminals}) == 2 

179 assert len(read_pex25d_text(write_pex25d_text(file), '<generated>').terminals) == 2