Skip to content
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

Merged
merged 51 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
195a7cc
add structures to eos output
JaGeo Jun 28, 2024
aa291da
fix defaults in phonon schema
JaGeo Jun 28, 2024
faf7633
add qha flows and one full workflow test
JaGeo Jun 28, 2024
a84161b
really add qha flows
JaGeo Jun 28, 2024
dc74e72
fix description of qha schema file
JaGeo Jun 28, 2024
adab362
remove tests
JaGeo Jun 28, 2024
c6b01a2
add schema test instead
JaGeo Jun 28, 2024
2f641da
new test files
JaGeo Jun 28, 2024
3c059e3
remove future import
JaGeo Jun 28, 2024
34640d5
Fix tests by changing the warning treatment back to default after the…
JaGeo Jun 29, 2024
6480df8
fix linting
JaGeo Jun 29, 2024
580ccab
add switch to eos types
JaGeo Jun 29, 2024
fdfb338
change how warnings are treated
JaGeo Jun 30, 2024
f26fe13
add safer code for literal
JaGeo Jul 1, 2024
1b86ed9
add more tests
JaGeo Jul 1, 2024
71ef7b8
fix tests add kwargs
JaGeo Jul 1, 2024
d7900ae
Fix tests
JaGeo Jul 1, 2024
e21a679
Fix tests
JaGeo Jul 1, 2024
3fe1597
reformat
JaGeo Jul 1, 2024
7145376
linting
JaGeo Jul 1, 2024
a991a6c
update prev calc
JaGeo Jul 1, 2024
80c4a79
Apply suggestions from code review
JaGeo Jul 1, 2024
7c07011
fix test tolerance
JaGeo Jul 2, 2024
0f08b5a
fix test tolerance
JaGeo Jul 2, 2024
89db46f
Merge branch 'main' into qha_fresh
JaGeo Jul 2, 2024
c948542
draft a first vasp workflow
JaGeo Jul 2, 2024
a85ff08
fix return
JaGeo Jul 2, 2024
6fff0e2
fix return
JaGeo Jul 2, 2024
f77bd37
make analysis optional and and names for jobs
JaGeo Jul 3, 2024
f573182
add vasp test
JaGeo Jul 3, 2024
0536047
fix document
JaGeo Jul 10, 2024
2d5f70e
fix dc
JaGeo Jul 10, 2024
d38de42
update
JaGeo Jul 10, 2024
135b4b8
fix phonon tests
JaGeo Jul 10, 2024
6bd1590
fix linting
JaGeo Jul 10, 2024
0c9faff
fix schema
JaGeo Jul 10, 2024
b1ad4f7
Merge branch 'main' into qha_fresh
JaGeo Jul 10, 2024
f2974bd
change to phonon static
JaGeo Jul 24, 2024
e0b4536
change some initialisations
JaGeo Aug 23, 2024
ca24d1c
Merge branch 'main' into qha_fresh
JaGeo Aug 23, 2024
de97216
change phonon maker
JaGeo Aug 23, 2024
ae34913
fix tests
JaGeo Aug 23, 2024
502b7dc
fix test
JaGeo Aug 23, 2024
b8af794
update qha_fresh
JaGeo Aug 23, 2024
8b0417d
fix comments
JaGeo Aug 23, 2024
b58857c
fix comments
JaGeo Aug 23, 2024
eb35145
connect flows
JaGeo Aug 23, 2024
9dd7056
fix linting
JaGeo Aug 23, 2024
f70f7cc
correct documentation
JaGeo Aug 23, 2024
0bd4a37
document workflows, fix imports
JaGeo Aug 23, 2024
9596811
Merge branch 'main' into qha_fresh
JaGeo Sep 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/atomate2/common/flows/eos.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
("relax", "static") if self.static_maker else ("relax",)
)
flow_output: dict[str, dict] = {
key: {quantity: [] for quantity in ("energy", "volume", "stress")}
key: {
quantity: [] for quantity in ("energy", "volume", "stress", "structure")
}
for key in job_types
}

Expand Down Expand Up @@ -166,6 +168,8 @@ def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
flow_output[key]["energy"] += [output.energy]
flow_output[key]["volume"] += [output.structure.volume]
flow_output[key]["stress"] += [output.stress]
# TODO: make this potentially optional?
flow_output[key]["structure"] += [output.structure]

if self.postprocessor is not None:
min_points = self.postprocessor.min_data_points
Expand Down
179 changes: 179 additions & 0 deletions src/atomate2/common/flows/qha.py
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,
)
Comment on lines +114 to +120
Copy link
Member Author

@JaGeo JaGeo Aug 1, 2024

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@utf ? any opinions?

Copy link
Member

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:

starting solely from the EosMaker for the qha workflow would take too much time

Either way, I'm open to both approaches:

  • Adding the EosMaker as an argument would reduce the number of options in the 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.
  • The current internal use of CommonEosMaker makes it easier to configure settings but is slightly less clean code. By the way, I noticed the linear_strain option is not currently used.

I'll let you decide!

Copy link
Member Author

@JaGeo JaGeo Aug 23, 2024

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.)

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)
"""
122 changes: 122 additions & 0 deletions src/atomate2/common/jobs/qha.py
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)
Copy link
Member

Choose a reason for hiding this comment

The 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 auto_ispin and make the calculations cheaper.

Copy link
Member Author

Choose a reason for hiding this comment

The 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,
)
Loading
Loading