"""Module defining core VASP input set generators."""
from __future__ import annotations
import logging
from copy import deepcopy
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
import numpy as np
from pymatgen.core.periodic_table import Element
from pymatgen.io.vasp.sets import LobsterSet
from atomate2.vasp.sets.base import VaspInputGenerator
if TYPE_CHECKING:
from emmet.core.math import Vector3D
from pymatgen.core import Structure
from pymatgen.io.vasp import Kpoints, Outcar, Vasprun
logger = logging.getLogger(__name__)
[docs]
@dataclass
class RelaxSetGenerator(VaspInputGenerator):
"""Class to generate VASP relaxation input sets."""
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a relaxation job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {"NSW": 99, "LCHARG": False, "ISIF": 3, "IBRION": 2}
[docs]
class RelaxConstVolSetGenerator(VaspInputGenerator):
"""Class to generate VASP constant volume relaxation input sets."""
[docs]
def get_incar_updates(
self,
structure: Structure,
prev_incar: dict = None,
bandgap: float = None,
vasprun: Vasprun = None,
outcar: Outcar = None,
) -> dict:
"""Get updates to the INCAR for a constant volume relaxation job.
Parameters
----------
structure
A structure.
prev_incar
An incar from a previous calculation.
bandgap
The band gap.
vasprun
A vasprun from a previous calculation.
outcar
An outcar from a previous calculation.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {"NSW": 99, "LCHARG": False, "ISIF": 2, "IBRION": 2}
[docs]
@dataclass
class TightRelaxSetGenerator(VaspInputGenerator):
"""Class to generate tight VASP relaxation input sets."""
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a tight relaxation job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {
"IBRION": 2,
"ISIF": 3,
"ENCUT": 700,
"EDIFF": 1e-7,
"LAECHG": False,
"EDIFFG": -0.001,
"LREAL": False,
"NSW": 99,
"LCHARG": False,
}
[docs]
@dataclass
class TightRelaxConstVolSetGenerator(VaspInputGenerator):
"""Class to generate constant volume tight VASP relaxation input sets."""
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a static VASP job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {
"IBRION": 2,
"ISIF": 2,
"ENCUT": 700,
"EDIFF": 1e-7,
"LAECHG": False,
"EDIFFG": -0.001,
"LREAL": False,
"NSW": 99,
"LCHARG": False,
}
[docs]
@dataclass
class StaticSetGenerator(VaspInputGenerator):
"""
Class to generate VASP static input sets.
Parameters
----------
lepsilon
Whether to set LEPSILON (used for calculating the high-frequency dielectric
tensor).
lcalcpol
Whether to set LCALCPOL (used for calculating the electronic contribution to
the polarization)
**kwargs
Other keyword arguments that will be passed to :obj:`VaspInputGenerator`.
"""
lepsilon: bool = False
lcalcpol: bool = False
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a static VASP job.
Returns
-------
dict
A dictionary of updates to apply.
"""
updates = {"NSW": 0, "ISMEAR": -5, "LCHARG": True, "LORBIT": 11, "LREAL": False}
if self.lepsilon:
# LPEAD=T: numerical evaluation of overlap integral prevents LRF_COMMUTATOR
# errors and can lead to better expt. agreement but produces slightly
# different results
updates.update(IBRION=8, LEPSILON=True, LPEAD=True, NSW=1)
if self.lcalcpol:
updates["LCALCPOL"] = True
return updates
[docs]
@dataclass
class NonSCFSetGenerator(VaspInputGenerator):
"""
Class to generate VASP non-self-consistent field input sets.
Parameters
----------
mode
Type of band structure mode. Options are "line", "uniform", or "boltztrap".
dedos
Energy difference used to set NEDOS, based on the total energy range.
reciprocal_density
Density of k-mesh by reciprocal volume.
reciprocal_density_metal
Density of k-mesh by reciprocal volume for use when the system is metallic
and ``auto_metal_kpoints=True`` (the default).
line_density
Line density for line mode band structure.
optics
Whether to add LOPTICS (used for calculating optical response).
nbands_factor
Multiplicative factor for NBANDS when starting from a previous calculation.
Choose a higher number if you are doing an LOPTICS calculation.
**kwargs
Other keyword arguments that will be passed to :obj:`VaspInputGenerator`.
"""
mode: str = "line"
dedos: float = 0.02
reciprocal_density: float = 100
reciprocal_density_metal: float = 400
line_density: float = 20
optics: bool = False
nbands_factor: float = 1.2
auto_ispin: bool = True
def __post_init__(self) -> None:
"""Ensure mode is set correctly."""
super().__post_init__()
self.mode = self.mode.lower()
supported_modes = ("line", "uniform", "boltztrap")
if self.mode not in supported_modes:
raise ValueError(f"Supported modes are: {', '.join(supported_modes)}")
@property
def kpoints_updates(self) -> dict | Kpoints:
"""Get updates to the kpoints configuration for a non-self consistent VASP job.
Note, these updates will be ignored if the user has set user_kpoint_settings.
Returns
-------
dict
A dictionary of updates to apply to the KPOINTS config.
"""
if self.mode == "line":
return {"line_density": self.line_density}
if self.mode == "boltztrap":
return {"explicit": True, "reciprocal_density": self.reciprocal_density}
return {
"reciprocal_density": self.reciprocal_density,
"reciprocal_density_metal": self.reciprocal_density_metal,
}
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a non-self-consistent field VASP job.
Returns
-------
dict
A dictionary of updates to apply.
"""
updates: dict[str, Any] = {
"LCHARG": False,
"LORBIT": 11,
"LWAVE": False,
"NSW": 0,
"ISYM": 0,
"ICHARG": 11,
"KSPACING": None,
}
if self.prev_vasprun is not None:
# set NBANDS
n_bands = (
self.prev_vasprun.parameters.get("NBANDS") or self.estimate_nbands()
)
updates["NBANDS"] = int(np.ceil(n_bands * self.nbands_factor))
if self.mode == "uniform":
# automatic setting of NEDOS using the energy range and the energy step
n_edos = self._get_nedos(self.dedos)
# use tetrahedron method for DOS and optics calculations
updates.update(ISMEAR=-5, ISYM=2, NEDOS=n_edos)
elif self.mode in ("line", "boltztrap"):
# if line mode or explicit k-points (boltztrap) can't use ISMEAR=-5
# use small sigma to avoid partial occupancies for small band gap materials
# use a larger sigma if the material is a metal
sigma = 0.2 if self.bandgap == 0 else 0.01
updates.update({"ISMEAR": 0, "SIGMA": sigma})
if self.optics:
# LREAL not supported with LOPTICS = True; automatic NEDOS usually
# underestimates, so set it explicitly
updates.update(LOPTICS=True, LREAL=False, CSHIFT=1e-5, NEDOS=2000)
updates["MAGMOM"] = None
return updates
[docs]
@dataclass
class HSERelaxSetGenerator(VaspInputGenerator):
"""Class to generate VASP HSE06 relaxation input sets.
.. note::
By default the hybrid input sets use ALGO = Normal which is only efficient for
VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/LFOCKACE for more
details.
"""
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a VASP HSE06 relaxation job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {
"NSW": 99,
"ALGO": "Normal",
"GGA": "PE",
"HFSCREEN": 0.2,
"LHFCALC": True,
"IBRION": 2,
"PRECFOCK": "Fast",
"ISIF": 3,
"LASPH": True,
"LDAU": False,
}
[docs]
@dataclass
class HSETightRelaxSetGenerator(VaspInputGenerator):
"""Class to generate tight VASP HSE relaxation input sets.
.. note::
By default the hybrid input sets use ALGO = Normal which is only efficient for
VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/LFOCKACE for more
details.
"""
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for an HSE tight relaxation job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {
"IBRION": 2,
"ISIF": 3,
"ENCUT": 700,
"EDIFF": 1e-7,
"LAECHG": False,
"EDIFFG": -0.001,
"LREAL": False,
"ALGO": "Normal",
"NSW": 99,
"LCHARG": False,
"GGA": "PE",
"HFSCREEN": 0.2,
"LHFCALC": True,
"PRECFOCK": "Fast",
"LASPH": True,
"LDAU": False,
}
[docs]
@dataclass
class HSEStaticSetGenerator(VaspInputGenerator):
"""Class to generate VASP HSE06 static input sets.
.. note::
By default the hybrid input sets use ALGO = Normal which is only efficient for
VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/LFOCKACE for more
details.
"""
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a VASP HSE06 static job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {
"NSW": 0,
"ALGO": "Normal",
"GGA": "PE",
"HFSCREEN": 0.2,
"LHFCALC": True,
"PRECFOCK": "Fast",
"ISMEAR": -5,
"LORBIT": 11,
"LCHARG": True,
"LASPH": True,
"LREAL": False,
"LDAU": False,
}
[docs]
@dataclass
class HSEBSSetGenerator(VaspInputGenerator):
"""
Class to generate VASP HSE06 band structure input sets.
HSE06 band structures must be self-consistent. A band structure along symmetry lines
for instance needs BOTH a uniform grid with appropriate weights AND a path along the
lines with weight 0.
Thus, the "uniform" mode is just like regular static SCF but allows adding custom
kpoints (e.g., corresponding to known VBM/CBM) to the uniform grid that have zero
weight (e.g., for better gap estimate).
The "gap" mode behaves just like the "uniform" mode, however, if starting from a
previous calculation, the VBM and CBM k-points will automatically be added to
``added_kpoints``.
The "line" mode is just like Uniform mode, but additionally adds k-points along
symmetry lines with zero weight.
The "uniform_dense" mode employs are regular weighted k-point mesh, in addition
to a zero-weighted uniform mesh with higher density.
.. note::
By default the hybrid input sets use ALGO = Normal which is only efficient for
VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/LFOCKACE for more
details.
Parameters
----------
mode
Type of band structure mode. Options are "line", "uniform", "gap", or
"uniform_dense".
dedos
Energy difference used to set NEDOS, based on the total energy range.
reciprocal_density
Density of k-mesh by reciprocal volume.
line_density
Line density for line mode band structure.
zero_weighted_reciprocal_density
Density of uniform zero weighted k-point mesh.
optics
Whether to add LOPTICS (used for calculating optical response).
nbands_factor
Multiplicative factor for NBANDS when starting from a previous calculation.
Choose a higher number if you are doing an LOPTICS calculation.
added_kpoints
A list of kpoints in fractional coordinates to add as zero-weighted points.
**kwargs
Other keyword arguments that will be passed to :obj:`VaspInputGenerator`.
"""
mode: str = "gap"
dedos: float = 0.02
reciprocal_density: float = 64
line_density: float = 20
zero_weighted_reciprocal_density: float = 100
optics: bool = False
nbands_factor: float = 1.2
added_kpoints: list[Vector3D] = field(default_factory=list)
auto_ispin: bool = True
def __post_init__(self) -> None:
"""Ensure mode is set correctly."""
super().__post_init__()
self.mode = self.mode.lower()
supported_modes = ("line", "uniform", "gap", "uniform_dense")
if self.mode not in supported_modes:
raise ValueError(f"Supported modes are: {', '.join(supported_modes)}")
@property
def kpoints_updates(self) -> dict | Kpoints:
"""Get updates to the kpoints configuration for a VASP HSE06 band structure job.
Note, these updates will be ignored if the user has set user_kpoint_settings.
Returns
-------
dict
A dictionary of updates to apply to the KPOINTS config.
"""
kpoints: dict[str, Any] = {"reciprocal_density": self.reciprocal_density}
if self.mode == "line":
# add line_density on top of reciprocal density
kpoints["zero_weighted_line_density"] = self.line_density
elif self.mode == "uniform_dense":
kpoints["zero_weighted_reciprocal_density"] = (
self.zero_weighted_reciprocal_density
)
added_kpoints = deepcopy(self.added_kpoints)
if self.prev_vasprun is not None and self.mode == "gap":
bs = self.prev_vasprun.get_band_structure()
if not bs.is_metal():
added_kpoints.append(bs.get_vbm()["kpoint"].frac_coords)
added_kpoints.append(bs.get_cbm()["kpoint"].frac_coords)
kpoints["added_kpoints"] = added_kpoints
return kpoints
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a VASP HSE06 band structure job.
Returns
-------
dict
A dictionary of updates to apply.
"""
updates = {
"NSW": 0,
"ALGO": "Normal",
"GGA": "PE",
"HFSCREEN": 0.2,
"PRECFOCK": "Fast",
"LHFCALC": True,
"LCHARG": False,
"NELMIN": 5,
"KSPACING": None,
"LORBIT": 11,
"LREAL": False,
"LDAU": False,
}
if self.mode == "uniform" and len(self.added_kpoints) == 0:
# automatic setting of nedos using the energy range and the energy step
nedos = self._get_nedos(self.dedos)
# use tetrahedron method for DOS and optics calculations
updates.update(ISMEAR=-5, NEDOS=nedos)
else:
# if line mode or explicit k-points (gap) can't use ISMEAR=-5
# use small sigma to avoid partial occupancies for small band gap materials
updates.update(ISMEAR=0, SIGMA=0.01)
if self.prev_vasprun is not None:
# set nbands
n_bands = self.prev_vasprun.parameters["NBANDS"] or self.estimate_nbands()
updates["NBANDS"] = int(np.ceil(n_bands * self.nbands_factor))
if self.optics:
# LREAL not supported with LOPTICS
updates.update(LOPTICS=True, LREAL=False, CSHIFT=1e-5)
updates["MAGMOM"] = None
return updates
[docs]
@dataclass
class ElectronPhononSetGenerator(VaspInputGenerator):
"""
Class to generate VASP electron phonon input sets.
.. note::
Requires VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/Electron-
phonon_interactions_from_Monte-Carlo_sampling for more details.
Parameters
----------
temperatures : list of float
The temperatures for which the electron-phonon interactions are evaluated.
reciprocal_density
Density of k-mesh by reciprocal volume.
"""
temperatures: tuple[float, ...] = (
0,
100,
200,
300,
400,
500,
600,
700,
800,
900,
1000,
)
reciprocal_density: float = 64
auto_ispin: bool = True
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a static VASP job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return {
"NSW": 1,
"ISMEAR": 0,
"IBRION": 6,
"ISIF": 2,
"ENCUT": 700,
"EDIFF": 1e-7,
"LAECHG": False,
"LREAL": False,
"LCHARG": False,
"LVTOT": False,
"LVHAR": False,
"PREC": "Accurate",
"KSPACING": None,
"PHON_NTLIST": len(self.temperatures),
"PHON_TLIST": list(self.temperatures), # has to be a list otherwise error
"PHON_NSTRUCT": 0,
"PHON_LMC": True,
}
@property
def kpoints_updates(self) -> dict | Kpoints:
"""Get updates to the kpoints configuration for a non-self consistent VASP job.
Note, these updates will be ignored if the user has set user_kpoint_settings.
Returns
-------
dict
A dictionary of updates to apply to the KPOINTS config.
"""
return {"reciprocal_density": self.reciprocal_density}
[docs]
@dataclass
class MDSetGenerator(VaspInputGenerator):
"""
Class to generate VASP molecular dynamics input sets.
Parameters
----------
ensemble
Molecular dynamics ensemble to run. Options include `nvt`, `nve`, and `npt`.
start_temp
Starting temperature. The VASP `TEBEG` parameter.
end_temp
Final temperature. The VASP `TEEND` parameter.
nsteps
Number of time steps for simulations. The VASP `NSW` parameter.
time_step
The time step (in femtosecond) for the simulation. The VASP `POTIM` parameter.
**kwargs
Other keyword arguments that will be passed to :obj:`VaspInputGenerator`.
"""
ensemble: str = "nvt"
start_temp: float = 300
end_temp: float = 300
nsteps: int = 1000
time_step: float = 2
auto_ispin: bool = True
@property
def incar_updates(self) -> dict:
"""Get updates to the INCAR for a molecular dynamics job.
Returns
-------
dict
A dictionary of updates to apply.
"""
updates = self._get_ensemble_defaults(self.structure, self.ensemble)
# Based on pymatgen.io.vasp.sets.MPMDSet.
updates.update(
ENCUT=520,
TEBEG=self.start_temp,
TEEND=self.end_temp,
NSW=self.nsteps,
POTIM=self.time_step,
LCHARG=False,
NELMIN=4,
MAXMIX=20,
NELM=500,
ISYM=0,
IBRION=0,
KBLOCK=100,
PREC="Normal",
)
if Element("H") in self.structure.species and updates["POTIM"] > 0.5:
logger.warning(
f"Molecular dynamics time step is {updates['POTIM']}, which is "
"typically too large for a structure containing H. Consider setting it "
"to a value of 0.5 or smaller."
)
return updates
@staticmethod
def _get_ensemble_defaults(structure: Structure, ensemble: str) -> dict[str, Any]:
"""Get default params for the ensemble."""
defaults = {
"nve": {"MDALGO": 1, "ISIF": 2, "ANDERSEN_PROB": 0.0},
"nvt": {"MDALGO": 2, "ISIF": 2, "SMASS": 0},
"npt": {
"MDALGO": 3,
"ISIF": 3,
"LANGEVIN_GAMMA": [10] * structure.ntypesp,
"LANGEVIN_GAMMA_L": 1,
"PMASS": 10,
"PSTRESS": 0,
},
}
try:
return defaults[ensemble.lower()] # type: ignore[return-value]
except KeyError as err:
supported = tuple(defaults)
raise ValueError(f"Expect {ensemble=} to be one of {supported}") from err
[docs]
@dataclass
class LobsterTightStaticSetGenerator(LobsterSet):
"""
Class to generate well-converged statics for LOBSTER analysis.
Parameters
----------
structure : Structure
input structure.
isym : int
ISYM entry for INCAR, only isym=-1 and isym=0 are allowed
ismear : int
ISMEAR entry for INCAR, only ismear=-5 and ismear=0 are allowed
reciprocal_density : int
Density of k-mesh by reciprocal volume
user_supplied_basis : dict
dict including basis functions for all elements in
structure, e.g. {"Fe": "3d 3p 4s", "O": "2s 2p"}; if not supplied, a
standard basis is used
address_basis_file : str
address to a file similar to "BASIS_PBE_54_standard.yaml"
in pymatgen.io.lobster.lobster_basis
user_potcar_settings :dict
dict including potcar settings for all elements in structure,
e.g. {"Fe": "Fe_pv", "O": "O"}; if not supplied, a standard basis is used.
**kwargs: Other kwargs supported by VaspInputSet.
"""
reciprocal_density: int = 400
@property
def incar_updates(self) -> dict[str, Any]:
"""Get updates to the INCAR for a molecular dynamics job.
Returns
-------
dict
A dictionary of updates to apply.
"""
return super().incar_updates | {
"EDIFF": 1e-7,
"ISPIN": 1,
"LAECHG": False,
"LREAL": False,
"LVTOT": False,
"ALGO": "Normal",
"LCHARG": False,
"LWAVE": True,
"ISYM": 0,
}