Coverage for gpaw/test/conftest.py: 84%

146 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-08 00:17 +0000

1import os 

2from contextlib import contextmanager 

3from functools import cached_property 

4 

5import numpy as np 

6import pytest 

7from gpaw import setup_paths, GPAW_NEW 

8from gpaw.cli.info import info 

9from gpaw.mpi import broadcast, world 

10from gpaw.test.gpwfile import GPWFiles, _all_gpw_methodnames 

11from gpaw.test.mmefile import MMEFiles 

12from gpaw.utilities import devnull 

13import subprocess 

14 

15 

16@contextmanager 

17def execute_in_tmp_path(request, tmp_path_factory): 

18 if world.rank == 0: 

19 # Obtain basename as 

20 # * request.function.__name__ for function fixture 

21 # * request.module.__name__ for module fixture 

22 basename = getattr(request, request.scope).__name__ 

23 path = tmp_path_factory.mktemp(basename) 

24 else: 

25 path = None 

26 path = broadcast(path) 

27 cwd = os.getcwd() 

28 os.chdir(path) 

29 try: 

30 yield path 

31 finally: 

32 os.chdir(cwd) 

33 

34 

35@pytest.fixture(scope='module') 

36def dftd3(): 

37 from ase.calculators.dftd3 import PureDFTD3 

38 try: 

39 subprocess.call(PureDFTD3().command) 

40 except FileNotFoundError: 

41 pytest.skip('dftd3 command not found') 

42 

43 

44@pytest.fixture(scope='function') 

45def in_tmp_dir(request, tmp_path_factory): 

46 """Run test function in a temporary directory.""" 

47 with execute_in_tmp_path(request, tmp_path_factory) as path: 

48 yield path 

49 

50 

51@pytest.fixture(scope='module') 

52def module_tmp_path(request, tmp_path_factory): 

53 """Run test module in a temporary directory.""" 

54 with execute_in_tmp_path(request, tmp_path_factory) as path: 

55 yield path 

56 

57 

58@pytest.fixture 

59def add_cwd_to_setup_paths(): 

60 """Temporarily add current working directory to setup_paths.""" 

61 try: 

62 setup_paths[:0] = ['.'] 

63 yield 

64 finally: 

65 del setup_paths[:1] 

66 

67 

68@pytest.fixture(scope='session') 

69def sessionscoped_monkeypatch(): 

70 # The standard monkeypatch fixture is function scoped 

71 # so we need to roll our own 

72 with pytest.MonkeyPatch.context() as monkeypatch: 

73 yield monkeypatch 

74 

75 

76@pytest.fixture(autouse=True, scope='session') 

77def monkeypatch_response_spline_points(sessionscoped_monkeypatch): 

78 import gpaw.response.paw as paw 

79 # https://gitlab.com/gpaw/gpaw/-/issues/984 

80 sessionscoped_monkeypatch.setattr(paw, 'DEFAULT_RADIAL_POINTS', 2**10) 

81 

82 

83@pytest.fixture(autouse=True, scope='session') 

84def monkeypatch_allow_cpupy(sessionscoped_monkeypatch): 

85 """Monkey-patch `gpaw.new.builder.DFTComponentsBuilder.gpu` to 

86 allow setting `gpu` regardless of the value of `GPAW_CPUPY`. 

87 """ 

88 import gpaw 

89 from gpaw.new.builder import DFTComponentsBuilder 

90 

91 @cached_property 

92 def gpu(self) -> bool: 

93 return self.params.parallel.get('gpu', gpaw.GPAW_USE_GPUS) 

94 

95 sessionscoped_monkeypatch.setattr(DFTComponentsBuilder, 'gpu', gpu) 

96 # Needed for `@cached_property` to work 

97 gpu.__set_name__(DFTComponentsBuilder, 'gpu') 

98 

99 

100@pytest.fixture(scope='session') 

101def gpw_files(request): 

102 """Reuse gpw-files. 

103 

104 Returns a dict mapping names to paths to gpw-files. 

105 The files are written to the pytest cache and can be cleared using 

106 pytest --cache-clear. 

107 

108 Example:: 

109 

110 def test_something(gpw_files): 

111 calc = GPAW(gpw_files['h2_lcao']) 

112 ... 

113 

114 Possible systems are: 

115 

116 * Bulk BCC-Li with 3x3x3 k-points: ``bcc_li_pw``, ``bcc_li_fd``, 

117 ``bcc_li_lcao``. 

118 

119 * O2 molecule: ``o2_pw``. 

120 

121 * H2 molecule: ``h2_pw``, ``h2_fd``, ``h2_lcao``. 

122 

123 * H2 molecule (not centered): ``h2_pw_0``. 

124 

125 * N2 molecule ``n2_pw`` 

126 

127 * N molecule ``n_pw`` 

128 

129 * Spin-polarized H atom: ``h_pw``. 

130 

131 * Polyethylene chain. One unit, 3 k-points, no symmetry: 

132 ``c2h4_pw_nosym``. Three units: ``c6h12_pw``. 

133 

134 * Bulk BN (zinkblende) with 2x2x2 k-points and 9 converged bands: 

135 ``bn_pw``. 

136 

137 * h-BN layer with 3x3x1 (gamma center) k-points and 26 converged bands: 

138 ``hbn_pw``. 

139 

140 * Graphene with 6x6x1 k-points: ``graphene_pw`` 

141 

142 * I2Sb2 (Z2 topological insulator) with 6x6x1 k-points and no 

143 symmetries: ``i2sb2_pw_nosym`` 

144 

145 * MoS2 with 6x6x1 k-points: ``mos2_pw`` and ``mos2_pw_nosym`` 

146 

147 * MoS2 with 5x5x1 k-points: ``mos2_5x5_pw`` 

148 

149 * NiCl2 with 6x6x1 k-points: ``nicl2_pw`` and ``nicl2_pw_evac`` 

150 

151 * V2Br4 (AFM monolayer), LDA, 4x2x1 k-points, 28(+1) converged bands: 

152 ``v2br4_pw`` and ``v2br4_pw_nosym`` 

153 

154 * Bulk Si, LDA, 2x2x2 k-points (gamma centered): ``si_pw`` 

155 

156 * Bulk Si, LDA, 4x4x4 k-points, 8(+1) converged bands: ``fancy_si_pw`` 

157 and ``fancy_si_pw_nosym`` 

158 

159 * Bulk SiC, LDA, 4x4x4 k-points, 8(+1) converged bands: ``sic_pw`` 

160 and ``sic_pw_spinpol`` 

161 

162 * Bulk Fe, LDA, 4x4x4 k-points, 9(+1) converged bands: ``fe_pw`` 

163 and ``fe_pw_nosym`` 

164 

165 * Bulk C, LDA, 2x2x2 k-points (gamma centered), ``c_pw`` 

166 

167 * Bulk Co (HCP), 4x4x4 k-points, 12(+1) converged bands: ``co_pw`` 

168 and ``co_pw_nosym`` 

169 

170 * Bulk SrVO3 (SC), 3x3x3 k-points, 20(+1) converged bands: ``srvo3_pw`` 

171 and ``srvo3_pw_nosym`` 

172 

173 * Bulk Al, LDA, 4x4x4 k-points, 10(+1) converged bands: ``al_pw`` 

174 and ``al_pw_nosym`` 

175 

176 * Bulk Al, LDA, 4x4x4 k-points, 4 converged bands: ``bse_al`` 

177 

178 * Bulk Ag, LDA, 2x2x2 k-points, 6 converged bands, 

179 2eV U on d-band: ``ag_pw`` 

180 

181 * Bulk GaAs, LDA, 4x4x4 k-points, 8(+1) bands converged: ``gaas_pw`` 

182 and ``gaas_pw_nosym`` 

183 

184 * Bulk P4, LDA, 4x4 k-points, 40 bands converged: ``p4_pw`` 

185 

186 * Distorted bulk Fe, revTPSS: ``fe_pw_distorted`` 

187 

188 * Distorted bulk Si, TPSS: ``si_pw_distorted`` 

189 

190 * C2H4 molecule (ethene) with direct optimization, 

191 in with finite difference: ``c2h4_do_fd`` 

192 

193 * C2H4 molecule (ethene) with direct optimization with 

194 plane wave mode: ``c2h4_do_pw`` 

195 

196 * H3 molecule, numerical, plane wave, 

197 complex: ``h3_do_num_pw_complex`` 

198 

199 * H3 molecule, numerical, PW: ``h3_do_num_pw`` 

200 

201 * H3 molecule, steepest descent, LCAO: ``h3_do_sd_lcao`` 

202 

203 * H3 molecule, numerical, LCAO: ``h3_do_num_lcao`` 

204 

205 * H2O molecule, GMF, LCAO: ``h2o_do_gmf_lcao`` 

206 

207 * H2O molecule, LCAO: ``h2o_do_lcao`` 

208 

209 * H2O molecule, constrained direct optimization, 

210 LCAO: ``h2o_cdo_lcao`` 

211 

212 * H2O molecule, constrained direct optimization, 

213 LCAO, SIC: ``h2o_cdo_lcao_sic`` 

214 

215 * H2O molecule, FD, SIC: ``h2o_fdsic`` 

216 

217 * H2O molecule, LCAO, SIC: ``h2o_lcaosic`` 

218 

219 * H2O molecule, MOM, LCAO, SIC: ``h2o_mom_lcaosic`` 

220 

221 * H2O molecule, GMF, LCAO, SIC: ``h2o_gmf_lcaosic`` 

222 

223 * H2O molecule, MOM, PW, SIC: ``h2o_mom_pwsic`` 

224 

225 * H2O molecule, PW, SIC: ``h2o_pwsic`` 

226 

227 * H2O molecule, MOM, direct optimization, PW: ``h2o_mom_do_pw`` 

228 

229 * CO molecule, MOM, direct optimization, 

230 LCAO: ``co_mom_do_lcao_forces`` 

231 

232 * H2O molecule, MOM, direct optimization, LCAO: ``h2o_mom_do_lcao`` 

233 

234 * H2O molecule, PZ localization, PW: ``h2o_pz_localization_pw`` 

235 

236 * C2H4 molecule (ethene), direct optimization, LCAO: ``c2h4_do_lcao`` 

237 

238 * H3 molecule, orthonorm, LCAO: ``h3_orthonorm_lcao`` 

239 

240 * H2 molecule, SIC, SCFSIC: ``h2_sic_scfsic`` 

241 

242 * H atom with magnetic moment: ``h_magmom`` 

243 

244 * H atom, hessian, numerical, PW: ``h_hess_num_pw`` 

245 

246 * H2 molecule breaking, iLCAO: ``h2_break_ilcao`` 

247 

248 * H atom, generalized davidson, LCAO: ``h_do_gdavid_lcao`` 

249 

250 * H2 molecule, MOM, direct optimization, PWH: ``h2_mom_do_pwh`` 

251 

252 * H atom, hessian, numerical, LCAO: ``h_hess_num_lcao`` 

253 

254 Files always include wave functions. 

255 """ 

256 cache = request.config.cache 

257 gpaw_cachedir = cache.mkdir('gpaw_test_gpwfiles') 

258 

259 gpwfiles = GPWFiles(gpaw_cachedir) 

260 

261 try: 

262 setup_paths.append(gpwfiles.testing_setup_path) 

263 yield gpwfiles 

264 finally: 

265 setup_paths.remove(gpwfiles.testing_setup_path) 

266 

267 

268@pytest.fixture(scope='session', params=sorted(_all_gpw_methodnames)) 

269def all_gpw_files(request, gpw_files, pytestconfig): 

270 """This fixture parametrizes a test over all gpw_files. 

271 

272 For example pytest test_generate_gpwfiles.py -n 16 is a way to quickly 

273 generate all gpw files independently of the rest of the test suite.""" 

274 

275 # Note: Parametrizing over _all_gpw_methodnames must happen *after* 

276 # it is populated, i.e., further down in the file than 

277 # the @gpwfile decorator. 

278 

279 # TODO This xfail-information should probably live closer to the 

280 # gpwfile definitions and not here in the fixture. 

281 skip_if_new = {'Cu3Au_qna', 

282 'nicl2_pw', 'nicl2_pw_evac', 

283 'v2br4_pw', 'v2br4_pw_nosym', 

284 'sih4_xc_gllbsc_fd', 'sih4_xc_gllbsc_lcao', 

285 'na2_isolated', 'h2o_xas'} 

286 if GPAW_NEW and request.param in skip_if_new: 

287 pytest.xfail(f'{request.param} gpwfile not yet working with GPAW_NEW') 

288 

289 if request.param == 'Tl_box_pw' and world.size > 1: 

290 pytest.skip(f'{request.param} gpwfile only works in serial') 

291 

292 # Accessing each file via __getitem__ executes the calculation: 

293 return gpw_files[request.param] 

294 

295 

296@pytest.fixture(scope='session') 

297def mme_files(request, gpw_files): 

298 """Reuse mme files""" 

299 cache = request.config.cache 

300 mme_cachedir = cache.mkdir('gpaw_test_mmefiles') 

301 

302 return MMEFiles(mme_cachedir, gpw_files) 

303 

304 

305class GPAWPlugin: 

306 def __init__(self): 

307 if world.rank == -1: 

308 print() 

309 info() 

310 

311 def pytest_terminal_summary(self, terminalreporter, exitstatus, config): 

312 from gpaw.mpi import size 

313 terminalreporter.section('GPAW-MPI stuff') 

314 terminalreporter.write(f'size: {size}\n') 

315 

316 

317@pytest.fixture 

318def sg15_hydrogen(): 

319 from io import StringIO 

320 from gpaw.test.pseudopotential.H_sg15 import pp_text 

321 from gpaw.upf import read_sg15 

322 # We can't easily load a non-python file from the test suite. 

323 # Therefore we load the pseudopotential from a Python file. 

324 return read_sg15(StringIO(pp_text)) 

325 

326 

327def pytest_configure(config): 

328 if world.rank != 0: 

329 try: 

330 tw = config.get_terminal_writer() 

331 except (AssertionError, AttributeError): 

332 pass 

333 else: 

334 tw._file = devnull 

335 config.pluginmanager.register(GPAWPlugin(), 'pytest_gpaw') 

336 

337 

338def pytest_runtest_setup(item): 

339 """Skip some tests. 

340 

341 If: 

342 

343 * they depend on libxc and GPAW is not compiled with libxc 

344 * they are before $PYTEST_START_AFTER 

345 """ 

346 from gpaw import get_libraries 

347 libraries = get_libraries() 

348 

349 if world.size > 1: 

350 for mark in item.iter_markers(): 

351 if mark.name == 'serial': 

352 pytest.skip('Only run in serial') 

353 else: 

354 for mark in item.iter_markers(): 

355 if mark.name == 'parallel': 

356 pytest.skip('Only run in parallel') 

357 

358 if item.location[0] <= os.environ.get('PYTEST_START_AFTER', ''): 

359 pytest.skip('Not after $PYTEST_START_AFTER') 

360 return 

361 

362 if libraries['libxc']: 

363 return 

364 

365 if any(mark.name in {'libxc', 'mgga'} 

366 for mark in item.iter_markers()): 

367 pytest.skip('No LibXC.') 

368 

369 

370@pytest.fixture 

371def scalapack(): 

372 """Skip if not compiled with sl. 

373 

374 This fixture otherwise does not return or do anything.""" 

375 from gpaw.utilities import compiled_with_sl 

376 if not compiled_with_sl(): 

377 pytest.skip('no scalapack') 

378 

379 

380@pytest.fixture 

381def needs_ase_master(): 

382 from ase.utils.filecache import MultiFileJSONCache 

383 try: 

384 MultiFileJSONCache('bla-bla', comm=None) 

385 except TypeError: 

386 pytest.skip('ASE is too old') 

387 

388 

389def pytest_report_header(config, start_path): 

390 # Use this to add custom information to the pytest printout. 

391 yield f'GPAW MPI rank={world.rank}, size={world.size}' 

392 

393 # We want the user to be able to see where gpw files are cached, 

394 # but the only way to see the cache location is to make a directory 

395 # inside it. mkdir('') returns the toplevel cache dir without 

396 # actually creating a subdirectory: 

397 cachedir = config.cache.mkdir('') 

398 yield f'Cache directory including gpw files: {cachedir}' 

399 

400 

401@pytest.fixture 

402def rng(): 

403 """Seeded random number generator. 

404 

405 Tests should be deterministic and should use this 

406 fixture or initialize their own rng.""" 

407 return np.random.default_rng(42) 

408 

409 

410@pytest.fixture 

411def gpaw_new() -> bool: 

412 """Are we testing the new code?""" 

413 return GPAW_NEW