-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Workflow for Quasi-harmonic approximation (forcefields and VASP) #903
Changes from 21 commits
195a7cc
aa291da
faf7633
a84161b
dc74e72
adab362
c6b01a2
2f641da
3c059e3
34640d5
6480df8
580ccab
fdfb338
f26fe13
1b86ed9
71ef7b8
d7900ae
e21a679
3fe1597
7145376
a991a6c
80c4a79
7c07011
0f08b5a
89db46f
c948542
a85ff08
6fff0e2
f77bd37
f573182
0536047
2d5f70e
d38de42
135b4b8
6bd1590
0c9faff
b1ad4f7
f2974bd
e0b4536
ca24d1c
de97216
ae34913
502b7dc
b8af794
8b0417d
b58857c
eb35145
9dd7056
f70f7cc
0bd4a37
9596811
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
"""Define common QHA flow agnostic to electronic-structure code.""" | ||
|
||
from __future__ import annotations | ||
|
||
from abc import ABC, abstractmethod | ||
from dataclasses import dataclass, field | ||
from typing import TYPE_CHECKING, Literal | ||
|
||
from jobflow import Flow, Maker | ||
|
||
from atomate2.common.flows.eos import CommonEosMaker | ||
from atomate2.common.jobs.qha import analyze_free_energy, get_phonon_jobs | ||
|
||
if TYPE_CHECKING: | ||
from pathlib import Path | ||
|
||
from pymatgen.core import Structure | ||
|
||
from atomate2.common.flows.phonons import BasePhononMaker | ||
from atomate2.forcefields.jobs import ForceFieldRelaxMaker, ForceFieldStaticMaker | ||
from atomate2.vasp.jobs.base import BaseVaspMaker | ||
|
||
supported_eos = frozenset(("vinet", "birch_murnaghan", "murnaghan")) | ||
|
||
|
||
@dataclass | ||
class CommonQhaMaker(Maker, ABC): | ||
""" | ||
Use the quasi-harmonic approximation. | ||
|
||
First relax a structure. | ||
Then we scale the relaxed structure, and | ||
then compute harmonic phonons for each scaled | ||
structure with Phonopy. | ||
Finally, we compute the Gibb's free energy and | ||
other thermodynamic properties available from | ||
the quasi-harmonic approximation. | ||
|
||
Note: We do not consider electronic free energies so far. | ||
This might be problematic for metals (see e.g., | ||
Wolverton and Zunger, Phys. Rev. B, 52, 8813 (1994).) | ||
|
||
Note: Magnetic Materials have never been computed with | ||
this workflow. | ||
|
||
Parameters | ||
---------- | ||
name: str | ||
Name of the flows produced by this maker. | ||
initial_relax_maker: .ForceFieldRelaxMaker | .BaseVaspMaker | None | ||
Maker to relax the input structure. | ||
eos_relax_maker: .ForceFieldRelaxMaker | .BaseVaspMaker | None | ||
Maker to relax deformed structures for the EOS fit. | ||
The volume has to be fixed! | ||
phonon_displacement_maker: .ForceFieldStaticMaker | .BaseVaspMaker | None | ||
phonon_static_maker: .ForceFieldStaticMaker | .BaseVaspMaker | None | ||
phonon_maker_kwargs: dict | ||
linear_strain: tuple[float, float] | ||
Percentage linear strain to apply as a deformation, default = -5% to 5%. | ||
number_of_frames: int | ||
Number of strain calculations to do for EOS fit, default = 6. | ||
t_max: float | None | ||
Maximum temperature until which the QHA will be performed | ||
pressure: float | None | ||
Pressure at which the QHA will be performed (default None, no pressure) | ||
ignore_imaginary_modes: bool | ||
By default, volumes where the harmonic phonon approximation shows imaginary | ||
will be ignored | ||
eos_type: str | ||
Equation of State type used for the fitting. Defaults to vinet. | ||
""" | ||
|
||
name: str = "QHA Maker" | ||
initial_relax_maker: ForceFieldRelaxMaker | BaseVaspMaker | None = None | ||
eos_relax_maker: ForceFieldRelaxMaker | BaseVaspMaker | None = None | ||
phonon_displacement_maker: ForceFieldStaticMaker | BaseVaspMaker | None = None | ||
phonon_static_maker: ForceFieldStaticMaker | BaseVaspMaker | None = None | ||
phonon_maker_kwargs: dict = field(default_factory=dict) | ||
linear_strain: tuple[float, float] = (-0.05, 0.05) | ||
number_of_frames: int = 6 | ||
t_max: float | None = None | ||
pressure: float | None = None | ||
ignore_imaginary_modes: bool = False | ||
eos_type: Literal["vinet", "birch_murnaghan", "murnaghan"] = "vinet" | ||
analyze_free_energy_kwargs: dict = field(default_factory=dict) | ||
# TODO: implement advanced handling of | ||
# imaginary modes in phonon runs (i.e., fitting procedures) | ||
|
||
def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow: | ||
"""Run an EOS flow. | ||
|
||
Parameters | ||
---------- | ||
structure : Structure | ||
A pymatgen structure object. | ||
prev_dir : str or Path or None | ||
A previous calculation directory to copy output files from. | ||
|
||
Returns | ||
------- | ||
.Flow, a QHA flow | ||
""" | ||
if self.eos_type not in supported_eos: | ||
raise ValueError( | ||
"EOS not supported.", | ||
"Please choose 'vinet', 'birch_murnaghan', 'murnaghan'", | ||
) | ||
|
||
# In this way, one can easily exchange makers and enforce postprocessor None | ||
self.eos = CommonEosMaker( | ||
initial_relax_maker=self.initial_relax_maker, | ||
eos_relax_maker=self.eos_relax_maker, | ||
static_maker=None, | ||
postprocessor=None, | ||
number_of_frames=self.number_of_frames, | ||
) | ||
self.phonon_maker = self.initialize_phonon_maker( | ||
phonon_displacement_maker=self.phonon_displacement_maker, | ||
phonon_static_maker=self.phonon_static_maker, | ||
bulk_relax_maker=None, | ||
phonon_maker_kwargs=self.phonon_maker_kwargs, | ||
) | ||
eos_job = self.eos.make(structure) | ||
phonon_jobs = get_phonon_jobs( | ||
phonon_maker=self.phonon_maker, eos_output=eos_job.output | ||
) | ||
analysis = analyze_free_energy( | ||
phonon_jobs.output, | ||
structure=structure, | ||
t_max=self.t_max, | ||
pressure=self.pressure, | ||
ignore_imaginary_modes=self.ignore_imaginary_modes, | ||
eos_type=self.eos_type, | ||
**self.analyze_free_energy_kwargs, | ||
) | ||
|
||
return Flow([eos_job, phonon_jobs, analysis]) | ||
|
||
@abstractmethod | ||
def initialize_phonon_maker( | ||
self, | ||
phonon_displacement_maker: ForceFieldStaticMaker | BaseVaspMaker | None, | ||
phonon_static_maker: ForceFieldStaticMaker | BaseVaspMaker | None, | ||
bulk_relax_maker: ForceFieldRelaxMaker | BaseVaspMaker | None, | ||
phonon_maker_kwargs: dict, | ||
) -> BasePhononMaker | None: | ||
"""Initialize phonon maker. | ||
|
||
This implementation will be different for | ||
any newly implemented QHAMaker. | ||
|
||
Parameters | ||
---------- | ||
phonon_displacement_maker: ForceFieldStaticMaker|BaseVaspMaker|None | ||
Maker for displacement calculations. | ||
phonon_static_maker: ForceFieldStaticMaker|BaseVaspMaker|None | ||
Maker for additional static calculations. | ||
bulk_relax_maker: : ForceFieldRelaxMaker|BaseVaspMaker|None | ||
Maker for optimization. Here: None. | ||
phonon_maker_kwargs: dict | ||
Additional keyword arguments for phonon maker. | ||
|
||
Returns | ||
------- | ||
.BasePhononMaker | ||
|
||
""" | ||
|
||
@property | ||
@abstractmethod | ||
def prev_calc_dir_argname(self) -> str | None: | ||
"""Name of argument informing static maker of previous calculation directory. | ||
|
||
As this differs between different DFT codes (e.g., VASP, CP2K), it | ||
has been left as a property to be implemented by the inheriting class. | ||
|
||
Note: this is only applicable if a relax_maker is specified; i.e., two | ||
calculations are performed for each ordering (relax -> static) | ||
""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
"""Jobs for running qha calculations.""" | ||
|
||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import TYPE_CHECKING | ||
|
||
from jobflow import Flow, Response, job | ||
|
||
from atomate2.common.schemas.qha import PhononQHADoc | ||
|
||
if TYPE_CHECKING: | ||
from pymatgen.core.structure import Structure | ||
|
||
from atomate2.common.flows.phonons import BasePhononMaker | ||
from atomate2.common.schemas.phonons import PhononBSDOSDoc | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@job | ||
def get_phonon_jobs(phonon_maker: BasePhononMaker, eos_output: dict) -> Flow: | ||
""" | ||
Start all relevant phonon jobs. | ||
|
||
Parameters | ||
---------- | ||
phonon_maker: .BasePhononMaker | ||
Maker to start harmonic phonon runs. | ||
eos_output: dict | ||
Output from EOSMaker | ||
|
||
""" | ||
phonon_jobs = [] | ||
outputs = [] | ||
for structure in eos_output["relax"]["structure"]: | ||
phonon_job = phonon_maker.make(structure) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, it would be nice to pass the prev_dir from the relaxation? That way we can make use of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did this now by extending the EOS dictionaries. |
||
phonon_jobs.append(phonon_job) | ||
outputs.append(phonon_job.output) | ||
|
||
return Response(replace=phonon_jobs, output=outputs) | ||
|
||
|
||
@job( | ||
output_schema=PhononQHADoc, | ||
) | ||
def analyze_free_energy( | ||
phonon_outputs: list[PhononBSDOSDoc], | ||
structure: Structure, | ||
t_max: float = None, | ||
pressure: float = None, | ||
ignore_imaginary_modes: bool = False, | ||
eos_type: str = "vinet", | ||
**kwargs, | ||
) -> Flow: | ||
"""Analyze the free energy from all phonon runs. | ||
|
||
Parameters | ||
---------- | ||
phonon_outputs: list[PhononBSDOSDoc] | ||
list of PhononBSDOSDoc objects | ||
structure: Structure object | ||
Corresponding structure object. | ||
t_max: float | ||
Max temperature for QHA in Kelvin. | ||
pressure: float | ||
Pressure for QHA in GPa. | ||
ignore_imaginary_modes: bool | ||
If True, all free energies will be used | ||
for EOS fit | ||
kwargs: dict | ||
Additional keywords to pass to this job | ||
""" | ||
# only add free energies if there are no imaginary modes | ||
# tolerance has to be tested | ||
electronic_energies: list[list[float]] = [] | ||
free_energies: list[list[float]] = [] | ||
heat_capacities: list[list[float]] = [] | ||
entropies: list[list[float]] = [] | ||
temperatures: list[float] = [] | ||
formula_units: list[int] = [] | ||
volume: list[float] = [ | ||
output.volume_per_formula_unit * output.formula_units | ||
for output in phonon_outputs | ||
] | ||
|
||
for itemp, temp in enumerate(phonon_outputs[0].temperatures): | ||
temperatures.append(float(temp)) | ||
sorted_volume = [] | ||
electronic_energies.append([]) | ||
free_energies.append([]) | ||
heat_capacities.append([]) | ||
entropies.append([]) | ||
|
||
for _, output in sorted(zip(volume, phonon_outputs)): | ||
# check if imaginary modes | ||
if (not output.has_imaginary_modes) or ignore_imaginary_modes: | ||
electronic_energies[itemp].append(output.total_dft_energy) | ||
# convert from J/mol in kJ/mol | ||
free_energies[itemp].append(output.free_energies[itemp] / 1000.0) | ||
heat_capacities[itemp].append(output.heat_capacities[itemp]) | ||
entropies[itemp].append(output.entropies[itemp]) | ||
sorted_volume.append(output.volume_per_formula_unit) | ||
formula_units.append(output.formula_units) | ||
|
||
if len(set(formula_units)) != 1: | ||
raise ValueError("There should be only one formula unit.") | ||
|
||
return PhononQHADoc.from_phonon_runs( | ||
volumes=sorted_volume, | ||
free_energies=free_energies, | ||
electronic_energies=electronic_energies, | ||
entropies=entropies, | ||
heat_capacities=heat_capacities, | ||
temperatures=temperatures, | ||
structure=structure, | ||
t_max=t_max, | ||
pressure=pressure, | ||
formula_units=next(iter(set(formula_units))), | ||
eos_type=eos_type, | ||
**kwargs, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@utf I am currently wondering about how to initalize and reuse the EosMaker in the best way. Unfortunately, starting solely from the EosMaker for the qha workflow would take too much time. This is why I only partly reuse and extend it here. Should I do it similarly to the Grüneisen workflow and add the EosMaker as an argument for the overall qhamaker?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@utf ? any opinions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused - what do you mean by:
Either way, I'm open to both approaches:
CommonQhaMaker
class but make it more difficult to set properties such as the number of frames. I don't think this is too much of an issue if we could include an example of how to configure the settings in the docstring/documentation page.linear_strain
option is not currently used.I'll let you decide!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I think I will go with 2.
What I meant: instead of relying on phonopy to deal with the equation of state fitting, to reuse the fitting options from the
EosStateMaker.
(EDIT: there was another comment here. I mixed something up.)