"""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.phonons import PhononBSDOSDoc
from atomate2.common.schemas.qha import PhononQHADoc
from atomate2.common.utils import get_supercell_matrix
if TYPE_CHECKING:
from pymatgen.core.structure import Structure
from atomate2.common.flows.phonons import BasePhononMaker
logger = logging.getLogger(__name__)
[docs]
@job
def get_supercell_size(
eos_output: dict,
min_length: float,
max_length: float,
prefer_90_degrees: bool,
allow_orthorhombic: bool = False,
**kwargs,
) -> list[list[float]]:
"""
Job to get the supercell size from an eos output.
Parameters
----------
eos_output: dict
output from eos state job
min_length: float
minimum length of cell in Angstrom
max_length: float
maximum length of cell in Angstrom
prefer_90_degrees: bool
if True, the algorithm will try to find a cell with 90 degree angles first
allow_orthorhombic: bool
if True, orthorhombic supercells are allowed
**kwargs:
Additional parameters that can be set.
"""
return get_supercell_matrix(
eos_output["relax"]["structure"][0],
min_length,
max_length,
prefer_90_degrees,
allow_orthorhombic,
**kwargs,
)
[docs]
@job(data=[PhononBSDOSDoc])
def get_phonon_jobs(
phonon_maker: BasePhononMaker, eos_output: dict, supercell_matrix: list[list[float]]
) -> Flow:
"""
Start all relevant phonon jobs.
Parameters
----------
phonon_maker: .BasePhononMaker
Maker to start harmonic phonon runs.
eos_output: dict
Output from EOSMaker
supercell_matrix:
Supercell matrix to be passed into the phonon runs.
"""
phonon_jobs = []
outputs = []
for istructure, structure in enumerate(eos_output["relax"]["structure"]):
if eos_output["relax"]["dir_name"][istructure] is not None:
phonon_job = phonon_maker.make(
structure,
prev_dir=eos_output["relax"]["dir_name"][istructure],
supercell_matrix=supercell_matrix,
)
else:
phonon_job = phonon_maker.make(structure, supercell_matrix=supercell_matrix)
phonon_job.append_name(f" eos deformation {istructure + 1}")
phonon_jobs.append(phonon_job)
outputs.append(phonon_job.output)
replace_flow = Flow(phonon_jobs, outputs)
return Response(replace=replace_flow)
[docs]
@job(
output_schema=PhononQHADoc,
data=["free_energies", "heat_capacities", "entropies", "helmholtz_volume"],
)
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
]
supercell_matrix: list[list[float]] = phonon_outputs[0].supercell_matrix
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, strict=True)):
# 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)
# potentially implement a space group check in the future
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,
supercell_matrix=supercell_matrix,
**kwargs,
)