File: test_write_control.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 (336 lines) | stat: -rw-r--r-- 12,104 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
# fmt: off
"""Test writing control.in files for Aims using ase.io.aims.

Control.in file contains calculation parameters such as the functional and
k grid as well as basis set size parameters. We write this file to a string
and assert we find expected values.
"""
# Standard imports.
import io
import re

# Third party imports.
import pytest

import ase.build
import ase.calculators.aims
import ase.io.aims


@pytest.fixture()
def parameters_dict():
    """Creates a parameters dictionary used to configure Aims simulation."""
    return {
        "xc": "LDA",
        "kpts": [2, 2, 2],
        "smearing": ("gaussian", 0.1),
        "output": ["dos 0.0 10.0 101 0.05", "hirshfeld"],
        "dos_kgrid_factors": [21, 21, 21],
        "vdw_correction_hirshfeld": True,
        "compute_forces": True,
        "output_level": "MD_light",
        "charge": 0.0}


def contains(pattern, txt):
    """"Regex search for pattern in the text."""
    return re.search(pattern, txt, re.M | re.DOTALL)


def write_control_to_string(ase_atoms_obj, parameters):
    """Helper function to write control.in file to a stringIO object.

    Args:
        ase_atoms_obj: ASE Atoms object that contains the atoms in the unit
            cell that are to be simulated.
        parameters: Dictionary that contains simulation parameters to be
            written to the control.in FHI-aims file which dictates to the
            aims executable how the simulation should be run.
    """
    string_output = io.StringIO()
    ase.io.aims.write_control(
        string_output, ase_atoms_obj, parameters)
    return string_output.getvalue()


@pytest.fixture()
def bulk_au():
    """Create an ASE.Atoms bulk object of Gold."""
    return ase.build.bulk("Au")


@pytest.fixture()
def bulk_aucl():
    """Create an ASE AuCl Atoms object"""
    return ase.build.bulk("AuCl",
                          crystalstructure="rocksalt",
                          a=6.32,
                          cubic=True)


AIMS_AU_SPECIES_LIGHT = """\
################################################################################
  species        Au
#     global species definitions
    nucleus             79
    mass                196.966569
#
    l_hartree           4
#
    cut_pot             3.5  1.5  1.0
    basis_dep_cutoff    1e-4
#
    radial_base         73 5.0
    radial_multiplier   1
    angular_grids specified
      division   0.5066   50
      division   0.9861  110
      division   1.2821  194
      division   1.5344  302
#      division   2.0427  434
#      division   2.1690  590
#      division   2.2710  770
#      division   2.3066  974
#      division   2.7597 1202
#      outer_grid 974
      outer_grid 302
################################################################################
#
#  Definition of "minimal" basis
#
################################################################################
#     valence basis states
    valence      6  s   1.
    valence      5  p   6.
    valence      5  d  10.
    valence      4  f  14.
#     ion occupancy
    ion_occ     6  s   0.
    ion_occ     5  p   6.
    ion_occ     5  d   9.
    ion_occ     4  f   14.
################################################################################
#
#  Suggested additional basis functions. For production calculations,
#  uncomment them one after another (the most important basis functions are
#  listed first).
#
#  Constructed for dimers: 2.10, 2.45, 3.00, 4.00 AA
#
################################################################################
#  "First tier" - max. impr. -161.60  meV, min. impr. -4.53 meV
     ionic 6 p auto
     hydro 4 f 7.4
     ionic 6 s auto
#     hydro 5 g 10
#     hydro 6 h 12.8
     hydro 3 d 2.5
#  "Second tier" - max. impr. -2.46  meV, min. impr. -0.28 meV
#     hydro 5 f 14.8
#     hydro 4 d 3.9
#     hydro 3 p 3.3
#     hydro 1 s 0.45
#     hydro 5 g 16.4
#     hydro 6 h 13.6
#  "Third tier" - max. impr. -0.49  meV, min. impr. -0.09 meV
#     hydro 4 f 5.2
#     hydro 4 d 5
#     hydro 5 g 8
#     hydro 5 p 8.2
#     hydro 6 d 12.4
#     hydro 6 s 14.8
#  Further basis functions: -0.08 meV and below
#     hydro 5 f 18.8
#     hydro 5 g 20
#    hydro 5 g 15.2
"""

# removed part of text that is not relevant to basis functions.
AIMS_CL_SPECIES_LIGHT = """\
################################################################################
#
#  Definition of "minimal" basis
#
################################################################################
#     valence basis states
    valence      3  s   2.
    valence      3  p   5.
#     ion occupancy
    ion_occ      3  s   1.
    ion_occ      3  p   4.
################################################################################
#
#  Suggested additional basis functions. For production calculations,
#  uncomment them one after another (the most important basis functions are
#  listed first).
#
#  Constructed for dimers: 1.65 A, 2.0 A, 2.5 A, 3.25 A, 4.0 A
#
################################################################################
#  "First tier" - improvements: -429.57 meV to -15.03 meV
     ionic 3 d auto
     hydro 2 p 1.9
     hydro 4 f 7.4
     ionic 3 s auto
#     hydro 5 g 10.4
#  "Second tier" - improvements: -7.84 meV to -0.48 meV
#     hydro 3 d 3.3
#     hydro 5 f 9.8
#     hydro 1 s 0.75
#     hydro 5 g 11.2
#     hydro 4 p 10.4
#  "Third tier" - improvements: -1.00 meV to -0.12 meV
#     hydro 4 d 12.8
#     hydro 4 f 4.6
#     hydro 4 d 10.8
#     hydro 2 s 1.8
#     hydro 3 p 3
"""


@pytest.fixture()
def aims_species_dir_light(tmp_path):
    """Create temporary directory to store species files."""
    species_dir_light = tmp_path / "light"
    species_dir_light.mkdir()
    path_au = species_dir_light / "79_Au_default"
    path_au.write_text(AIMS_AU_SPECIES_LIGHT)
    path_cl = species_dir_light / "17_Cl_default"
    path_cl.write_text(AIMS_CL_SPECIES_LIGHT)
    return species_dir_light


@pytest.mark.parametrize(
    "tier, expected_basis_functions",
    [
        (None, ["    ion_occ     4  f   14.", "#     hydro 5 f 14.8"]),
        (0, ["    ion_occ     4  f   14.", "#     ionic 6 p auto"]),
        (1, ["     hydro 6 h 12.8", "#     hydro 5 f 14.8"]),
        pytest.param("1", [None, None], marks=pytest.mark.xfail)])
def test_manipulate_tiers(tier, expected_basis_functions):
    """Test manipulating the basis functions using manipulate_tiers."""
    output_basis_functions = ase.io.aims.manipulate_tiers(
        AIMS_AU_SPECIES_LIGHT, tier=tier)
    for basis_function_line in expected_basis_functions:
        assert contains(basis_function_line, output_basis_functions)


def test_parse_species_path(aims_species_dir_light):
    """Test parsing the species file for all species."""
    species_array = ["Cl", "Au"]
    tier_array = [1, 0]
    basis_function_dict = ase.io.aims.parse_species_path(
        species_array=species_array,
        tier_array=tier_array,
        species_dir=aims_species_dir_light)
    assert "Cl" in basis_function_dict
    assert "Au" in basis_function_dict
    # First tier basis function should now be uncommented.
    assert "\n     hydro 5 g 10.4" in basis_function_dict["Cl"]
    # First tier basis function should be commented for Au.
    assert "#     ionic 6 s auto" in basis_function_dict["Au"]


def test_write_species(aims_species_dir_light):
    """Test writing species file."""
    parameters = {}
    file_handle = io.StringIO()
    basis_function_dict = {'Au': "#     ionic 6 p auto"}
    ase.io.aims.write_species(
        file_handle, basis_function_dict, parameters)
    assert contains("#     ionic 6 p auto", file_handle.getvalue())


@pytest.mark.parametrize(
    "output_level,tier,expected_basis_set_re", [
        ("tight", [0, 1],
         "#     hydro 4 f 7.4.*^     ionic 3 d auto\n     hydro 2 p 1.9"),
        ("tight", [1, 0],
         "ionic 6 p auto\n     hydro 4 f 7.4.*^#     ionic 3 d auto")])
def test_control_tier(
        aims_species_dir_light,
        bulk_aucl,
        parameters_dict,
        output_level: str, tier: int,
        expected_basis_set_re: str):
    """Test that the correct basis set functions are included.

    For a specific numerical settings (output_level) and basis set size (tier)
    we expect specific basis functions to be included for a species in the
    control.in file. We check that these functions are correctly commented
    for these combinations.

    Args:
        bulk_aucl: PyTest fixture to create a test AuCl bulk ase
            Atoms object that we can use to write out the control.in file.
        output_level: The numerical settings (e.g. light, tight, really_tight).
        tier: The basis set size (either None for standard, 0 for minimal, 1
            for tier1, etc...)
        expected_basis_set_re: Expression we expect to find in the control.in.
            We expect lines to be either commented or uncommented which
            indicate whether a basis set function is included or not in the
            calcuation.
    """
    parameters = parameters_dict
    parameters["output_level"] = output_level
    parameters["species_dir"] = aims_species_dir_light
    parameters['tier'] = tier

    control_file_as_string = write_control_to_string(bulk_aucl, parameters)

    assert contains(r"output_level\s+" + "", control_file_as_string)
    assert contains(expected_basis_set_re, control_file_as_string)


def test_control(bulk_au, parameters_dict, aims_species_dir_light):
    """Tests that control.in for a Gold bulk system works.

    This test tests several things simulationeously, much of
    the aims IO functionality for writing the conrol.in file, such as adding an
    AimsCube to the system.
    """
    # Copy the global parameters dicitonary to avoid rewriting common
    # parameters.
    parameters = parameters_dict
    parameters['species_dir'] = aims_species_dir_light
    # Add AimsCube to the parameter dictionary.
    parameters["cubes"] = ase.calculators.aims.AimsCube(
        plots=("delta_density",))
    # Write control.in file to a string which we can directly access for
    # testing.
    control_file_as_string = write_control_to_string(bulk_au, parameters)

    assert contains(r"k_grid\s+2 2 2", control_file_as_string)
    assert contains(
        r"k_offset\s+0.250000 0.250000 0.250000", control_file_as_string)
    assert contains(r"occupation_type\s+gaussian 0.1", control_file_as_string)
    assert contains(r"output\s+dos 0.0 10.0 101 0.05", control_file_as_string)
    assert contains(r"output\s+hirshfeld", control_file_as_string)
    assert contains(r"dos_kgrid_factors\s+21 21 21", control_file_as_string)
    assert contains(r"vdw_correction_hirshfeld", control_file_as_string)
    assert contains(r"compute_forces\s+.true.", control_file_as_string)
    assert contains(r"output_level\s+MD_light", control_file_as_string)
    assert contains(r"charge\s+0.0", control_file_as_string)
    assert contains("output cube delta_density", control_file_as_string)
    assert contains("   cube origin 0 0 0 ", control_file_as_string)
    assert contains("   cube edge 50 0.1 0.0 0.0 ", control_file_as_string)
    assert contains("   cube edge 50 0.0 0.1 0.0", control_file_as_string)
    assert contains("   cube edge 50 0.0 0.0 0.1", control_file_as_string)


@pytest.mark.parametrize(
    "functional,expected_functional_expression",
    [("PBE", r"xc\s+PBE"), ("LDA", r"xc\s+pw-lda"),
     pytest.param("PBE_06_Fake", None, marks=pytest.mark.xfail)])
def test_control_functional(
        aims_species_dir_light, bulk_au, parameters_dict, functional: str,
        expected_functional_expression: str):
    """Test that the functional written to the control.in file."""
    # Copy the global parameters dicitonary to avoid rewriting common
    # parameters. Then assign functional to parameter dictionary.
    parameters = parameters_dict
    parameters['species_dir'] = aims_species_dir_light
    parameters["xc"] = functional

    control_file_as_string = write_control_to_string(bulk_au, parameters)
    assert contains(expected_functional_expression, control_file_as_string)