Skip to content

Commit f1894c1

Browse files
authored
Workflow for Quasi-harmonic approximation (forcefields and VASP) (#903)
* add structures to eos output * fix defaults in phonon schema * add qha flows and one full workflow test * really add qha flows * fix description of qha schema file * remove tests * add schema test instead * new test files * remove future import * Fix tests by changing the warning treatment back to default after the QHA api * fix linting * add switch to eos types * change how warnings are treated * add safer code for literal * add more tests * fix tests add kwargs * Fix tests * Fix tests * reformat * linting * update prev calc * Apply suggestions from code review * fix test tolerance * draft a first vasp workflow * fix return * fix return * make analysis optional and and names for jobs * add vasp test * fix document * fix dc * update * fix phonon tests * fix linting * fix schema * change to phonon static * change phonon maker * fix tests * fix test * update qha_fresh * fix comments * fix comments * connect flows * fix linting * correct documentation * document workflows, fix imports
1 parent fdd2c9e commit f1894c1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+58712
-12
lines changed

docs/user/codes/vasp.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,21 @@ With the help of phonopy, these forces are then converted into a dynamical matri
260260
The dynamical matrices of three structures are then used as an input to the phonopy Grueneisen api
261261
to compute mode-dependent Grueneisen parameters.
262262

263+
264+
### Quasi-harmonic Workflow
265+
Uses the quasi-harmonic approximation with the help of Phonopy to compute thermodynamic properties.
266+
First, a tight relaxation is performed. Subsequently, several optimizations at different constant
267+
volumes are performed. At each of the volumes, an additional phonon run is performed as well.
268+
Afterwards, equation of state fits are performed with phonopy.
269+
270+
271+
272+
### Equation of State Workflow
273+
An equation of state workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant
274+
volumes are performed. Additional static calculations might be performed afterwards to arrive at more
275+
accurate energies. Then, an equation of state fit is performed with pymatgen.
276+
277+
263278
### LOBSTER
264279

265280
Perform bonding analysis with [LOBSTER](http://cohp.de/) and [LobsterPy](https://github.com/jageo/lobsterpy)

src/atomate2/common/flows/eos.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
8181
("relax", "static") if self.static_maker else ("relax",)
8282
)
8383
flow_output: dict[str, dict] = {
84-
key: {quantity: [] for quantity in ("energy", "volume", "stress")}
84+
key: {
85+
quantity: []
86+
for quantity in ("energy", "volume", "stress", "structure", "dir_name")
87+
}
8588
for key in job_types
8689
}
8790

@@ -163,9 +166,12 @@ def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
163166
for key in job_types:
164167
for idx in range(len(jobs[key])):
165168
output = jobs[key][idx].output.output
169+
dir_name = jobs[key][idx].output.dir_name
166170
flow_output[key]["energy"] += [output.energy]
167171
flow_output[key]["volume"] += [output.structure.volume]
168172
flow_output[key]["stress"] += [output.stress]
173+
flow_output[key]["structure"] += [output.structure]
174+
flow_output[key]["dir_name"] += [dir_name]
169175

170176
if self.postprocessor is not None:
171177
min_points = self.postprocessor.min_data_points

src/atomate2/common/flows/qha.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Define common QHA flow agnostic to electronic-structure code."""
2+
3+
from __future__ import annotations
4+
5+
import warnings
6+
from abc import ABC, abstractmethod
7+
from dataclasses import dataclass, field
8+
from typing import TYPE_CHECKING, Literal
9+
10+
from jobflow import Flow, Maker
11+
12+
from atomate2.common.flows.eos import CommonEosMaker
13+
from atomate2.common.jobs.qha import analyze_free_energy, get_phonon_jobs
14+
15+
if TYPE_CHECKING:
16+
from pathlib import Path
17+
18+
from pymatgen.core import Structure
19+
20+
from atomate2.common.flows.phonons import BasePhononMaker
21+
from atomate2.forcefields.jobs import ForceFieldRelaxMaker
22+
from atomate2.vasp.jobs.core import BaseVaspMaker
23+
24+
supported_eos = frozenset(("vinet", "birch_murnaghan", "murnaghan"))
25+
26+
27+
@dataclass
28+
class CommonQhaMaker(Maker, ABC):
29+
"""
30+
Use the quasi-harmonic approximation.
31+
32+
First relax a structure.
33+
Then we scale the relaxed structure, and
34+
then compute harmonic phonons for each scaled
35+
structure with Phonopy.
36+
Finally, we compute the Gibb's free energy and
37+
other thermodynamic properties available from
38+
the quasi-harmonic approximation.
39+
40+
Note: We do not consider electronic free energies so far.
41+
This might be problematic for metals (see e.g.,
42+
Wolverton and Zunger, Phys. Rev. B, 52, 8813 (1994).)
43+
44+
Note: Magnetic Materials have never been computed with
45+
this workflow.
46+
47+
Parameters
48+
----------
49+
name: str
50+
Name of the flows produced by this maker.
51+
initial_relax_maker: .ForceFieldRelaxMaker | .BaseVaspMaker | None
52+
Maker to relax the input structure.
53+
eos_relax_maker: .ForceFieldRelaxMaker | .BaseVaspMaker | None
54+
Maker to relax deformed structures for the EOS fit.
55+
The volume has to be fixed!
56+
phonon_maker: .BasePhononMaker | None
57+
Maker to compute phonons. The volume has to be fixed!
58+
The beforehand relaxation could be switched off.
59+
linear_strain: tuple[float, float]
60+
Percentage linear strain to apply as a deformation, default = -5% to 5%.
61+
number_of_frames: int
62+
Number of strain calculations to do for EOS fit, default = 6.
63+
t_max: float | None
64+
Maximum temperature until which the QHA will be performed
65+
pressure: float | None
66+
Pressure at which the QHA will be performed (default None, no pressure)
67+
skip_analysis: bool
68+
Skips the analysis step and only performs EOS and phonon computations.
69+
ignore_imaginary_modes: bool
70+
By default, volumes where the harmonic phonon approximation shows imaginary
71+
will be ignored
72+
eos_type: str
73+
Equation of State type used for the fitting. Defaults to vinet.
74+
"""
75+
76+
name: str = "QHA Maker"
77+
initial_relax_maker: ForceFieldRelaxMaker | BaseVaspMaker | None = None
78+
eos_relax_maker: ForceFieldRelaxMaker | BaseVaspMaker | None = None
79+
phonon_maker: BasePhononMaker | None = None
80+
linear_strain: tuple[float, float] = (-0.05, 0.05)
81+
number_of_frames: int = 6
82+
t_max: float | None = None
83+
pressure: float | None = None
84+
ignore_imaginary_modes: bool = False
85+
skip_analysis: bool = False
86+
eos_type: Literal["vinet", "birch_murnaghan", "murnaghan"] = "vinet"
87+
analyze_free_energy_kwargs: dict = field(default_factory=dict)
88+
# TODO: implement advanced handling of
89+
# imaginary modes in phonon runs (i.e., fitting procedures)
90+
91+
def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
92+
"""Run an EOS flow.
93+
94+
Parameters
95+
----------
96+
structure : Structure
97+
A pymatgen structure object.
98+
prev_dir : str or Path or None
99+
A previous calculation directory to copy output files from.
100+
101+
Returns
102+
-------
103+
.Flow, a QHA flow
104+
"""
105+
if self.eos_type not in supported_eos:
106+
raise ValueError(
107+
"EOS not supported.",
108+
"Please choose 'vinet', 'birch_murnaghan', 'murnaghan'",
109+
)
110+
111+
qha_jobs = []
112+
113+
# In this way, one can easily exchange makers and enforce postprocessor None
114+
self.eos = CommonEosMaker(
115+
initial_relax_maker=self.initial_relax_maker,
116+
eos_relax_maker=self.eos_relax_maker,
117+
static_maker=None,
118+
postprocessor=None,
119+
number_of_frames=self.number_of_frames,
120+
)
121+
122+
eos_job = self.eos.make(structure)
123+
qha_jobs.append(eos_job)
124+
125+
phonon_jobs = get_phonon_jobs(
126+
phonon_maker=self.phonon_maker, eos_output=eos_job.output
127+
)
128+
qha_jobs.append(phonon_jobs)
129+
if not self.skip_analysis:
130+
analysis = analyze_free_energy(
131+
phonon_jobs.output,
132+
structure=structure,
133+
t_max=self.t_max,
134+
pressure=self.pressure,
135+
ignore_imaginary_modes=self.ignore_imaginary_modes,
136+
eos_type=self.eos_type,
137+
**self.analyze_free_energy_kwargs,
138+
)
139+
qha_jobs.append(analysis)
140+
141+
return Flow(qha_jobs)
142+
143+
def __post_init__(self) -> None:
144+
"""Test settings during the initialisation."""
145+
if self.phonon_maker.bulk_relax_maker is not None:
146+
warnings.warn(
147+
"An additional bulk_relax_maker has been added "
148+
"to the phonon workflow. Please be aware "
149+
"that the volume needs to be kept fixed.",
150+
stacklevel=2,
151+
)
152+
# if self.phonon_maker.symprec != self.symprec:
153+
# warnings.warn(
154+
# "You are using different symmetry precisions "
155+
# "in the phonon makers and other parts of the "
156+
# "QHA workflow.",
157+
# stacklevel=2,
158+
# )
159+
if self.phonon_maker.static_energy_maker is None:
160+
warnings.warn(
161+
"A static energy maker "
162+
"is needed for "
163+
"this workflow."
164+
" Please add the static_energy_maker.",
165+
stacklevel=2,
166+
)
167+
168+
@property
169+
@abstractmethod
170+
def prev_calc_dir_argname(self) -> str | None:
171+
"""Name of argument informing static maker of previous calculation directory.
172+
173+
As this differs between different DFT codes (e.g., VASP, CP2K), it
174+
has been left as a property to be implemented by the inheriting class.
175+
176+
Note: this is only applicable if a relax_maker is specified; i.e., two
177+
calculations are performed for each ordering (relax -> static)
178+
"""

src/atomate2/common/jobs/eos.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def fit(self, eos_flow_output: dict[str, Any]) -> None:
8282
"energy": list, <required>
8383
"volume": list, <required>
8484
"stress": list <optional>
85+
"structure": list <not needed for the fit>
86+
"dir_name": list <optional for the fit>
8587
},
8688
"initial_<key>": {"E0": float, "V0": float} <optional>,
8789
for <key> in ("relax", "static")

src/atomate2/common/jobs/qha.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Jobs for running qha calculations."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import TYPE_CHECKING
7+
8+
from jobflow import Flow, Response, job
9+
10+
from atomate2.common.schemas.phonons import PhononBSDOSDoc
11+
from atomate2.common.schemas.qha import PhononQHADoc
12+
13+
if TYPE_CHECKING:
14+
from pymatgen.core.structure import Structure
15+
16+
from atomate2.common.flows.phonons import BasePhononMaker
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
@job(
22+
data=[PhononBSDOSDoc],
23+
)
24+
def get_phonon_jobs(phonon_maker: BasePhononMaker, eos_output: dict) -> Flow:
25+
"""
26+
Start all relevant phonon jobs.
27+
28+
Parameters
29+
----------
30+
phonon_maker: .BasePhononMaker
31+
Maker to start harmonic phonon runs.
32+
eos_output: dict
33+
Output from EOSMaker
34+
35+
"""
36+
phonon_jobs = []
37+
outputs = []
38+
for istructure, structure in enumerate(eos_output["relax"]["structure"]):
39+
if eos_output["relax"]["dir_name"][istructure] is not None:
40+
phonon_job = phonon_maker.make(
41+
structure, prev_dir=eos_output["relax"]["dir_name"][istructure]
42+
)
43+
else:
44+
phonon_job = phonon_maker.make(structure)
45+
phonon_job.append_name(f" eos deformation {istructure + 1}")
46+
phonon_jobs.append(phonon_job)
47+
outputs.append(phonon_job.output)
48+
replace_flow = Flow(phonon_jobs, outputs)
49+
return Response(replace=replace_flow)
50+
51+
52+
@job(
53+
output_schema=PhononQHADoc,
54+
data=["free_energies", "heat_capacities", "entropies", "helmholtz_volume"],
55+
)
56+
def analyze_free_energy(
57+
phonon_outputs: list[PhononBSDOSDoc],
58+
structure: Structure,
59+
t_max: float = None,
60+
pressure: float = None,
61+
ignore_imaginary_modes: bool = False,
62+
eos_type: str = "vinet",
63+
**kwargs,
64+
) -> Flow:
65+
"""Analyze the free energy from all phonon runs.
66+
67+
Parameters
68+
----------
69+
phonon_outputs: list[PhononBSDOSDoc]
70+
list of PhononBSDOSDoc objects
71+
structure: Structure object
72+
Corresponding structure object.
73+
t_max: float
74+
Max temperature for QHA in Kelvin.
75+
pressure: float
76+
Pressure for QHA in GPa.
77+
ignore_imaginary_modes: bool
78+
If True, all free energies will be used
79+
for EOS fit
80+
kwargs: dict
81+
Additional keywords to pass to this job
82+
"""
83+
# only add free energies if there are no imaginary modes
84+
# tolerance has to be tested
85+
electronic_energies: list[list[float]] = []
86+
free_energies: list[list[float]] = []
87+
heat_capacities: list[list[float]] = []
88+
entropies: list[list[float]] = []
89+
temperatures: list[float] = []
90+
formula_units: list[int] = []
91+
volume: list[float] = [
92+
output.volume_per_formula_unit * output.formula_units
93+
for output in phonon_outputs
94+
]
95+
96+
for itemp, temp in enumerate(phonon_outputs[0].temperatures):
97+
temperatures.append(float(temp))
98+
sorted_volume = []
99+
electronic_energies.append([])
100+
free_energies.append([])
101+
heat_capacities.append([])
102+
entropies.append([])
103+
104+
for _, output in sorted(zip(volume, phonon_outputs)):
105+
# check if imaginary modes
106+
if (not output.has_imaginary_modes) or ignore_imaginary_modes:
107+
electronic_energies[itemp].append(output.total_dft_energy)
108+
# convert from J/mol in kJ/mol
109+
free_energies[itemp].append(output.free_energies[itemp] / 1000.0)
110+
heat_capacities[itemp].append(output.heat_capacities[itemp])
111+
entropies[itemp].append(output.entropies[itemp])
112+
sorted_volume.append(output.volume_per_formula_unit)
113+
formula_units.append(output.formula_units)
114+
115+
# potentially implement a space group check in the future
116+
117+
if len(set(formula_units)) != 1:
118+
raise ValueError("There should be only one formula unit.")
119+
120+
return PhononQHADoc.from_phonon_runs(
121+
volumes=sorted_volume,
122+
free_energies=free_energies,
123+
electronic_energies=electronic_energies,
124+
entropies=entropies,
125+
heat_capacities=heat_capacities,
126+
temperatures=temperatures,
127+
structure=structure,
128+
t_max=t_max,
129+
pressure=pressure,
130+
formula_units=next(iter(set(formula_units))),
131+
eos_type=eos_type,
132+
**kwargs,
133+
)

0 commit comments

Comments
 (0)