File: test_vasp_input.py

package info (click to toggle)
python-ase 3.26.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,484 kB
  • sloc: python: 148,112; xml: 2,728; makefile: 110; javascript: 47
file content (349 lines) | stat: -rw-r--r-- 11,554 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# fmt: off
from io import StringIO
from unittest import mock

import numpy as np
import pytest

from ase.build import bulk
from ase.calculators.vasp.create_input import (
    GenerateVaspInput,
    _args_without_comment,
    _calc_nelect_from_charge,
    _from_vasp_bool,
    _to_vasp_bool,
    read_potcar_numbers_of_electrons,
)


def dict_is_subset(d1, d2):
    """True if all the key-value pairs in dict 1 are in dict 2"""
    # Note, we are using direct comparison, so we should not compare
    # floats if any real computations are made, as that would be unsafe.
    # Cannot use pytest.approx here, because of of string comparison
    # not being available in python 3.6.
    return all(key in d2 and d1[key] == d2[key] for key in d1)


@pytest.fixture()
def rng():
    return np.random.RandomState(seed=42)


@pytest.fixture()
def nacl(rng):
    atoms = bulk('NaCl', crystalstructure='rocksalt', a=4.1,
                 cubic=True) * (3, 3, 3)
    rng.shuffle(atoms.symbols)  # Ensure symbols are mixed
    return atoms


@pytest.fixture()
def vaspinput_factory(nacl):
    """Factory for GenerateVaspInput class, which mocks the generation of
    pseudopotentials."""
    def _vaspinput_factory(atoms=None, **kwargs) -> GenerateVaspInput:
        if atoms is None:
            atoms = nacl
        mocker = mock.Mock()
        inputs = GenerateVaspInput()
        inputs.set(**kwargs)
        inputs._build_pp_list = mocker(  # type: ignore[method-assign]
            return_value=None
        )
        inputs.initialize(atoms)
        return inputs

    return _vaspinput_factory


def test_sorting(nacl, vaspinput_factory):
    """Test that the sorting/resorting scheme works"""
    vaspinput = vaspinput_factory(atoms=nacl)
    srt = vaspinput.sort
    resrt = vaspinput.resort
    atoms = nacl.copy()
    assert atoms[srt] != nacl
    assert atoms[resrt] != nacl
    assert atoms[srt][resrt] == nacl

    # Check the first and second half of the sorted atoms have the same symbols
    assert len(atoms) % 2 == 0  # We should have an even number of atoms
    atoms_sorted = atoms[srt]
    N = len(atoms) // 2
    seq1 = set(atoms_sorted.symbols[:N])
    seq2 = set(atoms_sorted.symbols[N:])
    assert len(seq1) == 1
    assert len(seq2) == 1
    # Check that we have two different symbols
    assert len(seq1.intersection(seq2)) == 0


@pytest.fixture(params=['random', 'ones', 'binaries'])
def magmoms_factory(rng, request):
    """Factory for generating various kinds of magnetic moments"""
    kind = request.param
    if kind == 'random':
        # Array of random
        func = rng.rand
    elif kind == 'ones':
        # Array of just 1's
        func = np.ones
    elif kind == 'binaries':
        # Array of 0's and 1's
        def rand_binary(x):
            return rng.randint(2, size=x)

        func = rand_binary
    else:
        raise ValueError(f'Unknown kind: {kind}')

    def _magmoms_factory(atoms):
        magmoms = func(len(atoms))
        assert len(magmoms) == len(atoms)
        return magmoms

    return _magmoms_factory


def read_magmom_from_file(filename) -> np.ndarray:
    """Helper function to parse the magnetic moments from an INCAR file"""
    found = False
    with open(filename) as file:
        for line in file:
            # format "MAGMOM = n1*val1 n2*val2 ..."
            if 'MAGMOM = ' in line:
                found = True
                parts = line.strip().split()[2:]
                new_magmom = []
                for part in parts:
                    n, val = part.split('*')
                    # Add "val" to magmom "n" times
                    new_magmom += int(n) * [float(val)]
                break
    assert found
    return np.array(new_magmom)


@pytest.fixture()
def assert_magmom_equal_to_incar_value():
    """Fixture to compare a pre-made magmom array to the value
    a GenerateVaspInput.write_incar object writes to a file"""
    def _assert_magmom_equal_to_incar_value(atoms, expected_magmom, vaspinput):
        assert len(atoms) == len(expected_magmom)
        vaspinput.write_incar(atoms)
        new_magmom = read_magmom_from_file('INCAR')
        assert len(new_magmom) == len(expected_magmom)
        srt = vaspinput.sort
        resort = vaspinput.resort
        # We round to 4 digits
        assert np.allclose(expected_magmom, new_magmom[resort], atol=1e-3)
        assert np.allclose(np.array(expected_magmom)[srt],
                           new_magmom,
                           atol=1e-3)

    return _assert_magmom_equal_to_incar_value


@pytest.mark.parametrize('list_func', [list, tuple, np.array])
def test_write_magmom(magmoms_factory, list_func, nacl, vaspinput_factory,
                      assert_magmom_equal_to_incar_value, testdir):
    """Test writing magnetic moments to INCAR, and ensure we can do it
    passing different types of sequences"""
    magmom = magmoms_factory(nacl)

    vaspinput = vaspinput_factory(atoms=nacl, magmom=magmom, ispin=2)
    assert vaspinput.spinpol
    assert_magmom_equal_to_incar_value(nacl, magmom, vaspinput)


def test_atoms_with_initial_magmoms(magmoms_factory, nacl, vaspinput_factory,
                                    assert_magmom_equal_to_incar_value,
                                    testdir):
    """Test passing atoms with initial magnetic moments"""
    magmom = magmoms_factory(nacl)
    assert len(magmom) == len(nacl)
    nacl.set_initial_magnetic_moments(magmom)
    vaspinput = vaspinput_factory(atoms=nacl)
    assert vaspinput.spinpol
    assert_magmom_equal_to_incar_value(nacl, magmom, vaspinput)


def test_vasp_from_bool():
    for s in ('T', '.true.'):
        assert _from_vasp_bool(s) is True
    for s in ('f', '.False.'):
        assert _from_vasp_bool(s) is False
    with pytest.raises(ValueError):
        _from_vasp_bool('yes')
    with pytest.raises(AssertionError):
        _from_vasp_bool(True)


def test_vasp_to_bool():
    for x in ('T', '.true.', True):
        assert _to_vasp_bool(x) == '.TRUE.'
    for x in ('f', '.FALSE.', False):
        assert _to_vasp_bool(x) == '.FALSE.'

    with pytest.raises(ValueError):
        _to_vasp_bool('yes')
    with pytest.raises(AssertionError):
        _to_vasp_bool(1)


@pytest.mark.parametrize('args, expected_len',
                         [(['a', 'b', '#', 'c'], 2),
                          (['a', 'b', '!', 'c', '#', 'd'], 2),
                          (['#', 'a', 'b', '!', 'c', '#', 'd'], 0)])
def test_vasp_args_without_comment(args, expected_len):
    """Test comment splitting logic"""
    clean_args = _args_without_comment(args)
    assert len(clean_args) == expected_len


def test_vasp_xc(vaspinput_factory):
    """
    Run some tests to ensure that the xc setting in the VASP calculator
    works.
    """

    calc_vdw = vaspinput_factory(xc='optb86b-vdw')

    assert dict_is_subset({
        'param1': 0.1234,
        'param2': 1.0
    }, calc_vdw.float_params)
    assert calc_vdw.bool_params['luse_vdw'] is True

    calc_hse = vaspinput_factory(xc='hse06',
                                 hfscreen=0.1,
                                 gga='RE',
                                 encut=400,
                                 sigma=0.5)

    assert dict_is_subset({
        'hfscreen': 0.1,
        'encut': 400,
        'sigma': 0.5
    }, calc_hse.float_params)
    assert calc_hse.bool_params['lhfcalc'] is True
    assert dict_is_subset({'gga': 'RE'}, calc_hse.string_params)

    with pytest.warns(FutureWarning):
        calc_pw91 = vaspinput_factory(xc='pw91',
                                      kpts=(2, 2, 2),
                                      gamma=True,
                                      lreal='Auto')
        assert dict_is_subset(
            {
                'pp': 'PW91',
                'kpts': (2, 2, 2),
                'gamma': True,
                'reciprocal': False
            }, calc_pw91.input_params)


def test_ichain(vaspinput_factory):

    with pytest.warns(UserWarning):
        calc_warn = vaspinput_factory(ichain=1, ediffg=-0.01)
        calc_warn.write_incar(nacl)
        calc_warn.read_incar('INCAR')
        assert calc_warn.int_params['iopt'] == 1
        assert calc_warn.exp_params['ediffg'] == -0.01
        assert calc_warn.int_params['ibrion'] == 3
        assert calc_warn.float_params['potim'] == 0.0

    with pytest.raises(RuntimeError):
        calc_wrong = vaspinput_factory(ichain=1, ediffg=0.0001, iopt=1)
        calc_wrong.write_incar(nacl)
        calc_wrong.read_incar('INCAR')
        assert calc_wrong.int_params['iopt'] == 1

    calc = vaspinput_factory(ichain=1,
                             ediffg=-0.01,
                             iopt=1,
                             potim=0.0,
                             ibrion=3)
    calc.write_incar(nacl)
    calc.read_incar('INCAR')
    assert calc.int_params['iopt'] == 1
    assert calc.exp_params['ediffg'] == -0.01
    assert calc.int_params['ibrion'] == 3
    assert calc.float_params['potim'] == 0.0


def test_non_registered_keys(vaspinput_factory) -> None:
    """Test if non-registered INCAR keys can be written and read.

    Here the SCAN meta-GGA functional via LIBXC is tested.

    https://www.vasp.at/wiki/index.php/LIBXC1#Examples_of_INCAR

    """
    calc = vaspinput_factory(metagga='LIBXC')

    # Be sure that `libxc1` and `libxc2` are not in the registered INCAR keys.
    assert 'libxc1' not in calc.string_params
    assert 'libxc2' not in calc.int_params

    calc.set(libxc1='MGGA_X_SCAN')  # or 263
    calc.set(libxc2=267)  # or "MGGA_C_SCAN"

    calc.write_incar(nacl)
    calc.read_incar('INCAR')

    assert calc.string_params['libxc1'] == 'MGGA_X_SCAN'
    assert calc.int_params['libxc2'] == 267


def test_bool(tmp_path, vaspinput_factory):
    """Test that INCAR parser behaves similarly to Vasp, which uses
    default fortran 'read' parsing
    """

    for bool_str in ['t', 'T', 'true', 'TRUE', 'TrUe', '.true.', '.T', 'tbob']:
        with open(tmp_path / 'INCAR', 'w') as fout:
            fout.write('ENCUT = 100\n')
            fout.write(f'LCHARG = {bool_str}\n')
        calc = vaspinput_factory(encut=100)
        calc.read_incar(tmp_path / 'INCAR')
        assert calc.bool_params['lcharg']

    for bool_str in ['f', 'F', 'false', 'FALSE', 'FaLSe', '.false.', '.F',
                     'fbob']:
        with open(tmp_path / 'INCAR', 'w') as fout:
            fout.write('ENCUT = 100\n')
            fout.write(f'LCHARG = {bool_str}\n')
        calc = vaspinput_factory(encut=100)
        calc.read_incar(tmp_path / 'INCAR')
        assert not calc.bool_params['lcharg']

    for bool_str in ['x', '..true.', '1']:
        with open(tmp_path / 'INCAR', 'w') as fout:
            fout.write('ENCUT = 100\n')
            fout.write(f'LCHARG = {bool_str}\n')
        calc = vaspinput_factory(encut=100)
        with pytest.raises(ValueError):
            calc.read_incar(tmp_path / 'INCAR')


def test_read_potcar_numbers_of_electrons() -> None:
    """Test if the numbers of valence electrons are parsed correctly."""
    # POTCAR lines publicly available
    # https://www.vasp.at/wiki/index.php/POTCAR
    lines = """\
TITEL  = PAW_PBE Ti_pv 07Sep2000
...
...
...
POMASS =   47.880; ZVAL   =   10.000    mass and valenz
"""
    assert read_potcar_numbers_of_electrons(StringIO(lines)) == [('Ti', 10.0)]


def test_calc_nelect_from_charge() -> None:
    """Test if NELECT can be determined correctly."""
    assert _calc_nelect_from_charge(None, None, 10.0) is None
    assert _calc_nelect_from_charge(None, 4.0, 10.0) == 6.0