Coverage for tests/pex25d/validator_test.py: 99%

155 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 __future__ import annotations 

26 

27import allure 

28from typing import * 

29import unittest 

30 

31from klayout_pex.pex25d.diagnostics import DiagnosticsReport, Severity 

32from klayout_pex.pex25d.reader import ReadError 

33from klayout_pex.pex25d.resolver import resolve 

34from klayout_pex.pex25d.validator import validate 

35 

36from .pex25d_fixtures import MINIMAL, codes, read, replacing 

37 

38 

39def check(text: str, strict: bool = False) -> List[str]: 

40 """The codes validating ``text`` produces, reading errors included.""" 

41 report = DiagnosticsReport() 

42 try: 

43 message = read(text, report=report) 

44 except ReadError: 

45 return codes(report) 

46 validate(message, report=report, strict=strict) 

47 return codes(report) 

48 

49 

50def check_scene(scene: Any, strict: bool = False) -> List[str]: 

51 report = DiagnosticsReport() 

52 validate(scene, report=report, strict=strict) 

53 return codes(report) 

54 

55 

56def geometry(*records: str) -> str: 

57 """The fixture with its conductors and shapes replaced by ``records``.""" 

58 body = '\n'.join(records) 

59 return replacing( 

60 "CONDUCTOR A neta\n" 

61 "CONDUCTOR B netb\n" 

62 "BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 1.0 0.5\n" 

63 "BOX CONDUCTOR A LAYER via1 LL 0.2 0.1 UR 0.4 0.3\n" 

64 "BOX CONDUCTOR A LAYER met2 LL 0.0 0.0 UR 1.0 0.5\n" 

65 "POLYGON CONDUCTOR B LAYER met1 OUTER 2.0 0.0 3.0 0.0 3.0 1.0 2.0 1.0\n" 

66 "TERMINAL t1 CONDUCTOR A LAYER met1 KIND PIN LL 0.0 0.0 UR 0.2 0.5\n", 

67 body + '\n') 

68 

69 

70@allure.parent_suite("Unit Tests") 

71@allure.tag("PEX25D", "Validator") 

72class Pex25DValidatorTest(unittest.TestCase): 

73 def test_the_fixture_is_clean(self): 

74 assert check(MINIMAL) == [] 

75 assert check(MINIMAL, strict=True) == [] 

76 

77 def test_a_resolved_scene_is_clean(self): 

78 assert check_scene(resolve(read(MINIMAL)), strict=True) == [] 

79 

80 def test_validating_never_raises(self): 

81 # Whatever is wrong with a message, the diagnostics are the answer. 

82 broken = read(MINIMAL) 

83 broken.ClearField('ground_plane') 

84 broken.ClearField('units') 

85 del broken.metals[:] 

86 assert check_scene(broken) or True # the point is that it returned 

87 

88 # ------------------------------------------------------- semantic checks 

89 

90 def test_e0260_a_simple_dielectric_wrapping_an_inner_link(self): 

91 # Wrapping an inner link gives the fill the same depth as a film it 

92 # contains, and at equal depth there is no rule to break the tie. 

93 assert 'PEX25D-E0260' in check( 

94 replacing('DIELECTRIC_SIMPLE ild WRAPS lint', 'DIELECTRIC_SIMPLE ild WRAPS met1')) 

95 

96 def test_e0261_two_films_at_the_same_depth_on_one_root(self): 

97 assert 'PEX25D-E0261' in check( 

98 replacing('DIELECTRIC_SIMPLE ild WRAPS lint PERMITTIVITY 4.1 BETWEEN met1 met2', 

99 'DIELECTRIC_CONFORMAL other WRAPS met1 PERMITTIVITY 4.1 ' 

100 'THICKNESS_OVER_WRAPPED 0.1 THICKNESS_BESIDE_WRAPPED 0.08 ' 

101 'THICKNESS_ON_FIELD 0.0')) 

102 

103 def test_e0262_a_grid_that_does_not_divide_the_source_dbu(self): 

104 assert 'PEX25D-E0262' in check( 

105 replacing('META source_cell tiny', 

106 'META source_cell tiny\nMETA source_dbu 0.00015')) 

107 

108 def test_a_grid_that_does_divide_the_source_dbu(self): 

109 assert check(replacing('META source_cell tiny', 

110 'META source_cell tiny\nMETA source_dbu 0.001')) == [] 

111 

112 def test_e0263_a_terminal_on_a_floating_conductor(self): 

113 assert 'PEX25D-E0263' in check(replacing('CONDUCTOR A neta', 'CONDUCTOR A FLOATING')) 

114 

115 def test_w0264_a_conductor_with_no_geometry(self): 

116 report = DiagnosticsReport() 

117 message = read(replacing('CONDUCTOR B netb', 'CONDUCTOR B netb\nCONDUCTOR C netc'), 

118 report=report) 

119 validate(message, report=report) 

120 assert codes(report) == ['PEX25D-W0264'] 

121 assert report.diagnostics[0].severity == Severity.WARNING 

122 

123 def test_w0265_geometry_without_a_resistance_when_others_have_one(self): 

124 assert 'PEX25D-W0265' in check(replacing('RESISTANCE METAL met1 SHEET 0.125\n', '')) 

125 

126 def test_no_w0265_when_the_file_states_no_resistance_at_all(self): 

127 # A pure capacitance file is exactly what it was before RESISTANCE 

128 # existed, and must not start warning. 

129 text = MINIMAL 

130 for line in ('RESISTANCE TEMPERATURE 25.0\n', 

131 'RESISTANCE METAL met1 SHEET 0.125\n', 

132 'RESISTANCE METAL met2 SHEET 0.125\n', 

133 'RESISTANCE VIA via1 PER_CUT 4.5\n'): 

134 text = text.replace(line, '') 

135 assert check(text) == [] 

136 

137 def test_e0266_a_conductor_declared_twice(self): 

138 assert 'PEX25D-E0266' in check( 

139 replacing('CONDUCTOR B netb', 'CONDUCTOR B netb\nCONDUCTOR A netc')) 

140 

141 def test_e0267_a_terminal_declared_twice(self): 

142 assert 'PEX25D-E0267' in check( 

143 replacing('TERMINAL t1 CONDUCTOR A LAYER met1 KIND PIN LL 0.0 0.0 UR 0.2 0.5', 

144 'TERMINAL t1 CONDUCTOR A LAYER met1 KIND PIN LL 0.0 0.0 UR 0.2 0.5\n' 

145 'TERMINAL t1 CONDUCTOR A LAYER met2 KIND PIN LL 0.0 0.0 UR 0.2 0.5')) 

146 

147 def test_e0268_two_resistance_records_for_one_profile(self): 

148 assert 'PEX25D-E0268' in check( 

149 replacing('RESISTANCE METAL met2 SHEET 0.125', 

150 'RESISTANCE METAL met2 SHEET 0.125\nRESISTANCE METAL met2 SHEET 0.2')) 

151 

152 def test_w0269_coefficients_with_no_reference_temperature(self): 

153 assert 'PEX25D-W0269' in check( 

154 replacing('RESISTANCE TEMPERATURE 25.0\n', '').replace( 

155 'RESISTANCE METAL met1 SHEET 0.125', 

156 'RESISTANCE METAL met1 SHEET 0.125 TC1 0.003 TC2 0.0')) 

157 

158 # ---------------------------------------------------------- scene checks 

159 

160 def test_e0270_a_layer_with_no_kind(self): 

161 scene = resolve(read(MINIMAL)) 

162 scene.layers[0].ClearField('kind') 

163 assert 'PEX25D-E0270' in check_scene(scene) 

164 

165 def test_e0271_a_metal_with_no_height(self): 

166 scene = resolve(read(MINIMAL)) 

167 scene.layers[0].zhigh = scene.layers[0].zlow 

168 assert 'PEX25D-E0271' in check_scene(scene) 

169 

170 def test_a_via_between_abutting_layers_is_not_an_error(self): 

171 scene = resolve(read(MINIMAL)) 

172 via = next(l for l in scene.layers if l.name == 'via1') 

173 via.zhigh = via.zlow 

174 assert 'PEX25D-E0271' not in check_scene(scene) 

175 

176 def test_e0272_a_via_without_its_resolved_endpoints(self): 

177 scene = resolve(read(MINIMAL)) 

178 next(l for l in scene.layers if l.name == 'via1').ClearField('connects_above') 

179 assert 'PEX25D-E0272' in check_scene(scene) 

180 

181 def test_e0273_a_metal_carrying_a_via_resistance(self): 

182 scene = resolve(read(MINIMAL)) 

183 met1 = next(l for l in scene.layers if l.name == 'met1') 

184 met1.ClearField('metal_resistance') 

185 met1.via_resistance.per_cut = 1.0 

186 assert 'PEX25D-E0273' in check_scene(scene) 

187 

188 def test_e0274_dielectrics_out_of_depth_order(self): 

189 scene = resolve(read(MINIMAL)) 

190 reordered = list(scene.dielectrics)[::-1] 

191 del scene.dielectrics[:] 

192 for dielectric in reordered: 

193 scene.dielectrics.add().CopyFrom(dielectric) 

194 assert 'PEX25D-E0274' in check_scene(scene) 

195 

196 def test_e0275_a_dielectric_naming_a_profile_the_scene_has_not_got(self): 

197 scene = resolve(read(MINIMAL)) 

198 scene.dielectrics[0].wraps = 'nowhere' 

199 assert 'PEX25D-E0275' in check_scene(scene) 

200 

201 def test_e0276_a_terminal_on_a_layer_the_conductor_does_not_use(self): 

202 scene = resolve(read(MINIMAL)) 

203 scene.conductors[0].terminals[0].layer = 'met2' 

204 assert 'PEX25D-E0276' not in check_scene(scene) # A does have met2 

205 scene.conductors[0].terminals[0].layer = 'nowhere' 

206 assert 'PEX25D-E0276' in check_scene(scene) 

207 

208 def test_e0277_an_empty_terminal_node(self): 

209 scene = resolve(read(MINIMAL)) 

210 del scene.conductors[0].terminals[0].boxes[:] 

211 assert 'PEX25D-E0277' in check_scene(scene) 

212 

213 def test_e0279_geometry_on_a_layer_the_scene_has_not_got(self): 

214 scene = resolve(read(MINIMAL)) 

215 scene.conductors[0].regions[0].layer = 'nowhere' 

216 assert 'PEX25D-E0279' in check_scene(scene) 

217 

218 # ------------------------------------------------------- geometric tier 

219 

220 def test_the_geometric_tier_only_runs_under_strict(self): 

221 overlapping = geometry( 

222 'CONDUCTOR A neta', 

223 'CONDUCTOR B netb', 

224 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 1.0 1.0', 

225 'BOX CONDUCTOR B LAYER met1 LL 0.5 0.5 UR 1.5 1.5') 

226 assert 'PEX25D-E0309' not in check(overlapping) 

227 assert 'PEX25D-E0309' in check(overlapping, strict=True) 

228 

229 def test_e0302_a_ring_closed_explicitly(self): 

230 assert 'PEX25D-E0302' in check(geometry( 

231 'CONDUCTOR A neta', 

232 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0'), 

233 strict=True) 

234 

235 def test_e0303_a_repeated_vertex(self): 

236 assert 'PEX25D-E0303' in check(geometry( 

237 'CONDUCTOR A neta', 

238 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 1.0 0.0 1.0 0.0 1.0 1.0'), 

239 strict=True) 

240 

241 def test_e0304_a_collinear_ring(self): 

242 assert 'PEX25D-E0304' in check(geometry( 

243 'CONDUCTOR A neta', 

244 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 1.0 0.0 3.0 0.0 2.0 0.0'), 

245 strict=True) 

246 

247 def test_e0305_a_self_intersecting_ring(self): 

248 assert 'PEX25D-E0305' in check(geometry( 

249 'CONDUCTOR A neta', 

250 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 4.0 4.0 4.0 0.0 0.0 3.0'), 

251 strict=True) 

252 

253 def test_e0306_a_hole_touching_its_outer_ring(self): 

254 assert 'PEX25D-E0306' in check(geometry( 

255 'CONDUCTOR A neta', 

256 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 4.0 0.0 4.0 4.0 0.0 4.0 \\', 

257 ' HOLE 0.0 1.0 2.0 1.0 2.0 2.0 0.0 2.0'), 

258 strict=True) 

259 

260 def test_e0307_holes_that_touch(self): 

261 assert 'PEX25D-E0307' in check(geometry( 

262 'CONDUCTOR A neta', 

263 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\', 

264 ' HOLE 1.0 1.0 3.0 1.0 3.0 3.0 1.0 3.0 \\', 

265 ' HOLE 3.0 1.0 5.0 1.0 5.0 3.0 3.0 3.0'), 

266 strict=True) 

267 

268 def test_a_polygon_with_two_separate_holes_is_fine(self): 

269 assert check(geometry( 

270 'CONDUCTOR A neta', 

271 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\', 

272 ' HOLE 1.0 1.0 3.0 1.0 3.0 3.0 1.0 3.0 \\', 

273 ' HOLE 4.0 1.0 6.0 1.0 6.0 3.0 4.0 3.0'), 

274 strict=True) == [] 

275 

276 def test_e0308_an_empty_box(self): 

277 assert 'PEX25D-E0308' in check(geometry( 

278 'CONDUCTOR A neta', 

279 'BOX CONDUCTOR A LAYER met1 LL 1.0 1.0 UR 1.0 2.0'), 

280 strict=True) 

281 

282 # ---------------------------------------------------- conductor overlap 

283 

284 def overlap(self, *records: str) -> List[str]: 

285 return check(geometry(*records), strict=True) 

286 

287 def test_two_conductors_may_abut(self): 

288 assert self.overlap( 

289 'CONDUCTOR A neta', 

290 'CONDUCTOR B netb', 

291 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0', 

292 'BOX CONDUCTOR B LAYER met1 LL 2.0 0.0 UR 4.0 2.0') == [] 

293 

294 def test_one_conductor_may_overlap_itself(self): 

295 assert self.overlap( 

296 'CONDUCTOR A neta', 

297 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0', 

298 'BOX CONDUCTOR A LAYER met1 LL 1.0 1.0 UR 3.0 3.0') == [] 

299 

300 def test_layers_whose_z_extents_only_touch_do_not_overlap(self): 

301 # met1 and the via above it share a face by construction. 

302 assert self.overlap( 

303 'CONDUCTOR A neta', 

304 'CONDUCTOR B netb', 

305 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0', 

306 'BOX CONDUCTOR B LAYER via1 LL 0.0 0.0 UR 2.0 2.0') == [] 

307 

308 def test_e0309_boxes(self): 

309 assert 'PEX25D-E0309' in self.overlap( 

310 'CONDUCTOR A neta', 

311 'CONDUCTOR B netb', 

312 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0', 

313 'BOX CONDUCTOR B LAYER met1 LL 1.0 1.0 UR 3.0 3.0') 

314 

315 def test_e0309_identical_polygons(self): 

316 # Nothing on either boundary is strictly inside the other, which is why 

317 # sampling vertices and midpoints cannot answer this one. 

318 assert 'PEX25D-E0309' in self.overlap( 

319 'CONDUCTOR A neta', 

320 'CONDUCTOR B netb', 

321 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 2.0 0.0 2.0 2.0 0.0 2.0', 

322 'POLYGON CONDUCTOR B LAYER met1 OUTER 0.0 0.0 2.0 0.0 2.0 2.0 0.0 2.0') 

323 

324 def test_e0309_a_shape_reaching_out_of_a_hole(self): 

325 assert 'PEX25D-E0309' in self.overlap( 

326 'CONDUCTOR A neta', 

327 'CONDUCTOR B netb', 

328 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\', 

329 ' HOLE 2.0 2.0 6.0 2.0 6.0 6.0 2.0 6.0', 

330 'BOX CONDUCTOR B LAYER met1 LL 1.0 3.0 UR 5.0 5.0') 

331 

332 def test_a_conductor_may_sit_inside_another_ones_hole(self): 

333 assert self.overlap( 

334 'CONDUCTOR A neta', 

335 'CONDUCTOR B netb', 

336 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\', 

337 ' HOLE 2.0 2.0 6.0 2.0 6.0 6.0 2.0 6.0', 

338 'BOX CONDUCTOR B LAYER met1 LL 3.0 3.0 UR 5.0 5.0') == [] 

339 

340 def test_a_box_in_the_notch_of_an_l_shape(self): 

341 assert self.overlap( 

342 'CONDUCTOR A neta', 

343 'CONDUCTOR B netb', 

344 'POLYGON CONDUCTOR A LAYER met1 ' 

345 'OUTER 0.0 0.0 4.0 0.0 4.0 1.0 1.0 1.0 1.0 4.0 0.0 4.0', 

346 'BOX CONDUCTOR B LAYER met1 LL 1.0 1.0 UR 4.0 4.0') == []