Source code for atomate2.vasp.sets.base

"""Module defining base VASP input set and generator."""

from __future__ import annotations

import glob
import os
import warnings
from copy import deepcopy
from dataclasses import dataclass, field
from importlib.resources import files as get_mod_path
from itertools import groupby
from pathlib import Path
from typing import TYPE_CHECKING, Any

import numpy as np
from monty.io import zopen
from monty.serialization import loadfn
from pymatgen.electronic_structure.core import Magmom
from pymatgen.io.core import InputGenerator, InputSet
from pymatgen.io.vasp import Incar, Kpoints, Outcar, Poscar, Potcar, Vasprun
from pymatgen.io.vasp.sets import (
    BadInputSetWarning,
    get_valid_magmom_struct,
    get_vasprun_outcar,
)
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
from pymatgen.symmetry.bandstructure import HighSymmKpath

from atomate2 import SETTINGS

if TYPE_CHECKING:
    from collections.abc import Sequence

    from pymatgen.core import Structure


_BASE_VASP_SET = loadfn(get_mod_path("atomate2.vasp.sets") / "BaseVaspSet.yaml")


[docs] class VaspInputSet(InputSet): """ A class to represent a set of VASP inputs. Parameters ---------- incar An Incar object. kpoints A Kpoints object. poscar A Poscar object. potcar A list of Potcar objects. optional_files Other input files supplied as a dict of ``{filename: object}``. The objects should follow standard pymatgen conventions in implementing an ``as_dict()`` and ``from_dict`` method. """ def __init__( self, incar: Incar, poscar: Poscar, potcar: Potcar | list[str], kpoints: Kpoints | None = None, optional_files: dict | None = None, ) -> None: self.incar = incar self.poscar = poscar self.potcar = potcar self.kpoints = kpoints self.optional_files = optional_files or {}
[docs] def write_input( self, directory: str | Path, make_dir: bool = True, overwrite: bool = True, potcar_spec: bool = False, ) -> None: """ Write VASP input files to a directory. Parameters ---------- directory Directory to write input files to. make_dir Whether to create the directory if it does not already exist. overwrite Whether to overwrite an input file if it already exists. """ directory = Path(directory) if make_dir: os.makedirs(directory, exist_ok=True) inputs = {"INCAR": self.incar, "KPOINTS": self.kpoints, "POSCAR": self.poscar} inputs.update(self.optional_files) if isinstance(self.potcar, Potcar): inputs["POTCAR"] = self.potcar else: inputs["POTCAR.spec"] = "\n".join(self.potcar) for key, val in inputs.items(): if val is not None and (overwrite or not (directory / key).exists()): with zopen(directory / key, mode="wt") as file: if isinstance(val, Poscar): # write POSCAR with more significant figures file.write(val.get_str(significant_figures=16)) else: file.write(str(val)) elif not overwrite and (directory / key).exists(): raise FileExistsError(f"{directory / key} already exists.")
[docs] @staticmethod def from_directory( directory: str | Path, optional_files: dict = None ) -> VaspInputSet: """ Load a set of VASP inputs from a directory. Note that only the standard INCAR, POSCAR, POTCAR and KPOINTS files are read unless optional_filenames is specified. Parameters ---------- directory Directory to read VASP inputs from. optional_files Optional files to read in as well as a dict of {filename: Object class}. Object class must have a static/class method from_file. """ directory = Path(directory) objs = {"INCAR": Incar, "KPOINTS": Kpoints, "POSCAR": Poscar, "POTCAR": Potcar} inputs = {} for name, obj in objs.items(): if (directory / name).exists(): inputs[name.lower()] = obj.from_file(directory / name) else: # handle the case where there is no KPOINTS file inputs[name.lower()] = None optional_inputs = {} if optional_files is not None: for name, obj in optional_files.items(): optional_inputs[name] = obj.from_file(directory / name) return VaspInputSet(**inputs, optional_files=optional_inputs)
@property def is_valid(self) -> bool: """ Whether the input set is valid. Returns ------- bool Whether the input set is valid. """ if self.incar.get("KSPACING", 0) > 0.5 and self.incar.get("ISMEAR", 0) == -5: warnings.warn( "Large KSPACING value detected with ISMEAR=-5. Ensure that VASP " "generates enough KPOINTS, lower KSPACING, or set ISMEAR=0", BadInputSetWarning, stacklevel=1, ) ismear = self.incar.get("ISMEAR", 1) sigma = self.incar.get("SIGMA", 0.2) if ( all(elem.is_metal for elem in self.poscar.structure.composition) and self.incar.get("NSW", 0) > 0 and (ismear < 0 or (ismear == 0 and sigma > 0.05)) ): ismear_docs = "https://www.vasp.at/wiki/index.php/ISMEAR" msg = "" if ismear < 0: msg = f"Relaxation of likely metal with ISMEAR < 0 ({ismear})." elif ismear == 0 and sigma > 0.05: msg = f"ISMEAR = 0 with a small SIGMA ({sigma}) detected." warnings.warn( f"{msg} See VASP recommendations on ISMEAR for metals ({ismear_docs}).", BadInputSetWarning, stacklevel=1, ) if self.incar.get("LHFCALC") and self.incar.get("ALGO", "Normal") not in ( "Normal", "All", "Damped", ): warnings.warn( "Hybrid functionals only support Algo = All, Damped, or Normal.", BadInputSetWarning, stacklevel=1, ) if not self.incar.get("LASPH") and ( self.incar.get("METAGGA") or self.incar.get("LHFCALC") or self.incar.get("LDAU") or self.incar.get("LUSE_VDW") ): warnings.warn( "LASPH = True should be set for +U, meta-GGAs, hybrids, and vdW-DFT", BadInputSetWarning, stacklevel=1, ) return True
[docs] @dataclass class VaspInputGenerator(InputGenerator): """ A class to generate VASP input sets. .. Note:: Get the magmoms using the following precedence. 1. user incar settings 2. magmoms in input struct 3. spins in input struct 4. job config dict 5. set all magmoms to 0.6 Parameters ---------- user_incar_settings User INCAR settings. This allows a user to override INCAR settings, e.g., setting a different MAGMOM for various elements or species. The config_dict supports EDIFF_PER_ATOM and EDIFF keys. The former scales with # of atoms, the latter does not. If both are present, EDIFF is preferred. To force such settings, just supply user_incar_settings={"EDIFF": 1e-5, "LDAU": False} for example. The keys 'LDAUU', 'LDAUJ', 'LDAUL' are special cases since pymatgen defines different values depending on what anions are present in the structure, so these keys can be defined in one of two ways, e.g. either {"LDAUU":{"O":{"Fe":5}}} to set LDAUU for Fe to 5 in an oxide, or {"LDAUU":{"Fe":5}} to set LDAUU to 5 regardless of the input structure. To set magmoms, pass a dict mapping the strings of species to magnetic moments, e.g. {"MAGMOM": {"Co": 1}} or {"MAGMOM": {"Fe2+,spin=4": 3.7}} in the case of a site with Species("Fe2+", spin=4). If None is given, that key is unset. For example, {"ENCUT": None} will remove ENCUT from the incar settings. user_kpoints_settings Allow user to override kpoints setting by supplying a dict. E.g., ``{"reciprocal_density": 1000}``. User can also supply a Kpoints object. user_potcar_settings Allow user to override POTCARs. E.g., {"Gd": "Gd_3"}. user_potcar_functional Functional to use. Default is to use the functional in the config dictionary. Valid values: "PBE", "PBE_52", "PBE_54", "PBE_64", "LDA", "LDA_52", "LDA_54", "LDA_64", "PW91", "LDA_US", "PW91_US". auto_ismear If true, the values for ISMEAR and SIGMA will be set automatically depending on the bandgap of the system. If the bandgap is not known (e.g., there is no previous VASP directory) then ISMEAR=0 and SIGMA=0.2; if the bandgap is zero (a metallic system) then ISMEAR=2 and SIGMA=0.2; if the system is an insulator, then ISMEAR=-5 (tetrahedron smearing). Note, this only works when generating the input set from a previous VASP directory. auto_ispin If generating input set from a previous calculation, this controls whether to disable magnetisation (ISPIN = 1) if the absolute value of all magnetic moments are less than 0.02. auto_lreal If True, automatically use the VASP recommended LREAL based on cell size. auto_metal_kpoints If true and the system is metallic, try and use ``reciprocal_density_metal`` instead of ``reciprocal_density`` for metallic systems. Note, this only works when generating the input set from a previous VASP directory. auto_kspacing If true, automatically use the VASP recommended KSPACING based on bandgap, i.e. higher kpoint spacing for insulators than metals. Can be boolean or float. If a float, the value will be interpreted as the bandgap in eV to use for the KSPACING calculation. constrain_total_magmom Whether to constrain the total magmom (NUPDOWN in INCAR) to be the sum of the initial MAGMOM guess for all species. validate_magmom Ensure that missing magmom values are filled in with the default value of 1.0. use_structure_charge If set to True, then the overall charge of the structure (``structure.charge``) is used to set NELECT. sort_structure Whether to sort the structure (using the default sort order of electronegativity) before generating input files. Defaults to True, the behavior you would want most of the time. This ensures that similar atomic species are grouped together. force_gamma Force gamma centered kpoint generation. vdw Adds default parameters for van-der-Waals functionals supported by VASP to INCAR. Supported functionals are: DFT-D2, undamped DFT-D3, DFT-D3 with Becke-Jonson damping, Tkatchenko-Scheffler, Tkatchenko-Scheffler with iterative Hirshfeld partitioning, MBD@rSC, dDsC, Dion's vdW-DF, DF2, optPBE, optB88, optB86b and rVV10. symprec Tolerance for symmetry finding, used for line mode band structure k-points. config_dict The config dictionary to use containing the base input set settings. inherit_incar Whether to inherit INCAR settings from previous calculation. This might be useful to port Custodian fixes to child jobs but can also be dangerous e.g. when switching from GGA to meta-GGA or relax to static jobs. Defaults to True. """ user_incar_settings: dict = field(default_factory=dict) user_kpoints_settings: dict | Kpoints = field(default_factory=dict) user_potcar_settings: dict = field(default_factory=dict) user_potcar_functional: str = None auto_ismear: bool = True auto_ispin: bool = False auto_lreal: bool = False auto_kspacing: bool | float = False auto_metal_kpoints: bool = True constrain_total_magmom: bool = False validate_magmom: bool = True use_structure_charge: bool = False sort_structure: bool = True force_gamma: bool = True symprec: float = SETTINGS.SYMPREC vdw: str = None # copy _BASE_VASP_SET to ensure each class instance has its own copy # otherwise in-place changes can affect other instances config_dict: dict = field(default_factory=lambda: _BASE_VASP_SET) inherit_incar: bool = None def __post_init__(self) -> None: """Post init formatting of arguments.""" self.vdw = None if self.vdw is None else self.vdw.lower() if self.user_incar_settings.get("KSPACING") and self.user_kpoints_settings: warnings.warn( "You have specified KSPACING and also supplied kpoints settings. " "KSPACING only has effect when there is no KPOINTS file. Since both " "settings were given, pymatgen will generate a KPOINTS file and ignore " "KSPACING. Remove the `user_kpoints_settings` argument to enable " "KSPACING.", BadInputSetWarning, stacklevel=1, ) if self.vdw: from pymatgen.io.vasp.sets import MODULE_DIR as PMG_SET_DIR vdw_par = loadfn(PMG_SET_DIR / "vdW_parameters.yaml") if self.vdw not in vdw_par: raise KeyError( "Invalid or unsupported van-der-Waals functional. Supported " f"functionals are {list(vdw_par)}" ) self.config_dict["INCAR"].update(vdw_par[self.vdw]) # read the POTCAR_FUNCTIONAL from the .yaml self.potcar_functional = self.config_dict.get("POTCAR_FUNCTIONAL", "PS") # warn if a user is overriding POTCAR_FUNCTIONAL if ( self.user_potcar_functional and self.user_potcar_functional != self.potcar_functional ): warnings.warn( "Overriding the POTCAR functional is generally not recommended " "as it can significantly affect the results of calculations and " "compatibility with other calculations done with the same input set. " "Note that some POTCAR symbols specified in the configuration file may " "not be available in the selected functional.", BadInputSetWarning, stacklevel=1, ) self.potcar_functional = self.user_potcar_functional if self.user_potcar_settings: warnings.warn( "Overriding POTCARs is generally not recommended as it can " "significantly affect the results of calculations and compatibility " "with other calculations done with the same input set. In many " "instances, it is better to write a subclass of a desired input set and" " override the POTCAR in the subclass to be explicit on the " "differences.", BadInputSetWarning, stacklevel=1, ) for k, v in self.user_potcar_settings.items(): self.config_dict["POTCAR"][k] = v if self.inherit_incar is None: self.inherit_incar = SETTINGS.VASP_INHERIT_INCAR
[docs] def get_input_set( self, structure: Structure = None, prev_dir: str | Path = None, potcar_spec: bool = False, ) -> VaspInputSet: """ Get a VASP input set. Note, if both ``structure`` and ``prev_dir`` are set, then the structure specified will be preferred over the final structure from the last VASP run. Parameters ---------- structure A structure. prev_dir A previous directory to generate the input set from. potcar_spec Instead of generating a Potcar object, use a list of potcar symbols. This will be written as a "POTCAR.spec" file. This is intended to help sharing an input set with people who might not have a license to specific Potcar files. Given a "POTCAR.spec", the specific POTCAR file can be re-generated using pymatgen with the "generate_potcar" function in the pymatgen CLI. Returns ------- VaspInputSet A VASP input set. """ structure, prev_incar, bandgap, ispin, vasprun, outcar = self._get_previous( structure, prev_dir ) prev_incar = prev_incar if self.inherit_incar else {} kwds = { "structure": structure, "prev_incar": prev_incar, "bandgap": bandgap, "vasprun": vasprun, "outcar": outcar, } incar_updates = self.get_incar_updates(**kwds) kpoints_updates = self.get_kpoints_updates(**kwds) kspacing = self._kspacing(incar_updates) kpoints = self._get_kpoints(structure, kpoints_updates, kspacing, bandgap) incar = self._get_incar( structure, kpoints, prev_incar, incar_updates, bandgap=bandgap, ispin=ispin, ) site_properties = structure.site_properties poscar = Poscar( structure, velocities=site_properties.get("velocities"), predictor_corrector=site_properties.get("predictor_corrector"), predictor_corrector_preamble=structure.properties.get( "predictor_corrector_preamble" ), lattice_velocities=structure.properties.get("lattice_velocities"), ) return VaspInputSet( incar=incar, kpoints=kpoints, poscar=poscar, potcar=self._get_potcar(structure, potcar_spec=potcar_spec), )
[docs] def get_incar_updates( self, structure: Structure, prev_incar: dict = None, bandgap: float = 0.0, vasprun: Vasprun = None, outcar: Outcar = None, ) -> dict: """ Get updates to the INCAR for this calculation type. 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. """ raise NotImplementedError
[docs] def get_kpoints_updates( self, structure: Structure, prev_incar: dict = None, bandgap: float = 0.0, vasprun: Vasprun = None, outcar: Outcar = None, ) -> dict: """ Get updates to the kpoints configuration for this calculation type. Note, these updates will be ignored if the user has set user_kpoint_settings. 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 to the KPOINTS config. """ return {}
[docs] def get_nelect(self, structure: Structure) -> float: """ Get the default number of electrons for a given structure. Parameters ---------- structure A structure. Returns ------- float Number of electrons for the structure. """ potcar = self._get_potcar(structure, potcar_spec=False) map_elem_electrons = {p.element: p.nelectrons for p in potcar} comp = structure.composition.element_composition n_electrons = sum( num_atoms * map_elem_electrons[str(el)] for el, num_atoms in comp.items() ) return n_electrons - (structure.charge if self.use_structure_charge else 0)
def _get_previous( self, structure: Structure = None, prev_dir: str | Path = None ) -> tuple: """Load previous calculation outputs and decide which structure to use.""" if structure is None and prev_dir is None: raise ValueError("Either structure or prev_dir must be set") prev_incar = {} prev_structure = None vasprun = None outcar = None bandgap = None ispin = None if prev_dir: vasprun, outcar = get_vasprun_outcar(prev_dir) path_prev_dir = Path(prev_dir) # CONTCAR is already renamed POSCAR contcars = list(glob.glob(str(path_prev_dir / "POSCAR*"))) contcar_file_fullpath = str(path_prev_dir / "POSCAR") contcar_file = ( contcar_file_fullpath if contcar_file_fullpath in contcars else max(contcars) ) contcar = Poscar.from_file(contcar_file) if vasprun.efermi is None: # VASP doesn't output efermi in vasprun if IBRION = 1 vasprun.efermi = outcar.efermi bs = vasprun.get_band_structure(efermi="smart") prev_incar = vasprun.incar # use structure from CONTCAR as it is written to greater # precision than in the vasprun prev_structure = contcar.structure bandgap = 0 if bs.is_metal() else bs.get_band_gap()["energy"] if self.auto_ispin: # turn off spin when magmom for every site is smaller than 0.02. ispin = _get_ispin(vasprun, outcar) structure = structure if structure is not None else prev_structure structure = self._get_structure(structure) return structure, prev_incar, bandgap, ispin, vasprun, outcar def _get_structure(self, structure: Structure) -> Structure: """Get the standardized structure.""" for site in structure: if "magmom" in site.properties and isinstance( site.properties["magmom"], Magmom ): # required to fix bug in get_valid_magmom_struct site.properties["magmom"] = list(site.properties["magmom"]) if self.sort_structure: structure = structure.get_sorted_structure() if self.validate_magmom: get_valid_magmom_struct(structure, spin_mode="auto", inplace=True) return structure def _get_potcar(self, structure: Structure, potcar_spec: bool = False) -> Potcar: """Get the POTCAR.""" elements = [a[0] for a in groupby([s.specie.symbol for s in structure])] potcar_symbols = [self.config_dict["POTCAR"].get(el, el) for el in elements] if potcar_spec: return potcar_symbols potcar = Potcar(potcar_symbols, functional=self.potcar_functional) # warn if the selected POTCARs do not correspond to the chosen potcar_functional for psingle in potcar: if self.potcar_functional not in psingle.identify_potcar()[0]: warnings.warn( f"POTCAR data with symbol {psingle.symbol} is not known by pymatgen" " to correspond with the selected potcar_functional " f"{self.potcar_functional}. This POTCAR is known to correspond with" f" functionals {psingle.identify_potcar(mode='data')[0]}. Please " "verify that you are using the right POTCARs!", BadInputSetWarning, stacklevel=1, ) return potcar def _get_incar( self, structure: Structure, kpoints: Kpoints, previous_incar: dict = None, incar_updates: dict = None, bandgap: float = None, ispin: int = None, ) -> Incar: """Get the INCAR.""" previous_incar = previous_incar or {} incar_updates = incar_updates or {} incar_settings = dict(self.config_dict["INCAR"]) config_magmoms = incar_settings.get("MAGMOM", {}) auto_updates = {} # apply user incar settings to SETTINGS not to INCAR _apply_incar_updates(incar_settings, self.user_incar_settings) # generate incar incar = Incar() for key, val in incar_settings.items(): if key == "MAGMOM": incar[key] = get_magmoms( structure, magmoms=self.user_incar_settings.get("MAGMOM", {}), config_magmoms=config_magmoms, ) elif key in ("LDAUU", "LDAUJ", "LDAUL") and incar_settings.get( "LDAU", False ): incar[key] = _get_u_param(key, val, structure) elif key.startswith("EDIFF") and key != "EDIFFG": incar["EDIFF"] = _get_ediff(key, val, structure, incar_settings) else: incar[key] = val _set_u_params(incar, incar_settings, structure) # apply previous incar settings, be careful not to override user_incar_settings # also skip LDAU/MAGMOM as structure may have changed. skip = list(self.user_incar_settings) skip += ["MAGMOM", "NUPDOWN", "LDAUU", "LDAUL", "LDAUJ", "LMAXMIX"] _apply_incar_updates(incar, previous_incar, skip=skip) if self.constrain_total_magmom: nupdown = sum(mag if abs(mag) > 0.6 else 0 for mag in incar["MAGMOM"]) if abs(nupdown - round(nupdown)) > 1e-5: warnings.warn( "constrain_total_magmom was set to True, but the sum of MAGMOM " "values is not an integer. NUPDOWN is meant to set the spin " "multiplet and should typically be an integer. You are likely " "better off changing the values of MAGMOM or simply setting " "NUPDOWN directly in your INCAR settings.", UserWarning, stacklevel=1, ) auto_updates["NUPDOWN"] = nupdown if self.use_structure_charge: auto_updates["NELECT"] = self.get_nelect(structure) # handle auto ISPIN if ispin is not None and "ISPIN" not in self.user_incar_settings: auto_updates["ISPIN"] = ispin if self.auto_ismear: bandgap_tol = getattr(self, "bandgap_tol", SETTINGS.BANDGAP_TOL) if bandgap is None: # don't know if we are a metal or insulator so set ISMEAR and SIGMA to # be safe with the most general settings auto_updates.update(ISMEAR=0, SIGMA=0.2) elif bandgap <= bandgap_tol: auto_updates.update(ISMEAR=2, SIGMA=0.2) # metal else: auto_updates.update(ISMEAR=-5, SIGMA=0.05) # insulator if self.auto_lreal: auto_updates["LREAL"] = _get_recommended_lreal(structure) if self.auto_kspacing is False: bandgap = None # don't auto-set KSPACING based on bandgap elif isinstance(self.auto_kspacing, float): # interpret auto_kspacing as bandgap and set KSPACING based on user input bandgap = self.auto_kspacing _set_kspacing(incar, incar_settings, self.user_incar_settings, bandgap, kpoints) # apply updates from auto options, careful not to override user_incar_settings _apply_incar_updates(incar, auto_updates, skip=list(self.user_incar_settings)) # apply updates from inputset generator _apply_incar_updates(incar, incar_updates, skip=list(self.user_incar_settings)) # Remove unused INCAR parameters _remove_unused_incar_params(incar, skip=list(self.user_incar_settings)) # Finally, re-apply `self.user_incar_settings` to make sure any accidentally # overwritten settings are changed back to the intended values. # skip dictionary parameters to avoid dictionaries appearing in the INCAR skip = ["LDAUU", "LDAUJ", "LDAUL", "MAGMOM"] _apply_incar_updates(incar, self.user_incar_settings, skip=skip) return incar def _get_kpoints( self, structure: Structure, kpoints_updates: dict[str, Any] | None, kspacing: float | None, bandgap: float | None, ) -> Kpoints | None: """Get the kpoints file.""" kpoints_updates = kpoints_updates or {} # use user setting if set otherwise default to base config settings if self.user_kpoints_settings != {}: kconfig = deepcopy(self.user_kpoints_settings) else: # apply updates to k-points config kconfig = deepcopy(self.config_dict.get("KPOINTS", {})) kconfig.update(kpoints_updates) # Return None if KSPACING is set and no other user k-points settings have been # specified, because this will cause VASP to generate the kpoints automatically if kspacing and not self.user_kpoints_settings: return None if isinstance(kconfig, Kpoints): return kconfig explicit = ( kconfig.get("explicit") or len(kconfig.get("added_kpoints", [])) > 0 or "zero_weighted_reciprocal_density" in kconfig or "zero_weighted_line_density" in kconfig ) # handle length generation first as this doesn't support any additional options if kconfig.get("length"): if explicit: raise ValueError( "length option cannot be used with explicit k-point generation, " "added_kpoints, or zero weighted k-points." ) # If length is in kpoints settings use Kpoints.automatic return Kpoints.automatic(kconfig["length"]) base_kpoints = None if kconfig.get("line_density"): # handle line density generation kpath = HighSymmKpath(structure, **kconfig.get("kpath_kwargs", {})) frac_k_points, k_points_labels = kpath.get_kpoints( line_density=kconfig["line_density"], coords_are_cartesian=False ) base_kpoints = Kpoints( comment="Non SCF run along symmetry lines", style=Kpoints.supported_modes.Reciprocal, num_kpts=len(frac_k_points), kpts=frac_k_points, labels=k_points_labels, kpts_weights=[1] * len(frac_k_points), ) elif kconfig.get("grid_density") or kconfig.get("reciprocal_density"): # handle regular weighted k-point grid generation if kconfig.get("grid_density"): base_kpoints = Kpoints.automatic_density( structure, int(kconfig["grid_density"]), self.force_gamma ) elif kconfig.get("reciprocal_density"): if ( bandgap == 0 and kconfig.get("reciprocal_density_metal") and self.auto_metal_kpoints ): density = kconfig["reciprocal_density_metal"] else: density = kconfig["reciprocal_density"] base_kpoints = Kpoints.automatic_density_by_vol( structure, density, self.force_gamma ) if explicit: sga = SpacegroupAnalyzer(structure, symprec=self.symprec) mesh = sga.get_ir_reciprocal_mesh(base_kpoints.kpts[0]) base_kpoints = Kpoints( comment="Uniform grid", style=Kpoints.supported_modes.Reciprocal, num_kpts=len(mesh), kpts=[i[0] for i in mesh], kpts_weights=[i[1] for i in mesh], ) else: # if not explicit that means no other options have been specified # so we can return the k-points as is return base_kpoints zero_weighted_kpoints = None if kconfig.get("zero_weighted_line_density"): # zero_weighted k-points along line mode path kpath = HighSymmKpath(structure) frac_k_points, k_points_labels = kpath.get_kpoints( line_density=kconfig["zero_weighted_line_density"], coords_are_cartesian=False, ) zero_weighted_kpoints = Kpoints( comment="Hybrid run along symmetry lines", style=Kpoints.supported_modes.Reciprocal, num_kpts=len(frac_k_points), kpts=frac_k_points, labels=k_points_labels, kpts_weights=[0] * len(frac_k_points), ) elif kconfig.get("zero_weighted_reciprocal_density"): zero_weighted_kpoints = Kpoints.automatic_density_by_vol( structure, kconfig["zero_weighted_reciprocal_density"], self.force_gamma ) sga = SpacegroupAnalyzer(structure, symprec=self.symprec) mesh = sga.get_ir_reciprocal_mesh(zero_weighted_kpoints.kpts[0]) zero_weighted_kpoints = Kpoints( comment="Uniform grid", style=Kpoints.supported_modes.Reciprocal, num_kpts=len(mesh), kpts=[i[0] for i in mesh], kpts_weights=[0 for i in mesh], ) added_kpoints = None if kconfig.get("added_kpoints"): added_kpoints = Kpoints( comment="Specified k-points only", style=Kpoints.supported_modes.Reciprocal, num_kpts=len(kconfig.get("added_kpoints")), kpts=kconfig.get("added_kpoints"), labels=["user-defined"] * len(kconfig.get("added_kpoints")), kpts_weights=[0] * len(kconfig.get("added_kpoints")), ) if base_kpoints and not (added_kpoints or zero_weighted_kpoints): return base_kpoints if added_kpoints and not (base_kpoints or zero_weighted_kpoints): return added_kpoints # do some sanity checking if "line_density" in kconfig and zero_weighted_kpoints: raise ValueError( "Cannot combined line_density and zero weighted k-points options" ) if zero_weighted_kpoints and not base_kpoints: raise ValueError( "Zero weighted k-points must be used with reciprocal_density or " "grid_density options" ) if not (base_kpoints or zero_weighted_kpoints or added_kpoints): raise ValueError( "Invalid k-point generation algo. Supported Keys are 'grid_density' " "for Kpoints.automatic_density generation, 'reciprocal_density' for " "KPoints.automatic_density_by_vol generation, 'length' for " "Kpoints.automatic generation, 'line_density' for line mode generation," " 'added_kpoints' for specific k-points to include, " " 'zero_weighted_reciprocal_density' for a zero weighted uniform mesh," " or 'zero_weighted_line_density' for a zero weighted line mode mesh." ) return _combine_kpoints(base_kpoints, zero_weighted_kpoints, added_kpoints) def _kspacing(self, incar_updates: dict[str, Any]) -> float | None: """Get KSPACING value based on the config dict, updates and user settings.""" key = "KSPACING" return self.user_incar_settings.get( key, incar_updates.get(key, self.config_dict["INCAR"].get(key)) )
[docs] def get_magmoms( structure: Structure, magmoms: dict[str, float] = None, config_magmoms: dict[str, float] = None, ) -> list[float]: """Get the mamgoms using the following precedence. 1. user incar settings 2. magmoms in input struct 3. spins in input struct 4. job config dict 5. set all magmoms to 0.6 """ magmoms = magmoms or {} config_magmoms = config_magmoms or {} mag = [] msg = ( "Co without an oxidation state is initialized as low spin by default in " "Atomate2. If this default behavior is not desired, please set the spin on the " "magmom on the site directly to ensure correct initialization." ) for site in structure: specie = str(site.specie) if specie in magmoms: mag.append(magmoms.get(specie)) elif hasattr(site, "magmom"): mag.append(site.magmom) elif hasattr(site.specie, "spin") and site.specie.spin is not None: mag.append(site.specie.spin) elif specie in config_magmoms: if site.specie.symbol == "Co" and config_magmoms[specie] <= 1.0: warnings.warn(msg, stacklevel=2) mag.append(config_magmoms.get(specie)) else: if site.specie.symbol == "Co": warnings.warn(msg, stacklevel=2) mag.append(magmoms.get(site.specie.symbol, 0.6)) return mag
def _get_u_param( lda_param: str, lda_config: dict[str, Any], structure: Structure ) -> list[float]: """Get U parameters.""" comp = structure.composition elements = sorted((el for el in comp.elements if comp[el] > 0), key=lambda e: e.X) most_electroneg = elements[-1].symbol poscar = Poscar(structure) if hasattr(structure[0], lda_param.lower()): m = {site.specie.symbol: getattr(site, lda_param.lower()) for site in structure} return [m[sym] for sym in poscar.site_symbols] if isinstance(lda_config.get(most_electroneg, 0), dict): # lookup specific LDAU if specified for most_electroneg atom return [lda_config[most_electroneg].get(sym, 0) for sym in poscar.site_symbols] return [ lda_config.get(sym, 0) if isinstance(lda_config.get(sym, 0), (float, int)) else 0 for sym in poscar.site_symbols ] def _get_ediff( param: str, value: str | float, structure: Structure, incar_settings: dict[str, Any] ) -> float: """Get EDIFF.""" if incar_settings.get("EDIFF") is None and param == "EDIFF_PER_ATOM": return float(value) * structure.num_sites return float(incar_settings["EDIFF"]) def _set_u_params( incar: Incar, incar_settings: dict[str, Any], structure: Structure ) -> None: """Modify INCAR for use with U parameters.""" has_u = incar_settings.get("LDAU") and sum(incar["LDAUU"]) > 0 if not has_u: ldau_keys = [key for key in incar if key.startswith("LDAU")] for key in ldau_keys: incar.pop(key, None) # Modify LMAXMIX if you have d or f electrons present. Note that if the user # explicitly sets LMAXMIX in settings it will override this logic (setdefault keeps # current value). Previously, this was only set if Hubbard U was enabled as per the # VASP manual but following an investigation it was determined that this would lead # to a significant difference between SCF -> NonSCF even without Hubbard U enabled. # Thanks to Andrew Rosen for investigating and reporting. blocks = [site.specie.block for site in structure] if "f" in blocks: # contains f-electrons incar.setdefault("LMAXMIX", 6) elif "d" in blocks: # contains d-electrons incar.setdefault("LMAXMIX", 4) def _apply_incar_updates( incar: dict[str, Any], updates: dict[str, Any], skip: Sequence[str] = () ) -> None: """ Apply updates to an INCAR file. Parameters ---------- incar An incar. updates Updates to apply. skip Keys to skip. """ for key, val in updates.items(): if key in skip: continue if val is None: incar.pop(key, None) else: incar[key] = val def _remove_unused_incar_params( incar: dict[str, Any], skip: Sequence[str] = () ) -> None: """ Remove INCAR parameters that are not actively used by VASP. Parameters ---------- incar An incar. skip Keys to skip. """ # Turn off IBRION/ISIF/POTIM if NSW = 0 opt_flags = ["EDIFFG", "IBRION", "ISIF", "POTIM"] if incar.get("NSW", 0) == 0: for opt_flag in opt_flags: if opt_flag not in skip: incar.pop(opt_flag, None) # Remove MAGMOMs if they aren't used if incar.get("ISPIN", 1) == 1 and "MAGMOM" not in skip: incar.pop("MAGMOM", None) # Turn off +U flags if +U is not even used ldau_flags = ("LDAUU", "LDAUJ", "LDAUL", "LDAUTYPE") if not incar.get("LDAU"): for ldau_flag in ldau_flags: if ldau_flag not in skip: incar.pop(ldau_flag, None) def _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: """Combine k-points files together.""" labels, kpoints, weights = [], [], [] recip_mode = Kpoints.supported_modes.Reciprocal for kpoints_object in filter(None, kpoints_objects): if kpoints_object.style != recip_mode: raise ValueError( f"Can only combine kpoints with style {recip_mode}, " f"got {kpoints_object.style}" ) labels.append(kpoints_object.labels or [""] * len(kpoints_object.kpts)) weights.append(kpoints_object.kpts_weights) kpoints.append(kpoints_object.kpts) labels = np.concatenate(labels).tolist() weights = np.concatenate(weights).tolist() kpoints = np.concatenate(kpoints) return Kpoints( comment="Combined k-points", style=recip_mode, num_kpts=len(kpoints), kpts=kpoints, labels=labels, kpts_weights=weights, ) def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None) -> int: """Get value of ISPIN depending on the magnetisation in the OUTCAR and vasprun.""" if outcar is not None and outcar.magnetization is not None: # Turn off spin when magmom for every site is smaller than 0.02. site_magmom = np.array([i["tot"] for i in outcar.magnetization]) return 2 if np.any(np.abs(site_magmom) > 0.02) else 1 if vasprun is not None: return 2 if vasprun.is_spin else 1 return 2 def _get_recommended_lreal(structure: Structure) -> str | bool: """Get recommended LREAL flag based on the structure.""" return "Auto" if structure.num_sites > 16 else False def _get_kspacing(bandgap: float, tol: float = 1e-4) -> float: """Get KSPACING based on a band gap.""" if bandgap <= tol: # metallic return 0.22 rmin = max(1.5, 25.22 - 2.87 * bandgap) # Eq. 25 kspacing = 2 * np.pi * 1.0265 / (rmin - 1.0183) # Eq. 29 # cap kspacing at a max of 0.44, per internal benchmarking return min(kspacing, 0.44) def _set_kspacing( incar: Incar, incar_settings: dict, user_incar_settings: dict, bandgap: float | None, kpoints: Kpoints | None, ) -> Incar: """ Set KSPACING in an INCAR. if kpoints is not None then unset any KSPACING if kspacing set in user_incar_settings then use that if auto_kspacing then do that if kspacing is set in config use that. if from_prev is True, ISMEAR will be set according to the band gap. """ if kpoints is not None: # unset KSPACING as we are using a KPOINTS file incar.pop("KSPACING", None) # Ensure adequate number of KPOINTS are present for the tetrahedron method # (ISMEAR=-5). If KSPACING is in the INCAR file the number of kpoints is not # known before calling VASP, but a warning is raised when the KSPACING value is # > 0.5 (2 reciprocal Angstrom). An error handler in Custodian is available to # correct overly large KSPACING values (small number of kpoints) if necessary. if np.prod(kpoints.kpts) < 4 and incar.get("ISMEAR", 0) == -5: incar["ISMEAR"] = 0 elif "KSPACING" in user_incar_settings: incar["KSPACING"] = user_incar_settings["KSPACING"] elif incar_settings.get("KSPACING") and isinstance(bandgap, (int, float)): # will always default to 0.22 in first run as one # cannot be sure if one treats a metal or # semiconductor/insulator incar["KSPACING"] = _get_kspacing(bandgap) # This should default to ISMEAR=0 if band gap is not known (first computation) # if not from_prev: # # be careful to not override user_incar_settings elif incar_settings.get("KSPACING"): incar["KSPACING"] = incar_settings["KSPACING"] return incar