import os
import subprocess
from contextlib import contextmanager
from functools import cached_property
import numpy as np
import pytest
from gpaw import debug, setup_paths
from gpaw.cli.info import info
from gpaw.mpi import broadcast, world
from gpaw.test.gpwfile import GPWFiles, _all_gpw_methodnames
from gpaw.test.mmefile import MMEFiles
from gpaw.utilities import devnull
@contextmanager
def execute_in_tmp_path(request, tmp_path_factory):
if world.rank == 0:
# Obtain basename as
# * request.function.__name__ for function fixture
# * request.module.__name__ for module fixture
basename = getattr(request, request.scope).__name__
path = tmp_path_factory.mktemp(basename)
else:
path = None
path = broadcast(path, comm=world)
cwd = os.getcwd()
os.chdir(path)
try:
yield path
finally:
os.chdir(cwd)
@pytest.fixture(scope='module')
def set_device():
from gpaw.gpu import set_device
def log(*args, **kwargs):
kwargs.pop('parallel', None)
print(*args, **kwargs)
set_device(log, world)
@pytest.fixture(scope='module')
def dftd3():
from ase.calculators.dftd3 import PureDFTD3
try:
subprocess.call(PureDFTD3().command)
except FileNotFoundError:
pytest.skip('dftd3 command not found')
[docs]
@pytest.fixture(scope='function')
def in_tmp_dir(request, tmp_path_factory):
"""Run test function in a temporary directory."""
with execute_in_tmp_path(request, tmp_path_factory) as path:
yield path
@pytest.fixture(scope='module')
def module_tmp_path(request, tmp_path_factory):
"""Run test module in a temporary directory."""
with execute_in_tmp_path(request, tmp_path_factory) as path:
yield path
[docs]
@pytest.fixture
def add_cwd_to_setup_paths():
"""Temporarily add current working directory to setup_paths."""
try:
setup_paths[:0] = ['.']
yield
finally:
del setup_paths[:1]
@pytest.fixture(scope='session')
def sessionscoped_monkeypatch():
# The standard monkeypatch fixture is function scoped
# so we need to roll our own
with pytest.MonkeyPatch.context() as monkeypatch:
yield monkeypatch
@pytest.fixture(autouse=True, scope='session')
def monkeypatch_response_spline_points(sessionscoped_monkeypatch):
import gpaw.response.paw as paw
# https://gitlab.com/gpaw/gpaw/-/issues/984
sessionscoped_monkeypatch.setattr(paw, 'DEFAULT_RADIAL_POINTS', 2**10)
@pytest.fixture(autouse=True, scope='session')
def monkeypatch_allow_cpupy(sessionscoped_monkeypatch):
"""Monkey-patch `gpaw.new.builder.DFTComponentsBuilder.gpu` to
allow setting `gpu` regardless of the value of `GPAW_CPUPY`.
"""
import gpaw
from gpaw.new.builder import DFTComponentsBuilder
@cached_property
def gpu(self) -> bool:
return self.params.parallel.get('gpu', gpaw.GPAW_USE_GPUS)
sessionscoped_monkeypatch.setattr(DFTComponentsBuilder, 'gpu', gpu)
# Needed for `@cached_property` to work
gpu.__set_name__(DFTComponentsBuilder, 'gpu')
@pytest.fixture(autouse=True, scope='session')
def use_fftw_estimate_flag(sessionscoped_monkeypatch):
from gpaw.fftw import ESTIMATE, FFTWPlans
sessionscoped_monkeypatch.setattr(FFTWPlans, '_overwrite_flags', ESTIMATE)
[docs]
@pytest.fixture(scope='session')
def gpw_files(request, _not_world):
"""Reuse gpw-files.
Returns a dict mapping names to paths to gpw-files.
The files are written to the pytest cache and can be cleared using
pytest --cache-clear.
Example::
def test_something(gpw_files):
calc = GPAW(gpw_files['h2_lcao'])
...
Possible systems are:
* Bulk BCC-Li with 3x3x3 k-points: ``bcc_li_pw``, ``bcc_li_fd``,
``bcc_li_lcao``.
* O2 molecule: ``o2_pw``.
* H2 molecule: ``h2_pw``, ``h2_fd``, ``h2_lcao``.
* H2 molecule (not centered): ``h2_pw_0``.
* N2 molecule ``n2_pw``
* N molecule ``n_pw``
* Spin-polarized H atom: ``h_pw``.
* Polyethylene chain. One unit, 3 k-points, no symmetry:
``c2h4_pw_nosym``. Three units: ``c6h12_pw``.
* Bulk BN (zincblende) with 2x2x2 k-points and 9 converged bands:
``bn_pw``.
* h-BN layer with 3x3x1 (gamma center) k-points and 26 converged bands:
``hbn_pw``.
* Graphene with 6x6x1 k-points: ``graphene_pw``
* I2Sb2 (Z2 topological insulator) with 6x6x1 k-points and no
symmetries: ``i2sb2_pw_nosym``
* MoS2 with 6x6x1 k-points: ``mos2_pw`` and ``mos2_pw_nosym``
* MoS2 with 5x5x1 k-points: ``mos2_5x5_pw``
* NiCl2 with 6x6x1 k-points: ``nicl2_pw`` and ``nicl2_pw_evac``
* V2Br4 (AFM monolayer), LDA, 4x2x1 k-points, 28(+1) converged bands:
``v2br4_pw`` and ``v2br4_pw_nosym``
* Bulk Si, LDA, 2x2x2 k-points (gamma centered): ``si_pw``
* Bulk Si, LDA, 4x4x4 k-points, 8(+1) converged bands: ``fancy_si_pw``
and ``fancy_si_pw_nosym``
* Bulk SiC, LDA, 4x4x4 k-points, 8(+1) converged bands: ``sic_pw``
and ``sic_pw_spinpol``
* Bulk Fe, LDA, 4x4x4 k-points, 9(+1) converged bands: ``fe_pw``
and ``fe_pw_nosym``
* Bulk C, LDA, 2x2x2 k-points (gamma centered), ``c_pw``
* Bulk Co (HCP), 4x4x4 k-points, 12(+1) converged bands: ``co_pw``
and ``co_pw_nosym``
* Bulk SrVO3 (SC), 3x3x3 k-points, 20(+1) converged bands: ``srvo3_pw``
and ``srvo3_pw_nosym``
* Bulk Al, LDA, 4x4x4 k-points, 10(+1) converged bands: ``al_pw``
and ``al_pw_nosym``
* Bulk Al, LDA, 4x4x4 k-points, 4 converged bands: ``bse_al``
* Bulk Ag, LDA, 2x2x2 k-points, 6 converged bands,
2eV U on d-band: ``ag_pw``
* Bulk GaAs, LDA, 4x4x4 k-points, 8(+1) bands converged: ``gaas_pw``
and ``gaas_pw_nosym``
* Bulk P4, LDA, 4x4 k-points, 40 bands converged: ``p4_pw``
* Distorted bulk Fe, revTPSS: ``fe_pw_distorted``
* Distorted bulk Si, TPSS: ``si_pw_distorted``
* C2H4 molecule (ethene) with direct optimization,
in with finite difference: ``c2h4_do_fd``
* C2H4 molecule (ethene) with direct optimization with
plane wave mode: ``c2h4_do_pw``
* H3 molecule, numerical, plane wave,
complex: ``h3_do_num_pw_complex``
* H3 molecule, numerical, PW: ``h3_do_num_pw``
* H3 molecule, steepest descent, LCAO: ``h3_do_sd_lcao``
* H3 molecule, numerical, LCAO: ``h3_do_num_lcao``
* H2O molecule, GMF, LCAO: ``h2o_do_gmf_lcao``
* H2O molecule, LCAO: ``h2o_do_lcao``
* H2O molecule, constrained direct optimization,
LCAO: ``h2o_cdo_lcao``
* H2O molecule, constrained direct optimization,
LCAO, SIC: ``h2o_cdo_lcao_sic``
* H2O molecule, FD, SIC: ``h2o_fdsic``
* H2O molecule, LCAO, SIC: ``h2o_lcaosic``
* H2O molecule, MOM, LCAO, SIC: ``h2o_mom_lcaosic``
* H2O molecule, GMF, LCAO, SIC: ``h2o_gmf_lcaosic``
* H2O molecule, MOM, PW, SIC: ``h2o_mom_pwsic``
* H2O molecule, PW, SIC: ``h2o_pwsic``
* H2O molecule, MOM, direct optimization, PW: ``h2o_mom_do_pw``
* CO molecule, MOM, direct optimization,
LCAO: ``co_mom_do_lcao_forces``
* H2O molecule, MOM, direct optimization, LCAO: ``h2o_mom_do_lcao``
* H2O molecule, PZ localization, PW: ``h2o_pz_localization_pw``
* C2H4 molecule (ethene), direct optimization, LCAO: ``c2h4_do_lcao``
* H3 molecule, orthonorm, LCAO: ``h3_orthonorm_lcao``
* H2 molecule, SIC, SCFSIC: ``h2_sic_scfsic``
* H atom with magnetic moment: ``h_magmom``
* H atom, hessian, numerical, PW: ``h_hess_num_pw``
* H2 molecule breaking, iLCAO: ``h2_break_ilcao``
* H atom, generalized davidson, LCAO: ``h_do_gdavid_lcao``
* H2 molecule, MOM, direct optimization, PWH: ``h2_mom_do_pwh``
* H atom, hessian, numerical, LCAO: ``h_hess_num_lcao``
Files always include wave functions.
"""
cache = request.config.cache
gpaw_cachedir = cache.mkdir('gpaw_test_gpwfiles')
gpwfiles = GPWFiles(gpaw_cachedir, _not_world)
try:
setup_paths.append(gpwfiles.testing_setup_path)
yield gpwfiles
finally:
setup_paths.remove(gpwfiles.testing_setup_path)
@pytest.fixture(scope='session', params=sorted(_all_gpw_methodnames))
def all_gpw_files(request, gpw_files, pytestconfig):
"""This fixture parametrizes a test over all gpw_files.
For example pytest test_generate_gpwfiles.py -n 16 is a way to quickly
generate all gpw files independently of the rest of the test suite."""
# Note: Parametrizing over _all_gpw_methodnames must happen *after*
# it is populated, i.e., further down in the file than
# the @gpwfile decorator.
if request.param == 'Tl_box_pw' and world.size > 1:
pytest.skip(f'{request.param} gpwfile only works in serial')
# Accessing each file via __getitem__ executes the calculation:
return gpw_files[request.param]
@pytest.fixture(scope='session')
def mme_files(request, gpw_files, _not_world):
"""Reuse mme files"""
cache = request.config.cache
mme_cachedir = cache.mkdir('gpaw_test_mmefiles')
return MMEFiles(mme_cachedir, gpw_files, comm=_not_world)
class GPAWPlugin:
def __init__(self):
if world.rank == -1:
print()
info()
def pytest_terminal_summary(self, terminalreporter, exitstatus, config):
terminalreporter.section('GPAW-MPI stuff')
terminalreporter.write(f'size: {world.size}\n')
terminalreporter.write(f'debug-mode: {debug}\n')
@pytest.fixture(scope='function')
def not_parallelized(comm):
if comm.size > 1:
pytest.skip('Test/target of the test not parallelized.')
@pytest.fixture
def sg15_hydrogen():
from io import StringIO
from gpaw.test.pseudopotential.H_sg15 import pp_text
from gpaw.upf import read_sg15
# We can't easily load a non-python file from the test suite.
# Therefore we load the pseudopotential from a Python file.
return read_sg15(StringIO(pp_text))
def pytest_configure(config):
if world.rank != 0:
try:
tw = config.get_terminal_writer()
except (AssertionError, AttributeError):
pass
else:
tw._file = devnull
config.pluginmanager.register(GPAWPlugin(), 'pytest_gpaw')
def pytest_runtest_setup(item):
"""Skip some tests.
If:
* they depend on libxc and GPAW is not compiled with libxc
* they are before $PYTEST_START_AFTER
"""
from gpaw import get_libraries
libraries = get_libraries()
if world.size > 1:
for mark in item.iter_markers():
if mark.name == 'serial':
pytest.skip('Only run in serial')
else:
for mark in item.iter_markers():
if mark.name == 'parallel':
pytest.skip('Only run in parallel')
if item.location[0] <= os.environ.get('PYTEST_START_AFTER', ''):
pytest.skip('Not after $PYTEST_START_AFTER')
return
if libraries['libxc']:
return
if any(mark.name in {'libxc', 'mgga'}
for mark in item.iter_markers()):
pytest.skip('No LibXC.')
@pytest.fixture(scope='session')
def scalapack(require_real_mpi):
"""Skip if not compiled with sl.
This fixture otherwise does not return or do anything."""
from gpaw.utilities import compiled_with_sl
if not compiled_with_sl():
pytest.skip('no scalapack')
@pytest.fixture(scope='session')
def require_real_mpi(_not_world):
try:
_not_world.get_c_object()
except RuntimeError:
pytest.skip('This test requires actual MPI to be enabled')
def pytest_report_header(config, start_path):
# Use this to add custom information to the pytest printout.
yield f'GPAW MPI rank={world.rank}, size={world.size}'
# We want the user to be able to see where gpw files are cached,
# but the only way to see the cache location is to make a directory
# inside it. mkdir('') returns the toplevel cache dir without
# actually creating a subdirectory:
cachedir = config.cache.mkdir('')
yield f'Cache directory including gpw files: {cachedir}'
@pytest.fixture
def no_touch_world(monkeypatch, _not_world):
# We might also need module-scoped/session-scoped
import gpaw.mpi as mpi
import ase.parallel
# ase communicator is lazy-initialized. Make sure it is initialized
# by accessing rank.
aserank = ase.parallel.world.rank
assert aserank == mpi.world.rank
monkeypatch.setattr(mpi, '_NO_TOUCH_WORLD', True)
# We monkeypatch mpi.world.comm and sabotage it.
# But the C communicator object is immutable. We want to wrap it
# to intercept any calls and raise an error.
#
# With GPAW_DEBUG it will be wrapped, so in that case we can:
if debug:
# for obj in [mpi.world, ase.parallel.world]:
for attr in 'comm', 'rank', 'size':
monkeypatch.delattr(mpi.world, attr)
# XXX This is pretty brittle wrt. ASE internals
# (also requires new ASE master 2025-11-28)
import ase.parallel
monkeypatch.setattr(ase.parallel.world, 'comm', None)
@pytest.fixture(scope='session')
def _not_world():
return world.new_communicator(range(world.size))
@pytest.fixture
def comm(_not_world, no_touch_world):
return _not_world
@pytest.fixture
def rng():
"""Seeded random number generator.
Tests should be deterministic and should use this
fixture or initialize their own rng."""
return np.random.default_rng(42)
class MPIHelper:
def __init__(self, comm):
self.comm = comm
def GPAW(self, *args, **kwargs):
from gpaw import GPAW
return GPAW(*args, communicator=self.comm, **kwargs)
def NewGPAW(self, *args, **kwargs):
from gpaw.dft import GPAW
return GPAW(*args, communicator=self.comm, legacy_gpaw=False, **kwargs)
def OldGPAW(self, *args, **kwargs):
from gpaw.dft import GPAW as AnyGPAW
return AnyGPAW(*args, communicator=self.comm,
legacy_gpaw=True, **kwargs)
def restart(self, *args, **kwargs):
from gpaw import restart
return restart(*args, communicator=self.comm, **kwargs)
def print(self, *args, **kwargs):
if self.comm.rank == 0:
print(*args, **kwargs)
@pytest.fixture
def mpi(comm):
return MPIHelper(comm)