Source code for atomate2.vasp.jobs.ferroelectric
"""Job used in the Ferroelectric wflow."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from jobflow import Flow, Job, Response, job
from pymatgen.analysis.ferroelectricity.polarization import get_total_ionic_dipole
from atomate2.vasp.schemas.ferroelectric import PolarizationDocument
if TYPE_CHECKING:
from pymatgen.core.structure import Structure
from atomate2.vasp.jobs.base import BaseVaspMaker
logger = logging.getLogger(__name__)
__all__ = ["polarization_analysis"]
[docs]
@job(output_schema=PolarizationDocument)
def polarization_analysis(
np_lcalcpol_output: dict[str, Any],
p_lcalcpol_output: dict[str, Any],
interp_lcalcpol_outputs: dict[str, Any],
) -> PolarizationDocument:
"""
Recover the same branch polarization and the spontaneous polarization.
Parameters
----------
np_lcalcpol_output : dict
Output from previous nonpolar lcalcpol job.
p_lcalcpol_output : dict
Output from previous polar lcalcpol job.
interp_lcalcpol_outputs : dict
Output from previous interpolation lcalcpol jobs.
Returns
-------
PolarizationDocument
Document containing the polarization analysis results.
"""
# order previous calculations from nonpolar to polar
ordered_keys = [
f"interpolation_{i}" for i in reversed(range(len(interp_lcalcpol_outputs)))
]
polarization_tasks = [np_lcalcpol_output]
polarization_tasks += [interp_lcalcpol_outputs[k] for k in ordered_keys]
polarization_tasks += [p_lcalcpol_output]
task_lbls = []
structures = []
energies_per_atom = []
energies = []
job_dirs = []
uuids = []
for i, p in enumerate(polarization_tasks):
energies_per_atom.append(p["energy_per_atom"])
energies.append(p["energy"])
task_lbls.append(p["task_label"] or str(i))
structures.append(p["structure"])
job_dirs.append(p["job_dir"])
uuids.append(p["uuid"])
# If LCALCPOL = True then Outcar will parse and store the pseudopotential zvals.
zval_dict = p["zval_dict"]
# Assumes that we want to calculate the ionic contribution to the dipole moment.
# VASP's ionic contribution is sometimes strange.
# See pymatgen.analysis.ferroelectricity.polarization.Polarization for details.
p_elecs = [p["p_elecs"] for p in polarization_tasks]
p_ions = [get_total_ionic_dipole(st, zval_dict) for st in structures]
return PolarizationDocument.from_pol_output(
p_elecs,
p_ions,
structures,
energies,
energies_per_atom,
zval_dict,
task_lbls,
job_dirs,
uuids,
)
[docs]
@job
def interpolate_structures(p_st: Structure, np_st: Structure, nimages: int) -> list:
"""
Interpolate linearly the polar and the nonpolar structures with nimages structures.
Parameters
----------
p_st : Structure
A pymatgen structure of polar phase.
np_st : Structure
A pymatgen structure of nonpolar phase.
nimages : int
Number of interpolatated structures calculated
from polar to nonpolar structures.
Returns
-------
List of interpolated structures
"""
# adding +1 to nimages to match convention used in the interpolate
# func where nonpolar is (weirdly) included in the nimages count
return p_st.interpolate(
np_st, nimages + 1, interpolate_lattices=True, autosort_tol=0.0
)
[docs]
@job
def add_interpolation_flow(
interp_structures: list[Structure], lcalcpol_maker: BaseVaspMaker
) -> Response:
"""
Generate the interpolations jobs and add them to the main ferroelectric flow.
Parameters
----------
interp_structures: List[Structure]
List of interpolated structures
lcalcpol_maker : BaseVaspMaker
Vasp maker to compute the polarization of each structure.
Returns
-------
Response
Job response containing the interpolation flow.
"""
jobs = []
outputs = {}
for i, interp_structure in enumerate(interp_structures[1:-1]):
lcalcpol_maker.write_additional_data["structures:json"] = {
"st_polar": interp_structures[0],
"st_nonpolar": interp_structures[-1],
"st_interp_idx": i + 1,
}
interpolation = lcalcpol_maker.make(interp_structure)
interpolation.append_name(f" interpolation_{i}")
jobs.append(interpolation)
output = get_polarization_output(interpolation)
outputs.update({f"interpolation_{i}": output})
interp_flow = Flow(jobs, outputs)
return Response(replace=interp_flow)
[docs]
def get_polarization_output(job: Job) -> dict:
"""
Extract from lcalcpol job all the relevant output to compute the polarization.
Parameters
----------
job : Job
Job from which to extract relevant quantities.
Returns
-------
dict
Dictionary containing the extracted polarization data.
"""
p = job.output
outcar = p.calcs_reversed[0].output.outcar
return {
"energy_per_atom": p.calcs_reversed[0].output.energy_per_atom,
"energy": p.calcs_reversed[0].output.energy,
"task_label": p.task_label,
"structure": p.structure,
"zval_dict": outcar["zval_dict"],
"p_elecs": outcar["p_elec"],
"job_dir": p.dir_name,
"uuid": p.uuid,
}