Skip to content

Commit 3ddc48b

Browse files
authored
Aims phonon tutorials (#1136)
* Update test files for fitting with tutorial Makes the tutorial between aims and Vasp the same * Add FHI-aims phonon tutorial * Add utils for mock_aims monkey_patch for tutorials in FHI-aims added * Fix Lint errors in testing * Fix linting errors test data and the tutorial * Readd print statement in aims tutorial Printing here makes sense * Set species dir in tutorial Settings not setup in test area * Fix species dir fix for aims tutorial * Fix tutorial in test direc
1 parent 49357ed commit 3ddc48b

35 files changed

+607
-94
lines changed

src/atomate2/utils/testing/aims.py

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Utilities for testing FHI-aims calculations."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING, Any, Final, Literal
8+
9+
from jobflow import CURRENT_JOB
10+
from monty.os.path import zpath as monty_zpath
11+
from pymatgen.io.aims.sets.base import AimsInputGenerator
12+
13+
import atomate2.aims.jobs.base
14+
import atomate2.aims.run
15+
16+
if TYPE_CHECKING:
17+
from collections.abc import Callable, Generator, Sequence
18+
19+
from pymatgen.io.aims.sets import AimsInputSet
20+
from pytest import MonkeyPatch
21+
22+
logger = logging.getLogger("atomate2")
23+
24+
25+
_VFILES: Final = ("control.in",)
26+
_REF_PATHS: dict[str, str | Path] = {}
27+
_FAKE_RUN_AIMS_KWARGS: dict[str, dict] = {}
28+
29+
30+
def zpath(path: str | Path) -> Path:
31+
"""Return the path of a zip file.
32+
33+
Returns an existing (zipped or unzipped) file path given the unzipped
34+
version. If no path exists, returns the unmodified path.
35+
"""
36+
return Path(monty_zpath(str(path)))
37+
38+
39+
def monkeypatch_aims(
40+
monkeypatch: MonkeyPatch, ref_path: Path
41+
) -> Generator[Callable[[Any, Any], Any], None, None]:
42+
"""Allow one to mock (fake) running FHI-aims.
43+
44+
To use the fixture successfully, the following steps must be followed:
45+
1. "mock_aims" should be included as an argument to any test that would like to use
46+
its functionally.
47+
2. For each job in your workflow, you should prepare a reference directory
48+
containing two folders "inputs" (containing the reference input files expected
49+
to be produced by write_aims_input_set) and "outputs" (containing the expected
50+
output files to be produced by run_aims). These files should reside in a
51+
subdirectory of "tests/test_data/aims".
52+
3. Create a dictionary mapping each job name to its reference directory. Note that
53+
you should supply the reference directory relative to the "tests/test_data/aims"
54+
folder. For example, if your calculation has one job named "static" and the
55+
reference files are present in "tests/test_data/aims/Si_static", the dictionary
56+
would look like: ``{"static": "Si_static"}``.
57+
4. Optional (does not work yet): create a dictionary mapping each job name to
58+
custom keyword arguments that will be supplied to fake_run_aims.
59+
This way you can configure which control.in settings are expected for each job.
60+
For example, if your calculation has one job named "static" and you wish to
61+
validate that "xc" is set correctly in the control.in, your dictionary would
62+
look like
63+
``{"static": {"input_settings": {"relativistic": "atomic_zora scalar"}}``.
64+
5. Inside the test function, call `mock_aims(ref_paths, fake_aims_kwargs)`, where
65+
ref_paths is the dictionary created in step 3 and fake_aims_kwargs is the
66+
dictionary created in step 4.
67+
6. Run your aims job after calling `mock_aims`.
68+
69+
For examples, see the tests in tests/aims/jobs/core.py.
70+
"""
71+
72+
def mock_run_aims(*args, **kwargs) -> None: # noqa: ARG001
73+
name = CURRENT_JOB.job.name
74+
try:
75+
ref_dir = ref_path / _REF_PATHS[name]
76+
except KeyError:
77+
raise ValueError(
78+
f"no reference directory found for job {name!r}; "
79+
f"reference paths received={_REF_PATHS}"
80+
) from None
81+
fake_run_aims(ref_dir, **_FAKE_RUN_AIMS_KWARGS.get(name, {}))
82+
83+
get_input_set_orig = AimsInputGenerator.get_input_set
84+
85+
def mock_get_input_set(self: AimsInputGenerator, *args, **kwargs) -> AimsInputSet:
86+
return get_input_set_orig(self, *args, **kwargs)
87+
88+
monkeypatch.setattr(atomate2.aims.run, "run_aims", mock_run_aims)
89+
monkeypatch.setattr(atomate2.aims.jobs.base, "run_aims", mock_run_aims)
90+
monkeypatch.setattr(AimsInputGenerator, "get_input_set", mock_get_input_set)
91+
92+
def _run(ref_paths: dict, fake_run_aims_kwargs: dict | None = None) -> None:
93+
_REF_PATHS.update(ref_paths)
94+
_FAKE_RUN_AIMS_KWARGS.update(fake_run_aims_kwargs or {})
95+
96+
yield _run
97+
98+
monkeypatch.undo()
99+
_REF_PATHS.clear()
100+
_FAKE_RUN_AIMS_KWARGS.clear()
101+
102+
103+
def fake_run_aims(
104+
ref_path: str | Path,
105+
input_settings: Sequence[str] | None = None, # noqa: ARG001
106+
check_inputs: Sequence[Literal["control.in"]] = _VFILES, # noqa: ARG001
107+
clear_inputs: bool = False,
108+
) -> None:
109+
"""
110+
Emulate running aims and validate aims input files.
111+
112+
Parameters
113+
----------
114+
ref_path
115+
Path to reference directory with aims input files in the folder named 'inputs'
116+
and output files in the folder named 'outputs'.
117+
input_settings
118+
A list of input settings to check.
119+
check_inputs
120+
A list of aims input files to check. Supported options are "aims.inp"
121+
clear_inputs
122+
Whether to clear input files before copying in the reference aims outputs.
123+
"""
124+
logger.info("Running fake aims.")
125+
126+
ref_path = Path(ref_path)
127+
128+
logger.info("Verified inputs successfully")
129+
130+
if clear_inputs:
131+
clear_aims_inputs()
132+
133+
copy_aims_outputs(ref_path)
134+
135+
# pretend to run aims by copying pre-generated outputs from reference dir
136+
logger.info("Generated fake aims outputs")
137+
138+
139+
def clear_aims_inputs() -> None:
140+
"""Clean up FHI-aims input files."""
141+
for aims_file in ("control.in", "geometry.in", "parameters.json"):
142+
if Path(aims_file).exists():
143+
Path(aims_file).unlink()
144+
logger.info("Cleared aims inputs")
145+
146+
147+
def copy_aims_outputs(ref_path: str | Path) -> None:
148+
"""Copy FHI-aims output files from the reference directory."""
149+
import shutil
150+
151+
output_path = Path(ref_path) / "outputs"
152+
for output_file in output_path.iterdir():
153+
if output_file.is_file():
154+
shutil.copy(output_file, ".")

tests/aims/test_flows/test_anharmonic_quantification.py

+34-58
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from pathlib import Path
2+
13
import numpy as np
24
import pytest
35
from jobflow import run_locally
6+
from pymatgen.core import SETTINGS, Structure
47
from pymatgen.io.aims.sets.core import SocketIOSetGenerator, StaticSetGenerator
58

69
from atomate2.aims.flows.anharmonicity import AnharmonicityMaker
@@ -11,8 +14,11 @@
1114
PhononDisplacementMakerSocket,
1215
)
1316

17+
si_structure_file = Path(__file__).parents[2] / "test_data/structures/Si_diamond.cif"
18+
1419

15-
def test_anharmonic_quantification_oneshot(si, clean_dir, mock_aims, species_dir):
20+
def test_anharmonic_quantification_oneshot(clean_dir, mock_aims, species_dir):
21+
si = Structure.from_file(si_structure_file)
1622
# mapping from job name to directory containing test files
1723
ref_paths = {
1824
"Relaxation calculation": "phonon-relax-si",
@@ -21,94 +27,63 @@ def test_anharmonic_quantification_oneshot(si, clean_dir, mock_aims, species_dir
2127
"phonon static aims anharmonicity quant. 1/1": "anharm-os-si",
2228
}
2329

30+
SETTINGS["AIMS_SPECIES_DIR"] = species_dir / "tight"
2431
# settings passed to fake_run_aims; adjust these to check for certain input settings
2532
fake_run_aims_kwargs = {}
2633

2734
# automatically use fake FHI-aims
2835
mock_aims(ref_paths, fake_run_aims_kwargs)
2936

30-
parameters = {
31-
"species_dir": (species_dir / "light").as_posix(),
32-
"rlsy_symmetry": "all",
33-
"sc_accuracy_rho": 1e-06,
34-
"sc_accuracy_forces": 0.0001,
35-
"relativistic": "atomic_zora scalar",
36-
}
37-
38-
parameters_phonon_disp = dict(compute_forces=True, **parameters)
39-
parameters_phonon_disp["rlsy_symmetry"] = None
40-
4137
phonon_maker = PhononMaker(
42-
bulk_relax_maker=RelaxMaker.full_relaxation(
43-
user_params=parameters, user_kpoints_settings={"density": 5.0}
44-
),
45-
static_energy_maker=StaticMaker(
46-
input_set_generator=StaticSetGenerator(
47-
user_params=parameters, user_kpoints_settings={"density": 5.0}
48-
)
49-
),
38+
min_length=3.0,
39+
generate_frequencies_eigenvectors_kwargs={"tstep": 100},
40+
create_thermal_displacements=True,
41+
store_force_constants=True,
42+
born_maker=None,
5043
use_symmetrized_structure="primitive",
51-
phonon_displacement_maker=PhononDisplacementMaker(
52-
input_set_generator=StaticSetGenerator(
53-
user_params=parameters_phonon_disp,
54-
user_kpoints_settings={"density": 5.0},
55-
)
56-
),
5744
)
5845

5946
maker = AnharmonicityMaker(
6047
phonon_maker=phonon_maker,
6148
)
6249
maker.name = "anharmonicity"
63-
flow = maker.make(si, supercell_matrix=np.ones((3, 3)) - 2 * np.eye(3))
50+
flow = maker.make(
51+
si,
52+
supercell_matrix=np.ones((3, 3)) - 2 * np.eye(3),
53+
one_shot_approx=True,
54+
seed=1234,
55+
)
6456

6557
# run the flow or job and ensure that it finished running successfully
6658
responses = run_locally(flow, create_folders=True, ensure_success=True)
6759
dct = responses[flow.job_uuids[-1]][1].output.sigma_dict
68-
assert np.round(dct["one-shot"], 3) == 0.104
60+
assert np.round(dct["one-shot"], 3) == 0.120
61+
6962

63+
def test_anharmonic_quantification_full(clean_dir, mock_aims, species_dir):
64+
si = Structure.from_file(si_structure_file)
7065

71-
def test_anharmonic_quantification_full(si, clean_dir, mock_aims, species_dir):
7266
ref_paths = {
73-
"Relaxation calculation": "phonon-relax-si-full",
74-
"phonon static aims 1/1": "phonon-disp-si-full",
75-
"SCF Calculation": "phonon-energy-si-full",
67+
"Relaxation calculation": "phonon-relax-si",
68+
"phonon static aims 1/1": "phonon-disp-si",
69+
"SCF Calculation": "phonon-energy-si",
7670
"phonon static aims anharmonicity quant. 1/1": "anharm-si-full",
7771
}
7872

73+
SETTINGS["AIMS_SPECIES_DIR"] = species_dir / "tight"
7974
# settings passed to fake_run_aims; adjust these to check for certain input settings
8075
fake_run_aims_kwargs = {}
8176

8277
# automatically use fake FHI-aims
8378
mock_aims(ref_paths, fake_run_aims_kwargs)
8479

85-
parameters = {
86-
"species_dir": (species_dir / "light").as_posix(),
87-
"rlsy_symmetry": "all",
88-
"sc_accuracy_rho": 1e-06,
89-
"sc_accuracy_forces": 0.0001,
90-
"relativistic": "atomic_zora scalar",
91-
}
92-
93-
parameters_phonon_disp = dict(compute_forces=True, **parameters)
94-
parameters_phonon_disp["rlsy_symmetry"] = None
95-
9680
phonon_maker = PhononMaker(
97-
bulk_relax_maker=RelaxMaker.full_relaxation(
98-
user_params=parameters, user_kpoints_settings={"density": 5.0}
99-
),
100-
static_energy_maker=StaticMaker(
101-
input_set_generator=StaticSetGenerator(
102-
user_params=parameters, user_kpoints_settings={"density": 5.0}
103-
)
104-
),
81+
min_length=3.0,
82+
generate_frequencies_eigenvectors_kwargs={"tstep": 100},
83+
create_thermal_displacements=True,
84+
store_force_constants=True,
85+
born_maker=None,
10586
use_symmetrized_structure="primitive",
106-
phonon_displacement_maker=PhononDisplacementMaker(
107-
input_set_generator=StaticSetGenerator(
108-
user_params=parameters_phonon_disp,
109-
user_kpoints_settings={"density": 5.0},
110-
)
111-
),
11287
)
11388

11489
maker = AnharmonicityMaker(
@@ -125,7 +100,8 @@ def test_anharmonic_quantification_full(si, clean_dir, mock_aims, species_dir):
125100
# run the flow or job and ensure that it finished running successfully
126101
responses = run_locally(flow, create_folders=True, ensure_success=True)
127102
dct = responses[flow.job_uuids[-1]][1].output.sigma_dict
128-
assert pytest.approx(dct["full"], 0.001) == 0.12012
103+
104+
assert pytest.approx(dct["full"], 0.001) == 0.144264
129105

130106

131107
def test_mode_resolved_anharmonic_quantification(si, clean_dir, mock_aims, species_dir):

tests/aims/test_flows/test_phonon_workflow.py

+15-23
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"""Test various makers"""
22

33
import json
4+
from pathlib import Path
45

56
import pytest
67

8+
si_structure_file = Path(__file__).parents[2] / "test_data/structures/Si_diamond.cif"
79

8-
def test_phonon_flow(si, clean_dir, mock_aims, species_dir):
10+
11+
def test_phonon_flow(clean_dir, mock_aims, species_dir):
912
import numpy as np
1013
from jobflow import run_locally
11-
from pymatgen.io.aims.sets.core import StaticSetGenerator
14+
from pymatgen.core import Structure
1215

1316
from atomate2.aims.flows.phonons import PhononMaker
14-
from atomate2.aims.jobs.core import RelaxMaker, StaticMaker
15-
from atomate2.aims.jobs.phonons import PhononDisplacementMaker
17+
18+
si = Structure.from_file(si_structure_file)
1619

1720
# mapping from job name to directory containing test files
1821
ref_paths = {
@@ -26,26 +29,15 @@ def test_phonon_flow(si, clean_dir, mock_aims, species_dir):
2629

2730
# automatically use fake FHI-aims
2831
mock_aims(ref_paths, fake_run_aims_kwargs)
29-
30-
parameters = {
31-
"k_grid": [2, 2, 2],
32-
"species_dir": (species_dir / "light").as_posix(),
33-
}
3432
# generate job
3533

36-
parameters_phonon_disp = dict(compute_forces=True, **parameters)
3734
maker = PhononMaker(
38-
bulk_relax_maker=RelaxMaker.full_relaxation(user_params=parameters),
39-
static_energy_maker=StaticMaker(
40-
input_set_generator=StaticSetGenerator(user_params=parameters)
41-
),
35+
min_length=3.0,
36+
generate_frequencies_eigenvectors_kwargs={"tstep": 100},
37+
create_thermal_displacements=True,
38+
store_force_constants=True,
39+
born_maker=None,
4240
use_symmetrized_structure="primitive",
43-
phonon_displacement_maker=PhononDisplacementMaker(
44-
input_set_generator=StaticSetGenerator(
45-
user_params=parameters_phonon_disp,
46-
user_kpoints_settings={"density": 5.0, "even": True},
47-
)
48-
),
4941
)
5042
maker.name = "phonons"
5143
flow = maker.make(si, supercell_matrix=np.ones((3, 3)) - 2 * np.eye(3))
@@ -84,11 +76,11 @@ def test_phonon_flow(si, clean_dir, mock_aims, species_dir):
8476
assert output.born is None
8577
assert not output.has_imaginary_modes
8678

87-
assert output.temperatures == list(range(0, 500, 10))
79+
assert output.temperatures == list(range(0, 500, 100))
8880
assert output.heat_capacities[0] == 0.0
89-
assert np.round(output.heat_capacities[-1], 2) == 23.06
81+
assert np.round(output.heat_capacities[-1], 2) == 21.95
9082
assert output.phonopy_settings.schema_json() == json.dumps(phonopy_settings_schema)
91-
assert np.round(output.phonon_bandstructure.bands[-1, 0], 2) == 14.41
83+
assert np.round(output.phonon_bandstructure.bands[-1, 0], 2) == 15.1
9284

9385

9486
@pytest.mark.skip(reason="Currently not mocked and needs FHI-aims binary")
Binary file not shown.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"xc": "pbe", "relativistic": "atomic_zora scalar", "compute_forces": true, "k_grid": [2, 2, 2], "species_dir": "/home/purcellt/git/atomate2/tests/aims/species_dir/light", "rlsy_symmetry": null, "sc_accuracy_rho": 1e-06, "sc_accuracy_forces": 0.0001}
1+
{"xc": "pbe", "relativistic": "atomic_zora scalar", "compute_forces": true, "k_grid": [6, 6, 6]}
Binary file not shown.

0 commit comments

Comments
 (0)