diff --git a/docs/pymatgen.md b/docs/pymatgen.md index d3105a49dde..a72f8e125bf 100644 --- a/docs/pymatgen.md +++ b/docs/pymatgen.md @@ -2,9 +2,6 @@ layout: default title: API Documentation nav_order: 6 ---- - ---- layout: default title: API Documentation nav_order: 6 @@ -4056,13 +4053,13 @@ nav_order: 6 * [`AbstractFeffInputSet.potential`](pymatgen.io.feff.md#pymatgen.io.feff.sets.AbstractFeffInputSet.potential) * [`AbstractFeffInputSet.tags`](pymatgen.io.feff.md#pymatgen.io.feff.sets.AbstractFeffInputSet.tags) * [`AbstractFeffInputSet.write_input()`](pymatgen.io.feff.md#pymatgen.io.feff.sets.AbstractFeffInputSet.write_input) - * [`FEFFDictSet`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFDictSet) - * [`FEFFDictSet.atoms`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFDictSet.atoms) - * [`FEFFDictSet.from_directory()`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFDictSet.from_directory) - * [`FEFFDictSet.header()`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFDictSet.header) - * [`FEFFDictSet.potential`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFDictSet.potential) - * [`FEFFDictSet.tags`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFDictSet.tags) - * [`MPEELSDictSet`](pymatgen.io.feff.md#pymatgen.io.feff.sets.MPEELSDictSet) + * [`FEFFVaspInputSet`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFVaspInputSet) + * [`FEFFVaspInputSet.atoms`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFVaspInputSet.atoms) + * [`FEFFVaspInputSet.from_directory()`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFVaspInputSet.from_directory) + * [`FEFFVaspInputSet.header()`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFVaspInputSet.header) + * [`FEFFVaspInputSet.potential`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFVaspInputSet.potential) + * [`FEFFVaspInputSet.tags`](pymatgen.io.feff.md#pymatgen.io.feff.sets.FEFFVaspInputSet.tags) + * [`MPEELSVaspInputSet`](pymatgen.io.feff.md#pymatgen.io.feff.sets.MPEELSVaspInputSet) * [`MPELNESSet`](pymatgen.io.feff.md#pymatgen.io.feff.sets.MPELNESSet) * [`MPELNESSet.CONFIG`](pymatgen.io.feff.md#pymatgen.io.feff.sets.MPELNESSet.CONFIG) * [`MPEXAFSSet`](pymatgen.io.feff.md#pymatgen.io.feff.sets.MPEXAFSSet) @@ -4395,8 +4392,8 @@ nav_order: 6 * [`FreqSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.FreqSet) * [`OptSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.OptSet) * [`PESScanSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.PESScanSet) - * [`QChemDictSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.QChemDictSet) - * [`QChemDictSet.write()`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.QChemDictSet.write) + * [`QChemVaspInputSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.QChemVaspInputSet) + * [`QChemVaspInputSet.write()`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.QChemVaspInputSet.write) * [`SinglePointSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.SinglePointSet) * [`TransitionStateSet`](pymatgen.io.qchem.md#pymatgen.io.qchem.sets.TransitionStateSet) * [pymatgen.io.qchem.utils module](pymatgen.io.qchem.md#module-pymatgen.io.qchem.utils) @@ -4763,16 +4760,16 @@ nav_order: 6 * [`get_band_structure_from_vasp_multiple_branches()`](pymatgen.io.vasp.md#pymatgen.io.vasp.outputs.get_band_structure_from_vasp_multiple_branches) * [pymatgen.io.vasp.sets module](pymatgen.io.vasp.md#module-pymatgen.io.vasp.sets) * [`BadInputSetWarning`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.BadInputSetWarning) - * [`DictSet`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet) - * [`DictSet.calculate_ng()`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.calculate_ng) - * [`DictSet.estimate_nbands()`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.estimate_nbands) - * [`DictSet.incar`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.incar) - * [`DictSet.kpoints`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.kpoints) - * [`DictSet.nelect`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.nelect) - * [`DictSet.poscar`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.poscar) - * [`DictSet.potcar_functional`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.potcar_functional) - * [`DictSet.structure`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.structure) - * [`DictSet.write_input()`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.DictSet.write_input) + * [`VaspInputSet`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet) + * [`VaspInputSet.calculate_ng()`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.calculate_ng) + * [`VaspInputSet.estimate_nbands()`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.estimate_nbands) + * [`VaspInputSet.incar`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.incar) + * [`VaspInputSet.kpoints`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.kpoints) + * [`VaspInputSet.nelect`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.nelect) + * [`VaspInputSet.poscar`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.poscar) + * [`VaspInputSet.potcar_functional`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.potcar_functional) + * [`VaspInputSet.structure`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.structure) + * [`VaspInputSet.write_input()`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.VaspInputSet.write_input) * [`LobsterSet`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.LobsterSet) * [`LobsterSet.incar`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.LobsterSet.incar) * [`MITMDSet`](pymatgen.io.vasp.md#pymatgen.io.vasp.sets.MITMDSet) @@ -5905,5 +5902,4 @@ nav_order: 6 * [`StructureVis.zoom()`](pymatgen.vis.md#pymatgen.vis.structure_vtk.StructureVis.zoom) * [`make_movie()`](pymatgen.vis.md#pymatgen.vis.structure_vtk.make_movie) - -## pymatgen.dao module \ No newline at end of file +## pymatgen.dao module diff --git a/pymatgen/io/vasp/inputs.py b/pymatgen/io/vasp/inputs.py index 8675b5823c9..e882737c8b6 100644 --- a/pymatgen/io/vasp/inputs.py +++ b/pymatgen/io/vasp/inputs.py @@ -19,7 +19,10 @@ from enum import Enum, unique from glob import glob from hashlib import sha256 +from pathlib import Path +from shutil import copyfileobj from typing import TYPE_CHECKING, NamedTuple, cast +from zipfile import ZipFile import numpy as np import scipy.constants as const @@ -38,7 +41,6 @@ if TYPE_CHECKING: from collections.abc import Iterator - from pathlib import Path from typing import Any, ClassVar, Literal from numpy.typing import ArrayLike @@ -2412,7 +2414,7 @@ def identify_potcar_hash_based( potcar_functionals (list): List of potcar functionals associated with the PotcarSingle """ - # Dict to translate the sets in the .json file to the keys used in DictSet + # Dict to translate the sets in the .json file to the keys used in VaspInputSet mapping_dict = { "potUSPP_GGA": { "pymatgen_key": "PW91_US", @@ -2737,7 +2739,8 @@ def __init__( incar: dict | Incar, kpoints: Kpoints | None, poscar: Poscar, - potcar: Potcar | None, + potcar: Potcar | str | None, + potcar_spec: bool = False, optional_files: dict[PathLike, object] | None = None, **kwargs, ) -> None: @@ -2748,14 +2751,18 @@ def __init__( incar (Incar): The Incar object. kpoints (Kpoints): The Kpoints object. poscar (Poscar): The Poscar object. - potcar (Potcar): The Potcar object. + potcar (Potcar or str): The Potcar object. + potcar_spec (bool = False) : used to share POTCAR info without license issues. + True --> POTCAR is a list of symbols, write POTCAR.spec + False --> POTCAR is a VASP POTCAR, write POTCAR optional_files (dict): Other input files supplied as a dict of {filename: object}. The object should follow standard pymatgen conventions in implementing a as_dict() and from_dict method. **kwargs: Additional keyword arguments to be stored in the VaspInput object. """ super().__init__(**kwargs) - self.update({"INCAR": incar, "KPOINTS": kpoints, "POSCAR": poscar, "POTCAR": potcar}) + self._potcar_filename = "POTCAR" + (".spec" if potcar_spec else "") + self.update({"INCAR": incar, "KPOINTS": kpoints, "POSCAR": poscar, self._potcar_filename: potcar}) if optional_files is not None: self.update(optional_files) @@ -2793,6 +2800,9 @@ def write_input( self, output_dir: PathLike = ".", make_dir_if_not_present: bool = True, + cif_name: str | None = None, + zip_name: str | None = None, + files_to_transfer: dict | None = None, ) -> None: """ Write VASP inputs to a directory. @@ -2802,6 +2812,14 @@ def write_input( Defaults to current directory ("."). make_dir_if_not_present (bool): Create the directory if not present. Defaults to True. + cif_name (str or None): If a str, the name of the CIF file + to write the POSCAR to (the POSCAR will also be written). + zip_name (str or None): If a str, the name of the zip to + archive the VASP input set to. + files_to_transfer (dict) : A dictionary of + { < input filename >: < output filepath >}. + This allows the transfer of < input filename > files from + a previous calculation to < output filepath >. """ if not os.path.isdir(output_dir) and make_dir_if_not_present: os.makedirs(output_dir) @@ -2811,6 +2829,28 @@ def write_input( with zopen(os.path.join(output_dir, key), mode="wt") as file: file.write(str(value)) + if cif_name: + self["POSCAR"].structure.to(filename=cif_name) + + if zip_name: + files_to_zip = list(self) + ([cif_name] if cif_name else []) + with ZipFile(os.path.join(output_dir, zip_name), mode="w") as zip_file: + for file in files_to_zip: + try: + zip_file.write(os.path.join(output_dir, file), arcname=file) + except FileNotFoundError: + pass + + try: + os.remove(os.path.join(output_dir, file)) + except (FileNotFoundError, PermissionError, IsADirectoryError): + pass + + files_to_transfer = files_to_transfer or {} + for key, val in files_to_transfer.items(): + with zopen(val, "rb") as fin, zopen(str(Path(output_dir) / key), "wb") as fout: + copyfileobj(fin, fout) + @classmethod def from_directory( cls, @@ -2884,3 +2924,23 @@ def run_vasp( open(err_file, mode="w", encoding="utf-8", buffering=1) as stderr_file, ): subprocess.check_call(vasp_cmd, stdout=stdout_file, stderr=stderr_file) + + @property + def incar(self) -> Incar: + """INCAR object.""" + return Incar(self["INCAR"]) if isinstance(self["INCAR"], dict) else self["INCAR"] + + @property + def kpoints(self) -> Kpoints | None: + """KPOINTS object.""" + return self["KPOINTS"] + + @property + def poscar(self) -> Poscar: + """POSCAR object.""" + return self["POSCAR"] + + @property + def potcar(self) -> Potcar | str | None: + """POTCAR or POTCAR.spec object.""" + return self[self._potcar_filename] diff --git a/pymatgen/io/vasp/sets.py b/pymatgen/io/vasp/sets.py index 8b98e433c0f..0071f8613ac 100644 --- a/pymatgen/io/vasp/sets.py +++ b/pymatgen/io/vasp/sets.py @@ -10,7 +10,7 @@ various input sets. Unless there is an extremely good reason to add a new set, **do not** add one. e.g. if you want to turn the Hubbard U off, just set "LDAU": False as a user_incar_setting. 2. All derivative input sets should inherit appropriate configurations (e.g., from MPRelaxSet), and more often than - not, DictSet should be the superclass. Proper superclass delegation should be used where possible. In particular, + not, VaspInputSet should be the superclass. Superclass delegation should be used where possible. In particular, you are not supposed to implement your own as_dict or from_dict for derivative sets unless you know what you are doing. Improper overriding the as_dict and from_dict protocols is the major cause of implementation headaches. If you need an example, look at how the MPStaticSet is initialized. @@ -30,9 +30,7 @@ import abc import itertools -import os import re -import shutil import warnings from collections.abc import Sequence from copy import deepcopy @@ -41,11 +39,9 @@ from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, cast -from zipfile import ZipFile import numpy as np from monty.dev import deprecated -from monty.io import zopen from monty.json import MSONable from monty.serialization import loadfn @@ -77,166 +73,6 @@ # MODULE_DIR = os.path.dirname(__file__) -class VaspInputSet(InputGenerator, abc.ABC): - """ - Base class representing a set of VASP input parameters with a structure - supplied as init parameters. Typically, you should not inherit from this - class. Start from DictSet or MPRelaxSet or MITRelaxSet. - """ - - _valid_potcars: Sequence[str] | None = None - - @property - @abc.abstractmethod - def incar(self): - """The input set's INCAR.""" - - @property - @abc.abstractmethod - def kpoints(self): - """The input set's KPOINTS.""" - - @property - @abc.abstractmethod - def poscar(self): - """The input set's POSCAR.""" - - @property - def potcar_symbols(self): - """List of POTCAR symbols.""" - - elements = self.poscar.site_symbols - potcar_symbols = [] - settings = self._config_dict["POTCAR"] - - if isinstance(settings[elements[-1]], dict): - for el in elements: - potcar_symbols.append(settings[el]["symbol"] if el in settings else el) - else: - for el in elements: - potcar_symbols.append(settings.get(el, el)) - - return potcar_symbols - - @property - def potcar(self) -> Potcar: - """The input set's POTCAR.""" - user_potcar_functional = self.user_potcar_functional - potcar = Potcar(self.potcar_symbols, functional=user_potcar_functional) - - # warn if the selected POTCARs do not correspond to the chosen user_potcar_functional - for p_single in potcar: - if user_potcar_functional not in p_single.identify_potcar()[0]: - warnings.warn( - f"POTCAR data with symbol {p_single.symbol} is not known by pymatgen to " - f"correspond with the selected {user_potcar_functional=}. This POTCAR " - f"is known to correspond with functionals {p_single.identify_potcar(mode='data')[0]}. " - "Please verify that you are using the right POTCARs!", - BadInputSetWarning, - ) - - return potcar - - @deprecated(message="get_vasp_input will be removed in a future version of pymatgen. Use get_input_set instead.") - def get_vasp_input(self, structure=None) -> VaspInput: - """Get a VaspInput object. - - Returns: - VaspInput. - """ - return self.get_input_set(structure=structure) - - def get_input_set(self, structure=None) -> VaspInput: - """Get a VaspInput object. - - Returns: - VaspInput. - """ - if structure is not None: - self.structure = structure - return VaspInput(incar=self.incar, kpoints=self.kpoints, poscar=self.poscar, potcar=self.potcar) - - def write_input( - self, - output_dir: str, - make_dir_if_not_present: bool = True, - include_cif: bool = False, - potcar_spec: bool = False, - zip_output: bool = False, - ) -> None: - """ - Writes a set of VASP input to a directory. - - Args: - output_dir (str): Directory to output the VASP input files - make_dir_if_not_present (bool): Set to True if you want the - directory (and the whole path) to be created if it is not - present. - include_cif (bool): Whether to write a CIF file in the output - directory for easier opening by VESTA. - potcar_spec (bool): Instead of writing the POTCAR, write a "POTCAR.spec". - 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. - zip_output (bool): If True, output will be zipped into a file with the - same name as the InputSet (e.g., MPStaticSet.zip) - """ - if potcar_spec: - vasp_input = None - if make_dir_if_not_present: - os.makedirs(output_dir, exist_ok=True) - - with zopen(f"{output_dir}/POTCAR.spec", mode="wt") as file: - file.write("\n".join(self.potcar_symbols)) - - for key in ["INCAR", "POSCAR", "KPOINTS"]: - if (val := getattr(self, key.lower())) is not None: - with zopen(os.path.join(output_dir, key), mode="wt") as file: - file.write(str(val)) - else: - vasp_input = self.get_input_set() - vasp_input.write_input(output_dir, make_dir_if_not_present=make_dir_if_not_present) - - cif_name = "" - if include_cif and vasp_input is not None: - struct = vasp_input["POSCAR"].structure - cif_name = f"{output_dir}/{struct.formula.replace(' ', '')}.cif" - struct.to(filename=cif_name) - - if zip_output: - filename = f"{type(self).__name__}.zip" - with ZipFile(os.path.join(output_dir, filename), mode="w") as zip_file: - for file in ["INCAR", "POSCAR", "KPOINTS", "POTCAR", "POTCAR.spec", cif_name]: - try: - zip_file.write(os.path.join(output_dir, file), arcname=file) - except FileNotFoundError: - pass - - try: - os.remove(os.path.join(output_dir, file)) - except (FileNotFoundError, PermissionError, IsADirectoryError): - pass - - def as_dict(self, verbosity=2): - """ - Args: - verbosity: Verbosity for generated dict. If 1, structure is - excluded. - - Returns: - dict: MSONable VaspInputSet representation. - """ - dct = MSONable.as_dict(self) - if verbosity == 1: - dct.pop("structure", None) - return dct - - -# create VaspInputGenerator alias to follow atomate2 terminology -VaspInputGenerator = VaspInputSet - - def _load_yaml_config(fname): config = loadfn(MODULE_DIR / (f"{fname}.yaml")) if "PARENT" in config: @@ -252,10 +88,11 @@ def _load_yaml_config(fname): @dataclass -class DictSet(VaspInputSet): +class VaspInputSet(InputGenerator, abc.ABC): """ - Concrete implementation of VaspInputSet that is initialized from a dict - settings. This allows arbitrary settings to be input. In general, + Base class representing a set of VASP input parameters with a structure + supplied as init parameters and initialized from a dict of settings. + This allows arbitrary settings to be input. In general, this is rarely used directly unless there is a source of settings in yaml format (e.g., from a REST interface). It is typically used by other VaspInputSets for initialization. @@ -265,13 +102,14 @@ class DictSet(VaspInputSet): structure and the configuration settings. The order in which the magmom is determined is as follows: - 1. If the site itself has a magmom setting (i.e. site.properties["magmom"] = float), + 1. If the site is specified in user_incar_settings, use that setting. + 2. If the site itself has a magmom setting (i.e. site.properties["magmom"] = float), that is used. This can be set with structure.add_site_property(). - 2. If the species of the site has a spin setting, that is used. This can be set + 3. If the species of the site has a spin setting, that is used. This can be set with structure.add_spin_by_element(). - 3. If the species itself has a particular setting in the config file, that + 4. If the species itself has a particular setting in the config file, that is used, e.g. Mn3+ may have a different magmom than Mn4+. - 4. Lastly, the element symbol itself is checked in the config file. If + 5. Lastly, the element symbol itself is checked in the config file. If there are no settings, a default value of 0.6 is used. Args: @@ -335,6 +173,8 @@ class DictSet(VaspInputSet): 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. + auto_kspacing (bool): If true, determines the value of KSPACING from the bandgap + of a previous calculation. auto_ismear (bool): 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 @@ -342,6 +182,16 @@ class DictSet(VaspInputSet): 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 (bool) = False: + 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 (bool) = False: + 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 + if the bandgap is not None. bandgap_tol (float): Tolerance for determining if a system is metallic when KSPACING is set to "auto". If the bandgap is less than this value, the system is considered metallic. Defaults to 1e-4 (eV). @@ -371,16 +221,22 @@ class DictSet(VaspInputSet): international_monoclinic: bool = True validate_magmom: bool = True inherit_incar: bool | list[str] = False + auto_kspacing: bool = False auto_ismear: bool = False + auto_ispin: bool = False + auto_lreal: bool = False + auto_metal_kpoints: bool = False bandgap_tol: float = 1e-4 bandgap: float | None = None prev_incar: str | dict | None = None prev_kpoints: str | Kpoints | None = None + _valid_potcars: Sequence[str] | None = None def __post_init__(self): """Perform validation""" - if (valid_potcars := self._valid_potcars) and self.user_potcar_functional not in valid_potcars: - raise ValueError(f"Invalid {self.user_potcar_functional=}, must be one of {valid_potcars}") + user_potcar_functional = self.user_potcar_functional + if (valid_potcars := self._valid_potcars) and user_potcar_functional not in valid_potcars: + raise ValueError(f"Invalid {user_potcar_functional=}, must be one of {valid_potcars}") if hasattr(self, "CONFIG"): self.config_dict = self.CONFIG @@ -392,9 +248,11 @@ def __post_init__(self): self.user_kpoints_settings = self.user_kpoints_settings or {} self.vdw = self.vdw.lower() if isinstance(self.vdw, str) else self.vdw - if self.user_incar_settings.get("KSPACING") and self.user_kpoints_settings is not None: + if self.user_incar_settings.get("KSPACING") and self.user_kpoints_settings: + # self.user_kpoints_settings will never be `None` because it is set to + # an empty dict if it is `None`. warnings.warn( - "You have specified KSPACING and also supplied kpoints " + "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." @@ -404,9 +262,9 @@ def __post_init__(self): if self.vdw: vdw_par = loadfn(MODULE_DIR / "vdW_parameters.yaml") - try: - self._config_dict["INCAR"].update(vdw_par[self.vdw]) - except KeyError: + if vdw_param := vdw_par.get(self.vdw): + self._config_dict["INCAR"].update(vdw_param) + else: raise KeyError( f"Invalid or unsupported van-der-Waals functional. Supported functionals are {', '.join(vdw_par)}." ) @@ -453,6 +311,71 @@ def __post_init__(self): self.prev_vasprun = None self.prev_outcar = None + self._ispin = None + + @deprecated(message="get_vasp_input will be removed in a future version of pymatgen. Use get_input_set instead.") + def get_vasp_input(self, structure=None) -> VaspInput: + """Get a VaspInput object. + + Returns: + VaspInput. + """ + return self.get_input_set(structure=structure) + + def write_input( + self, + output_dir: str, + make_dir_if_not_present: bool = True, + include_cif: bool | str = False, + potcar_spec: bool = False, + zip_output: bool | str = False, + ) -> None: + """ + Writes a set of VASP input to a directory. + + Args: + output_dir (str): Directory to output the VASP input files + make_dir_if_not_present (bool): Set to True if you want the + directory (and the whole path) to be created if it is not + present. + include_cif (bool): Whether to write a CIF file in the output + directory for easier opening by VESTA. + potcar_spec (bool): Instead of writing the POTCAR, write a "POTCAR.spec". + 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. + zip_output (bool): If True, output will be zipped into a file with the + same name as the InputSet (e.g., MPStaticSet.zip). + """ + vasp_input = self.get_input_set(potcar_spec=potcar_spec) + + cif_name = None + if include_cif: + struct = vasp_input["POSCAR"].structure + cif_name = f"{output_dir}/{struct.formula.replace(' ', '')}.cif" + + vasp_input.write_input( + output_dir=output_dir, + make_dir_if_not_present=make_dir_if_not_present, + cif_name=cif_name, + zip_name=f"{type(self).__name__}.zip" if zip_output else None, + files_to_transfer=self.files_to_transfer, + ) + + def as_dict(self, verbosity=2): + """ + Args: + verbosity: Verbosity for generated dict. If 1, structure is + excluded. + + Returns: + dict: MSONable VaspInputSet representation. + """ + dct = MSONable.as_dict(self) + if verbosity == 1: + dct.pop("structure", None) + return dct @property # type: ignore def structure(self) -> Structure: @@ -521,7 +444,7 @@ def get_input_set( VaspInput: A VASP input object. """ if structure is None and prev_dir is None and self.structure is None: - raise ValueError("Either structure or prev_dir must be set.") + raise ValueError("Either structure or prev_dir must be set") self._set_previous(prev_dir) @@ -532,7 +455,8 @@ def get_input_set( incar=self.incar, kpoints=self.kpoints, poscar=self.poscar, - potcar=self.potcar_symbols if potcar_spec else self.potcar, + potcar="\n".join(self.potcar_symbols) if potcar_spec else self.potcar, + potcar_spec=potcar_spec, ) @property @@ -567,6 +491,10 @@ def _set_previous(self, prev_dir: str | Path | None = None): bs = vasprun.get_band_structure(efermi="smart") self.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. + self._ispin = _get_ispin(vasprun, outcar) + self.structure = get_structure_from_prev_run(vasprun, outcar) @property @@ -584,9 +512,12 @@ def incar(self) -> Incar: incar_updates = self.incar_updates settings = dict(self._config_dict["INCAR"]) auto_updates = {} + if self.auto_ispin and (self._ispin is not None): + auto_updates["ISPIN"] = self._ispin + # breaking change - order in which settings applied inconsistent with atomate2 # apply updates from input set generator to SETTINGS - _apply_incar_updates(settings, incar_updates) + # _apply_incar_updates(settings, incar_updates) # apply user incar settings to SETTINGS not to INCAR _apply_incar_updates(settings, self.user_incar_settings) @@ -604,7 +535,9 @@ def incar(self) -> Incar: if key == "MAGMOM": mag = [] for site in structure: - if hasattr(site, "magmom"): + if uic_magmom := self.user_incar_settings.get("MAGMOM", {}).get(site.species_string): + mag.append(uic_magmom) + elif hasattr(site, "magmom"): mag.append(site.magmom) elif getattr(site.specie, "spin", None) is not None: mag.append(site.specie.spin) @@ -644,7 +577,7 @@ def incar(self) -> Incar: incar["EDIFF"] = float(setting) * len(structure) else: incar["EDIFF"] = float(settings["EDIFF"]) - elif key == "KSPACING" and setting == "auto": + elif key == "KSPACING" and self.auto_kspacing: # default to metal if no prev calc available bandgap = 0 if self.bandgap is None else self.bandgap incar[key] = auto_kspacing(bandgap, self.bandgap_tol) @@ -714,12 +647,29 @@ def incar(self) -> Incar: if self.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=2, SIGMA=0.2) + auto_updates.update(ISMEAR=0, SIGMA=0.2) elif self.bandgap <= self.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.update(LREAL=_get_recommended_lreal(structure)) + + # 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 input set generator to INCAR + _apply_incar_updates(incar, incar_updates, 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 + _apply_incar_updates(incar, self.user_incar_settings, skip=["LDAUU", "LDAUJ", "LDAUL", "MAGMOM"]) + + # Remove unused INCAR parameters + _remove_unused_incar_params(incar, skip=list(self.user_incar_settings)) + kpoints = self.kpoints if kpoints is not None: # unset KSPACING as we are using a KPOINTS file @@ -729,12 +679,6 @@ def incar(self) -> Incar: # TODO: Is that we actually want to do? Copied from current pymatgen inputsets incar["KSPACING"] = prev_incar["KSPACING"] - # apply updates from auto options, careful not to override user_incar_settings - _apply_incar_updates(incar, auto_updates, skip=list(self.user_incar_settings)) - - # Remove unused INCAR parameters - _remove_unused_incar_params(incar, skip=list(self.user_incar_settings)) - # 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 @@ -743,7 +687,7 @@ def incar(self) -> Incar: if kpoints is not None and np.prod(kpoints.kpts) < 4 and incar.get("ISMEAR", 0) == -5: incar["ISMEAR"] = 0 - if self.user_incar_settings.get("KSPACING", 0) > 0.5 and incar.get("ISMEAR", 0) == -5: + if incar.get("KSPACING", 0) > 0.5 and incar.get("ISMEAR", 0) == -5: warnings.warn( "Large KSPACING value detected with ISMEAR = -5. Ensure that VASP " "generates an adequate number of KPOINTS, lower KSPACING, or " @@ -758,14 +702,13 @@ def incar(self) -> Incar: and 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}).", + f"{msg} See VASP recommendations on ISMEAR for metals (https://www.vasp.at/wiki/index.php/ISMEAR).", BadInputSetWarning, stacklevel=1, ) @@ -777,7 +720,14 @@ def poscar(self) -> Poscar: """Poscar""" if self.structure is None: raise RuntimeError("No structure is associated with the input set!") - return Poscar(self.structure) + site_properties = self.structure.site_properties + return Poscar( + self.structure, + velocities=site_properties.get("velocities"), + predictor_corrector=site_properties.get("predictor_corrector"), + predictor_corrector_preamble=self.structure.properties.get("predictor_corrector_preamble"), + lattice_velocities=self.structure.properties.get("lattice_velocities"), + ) @property def potcar_functional(self) -> UserPotcarFunctional: @@ -795,9 +745,7 @@ def nelect(self) -> float: num_atoms * n_electrons_by_element[el.symbol] for el, num_atoms in self.structure.composition.items() ) - if self.use_structure_charge: - return n_elect - self.structure.charge - return n_elect + return n_elect - (self.structure.charge if self.use_structure_charge else 0) @property def kpoints(self) -> Kpoints | None: @@ -950,7 +898,39 @@ def potcar(self) -> Potcar: """The input set's POTCAR.""" if self.structure is None: raise RuntimeError("No structure is associated with the input set!") - return super().potcar + + user_potcar_functional = self.user_potcar_functional + potcar = Potcar(self.potcar_symbols, functional=user_potcar_functional) + + # warn if the selected POTCARs do not correspond to the chosen user_potcar_functional + for p_single in potcar: + if user_potcar_functional not in p_single.identify_potcar()[0]: + warnings.warn( + f"POTCAR data with symbol {p_single.symbol} is not known by pymatgen to " + f"correspond with the selected {user_potcar_functional=}. This POTCAR " + f"is known to correspond with functionals {p_single.identify_potcar(mode='data')[0]}. " + "Please verify that you are using the right POTCARs!", + BadInputSetWarning, + ) + + return potcar + + @property + def potcar_symbols(self): + """List of POTCAR symbols.""" + + elements = self.poscar.site_symbols + potcar_symbols = [] + settings = self._config_dict["POTCAR"] + + if isinstance(settings[elements[-1]], dict): + for el in elements: + potcar_symbols.append(settings[el]["symbol"] if el in settings else el) + else: + for el in elements: + potcar_symbols.append(settings.get(el, el)) + + return potcar_symbols def estimate_nbands(self) -> int: """Estimate the number of bands that VASP will initialize a @@ -1047,42 +1027,6 @@ def __str__(self) -> str: def __repr__(self) -> str: return type(self).__name__ - def write_input( - self, - output_dir: str, - make_dir_if_not_present: bool = True, - include_cif: bool = False, - potcar_spec: bool = False, - zip_output: bool = False, - ): - """ - Writes out all input to a directory. - - Args: - output_dir (str): Directory to output the VASP input files - make_dir_if_not_present (bool): Set to True if you want the - directory (and the whole path) to be created if it is not - present. - include_cif (bool): Whether to write a CIF file in the output - directory for easier opening by VESTA. - potcar_spec (bool): Instead of writing the POTCAR, write a "POTCAR.spec". - 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. - zip_output (bool): Whether to zip each VASP input file written to the output directory. - """ - super().write_input( - output_dir=output_dir, - make_dir_if_not_present=make_dir_if_not_present, - include_cif=include_cif, - potcar_spec=potcar_spec, - zip_output=zip_output, - ) - for k, v in self.files_to_transfer.items(): - with zopen(v, "rb") as fin, zopen(str(Path(output_dir) / k), "wb") as fout: - shutil.copyfileobj(fin, fout) - def calculate_ng( self, max_prime_factor: int = 7, @@ -1149,6 +1093,70 @@ def next_g_size(cur_g_size): return ng_vec, [ng_ * finer_g_scale for ng_ in ng_vec] + @staticmethod + def from_directory(directory: str | Path, optional_files: dict | None = None) -> VaspInput: + """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.upper()] = obj.from_file(directory / name) # type: ignore[attr-defined] + else: + # handle the case where there is no KPOINTS file + inputs[name.upper()] = None + + optional_inputs = {} + if optional_files is not None: + for name, obj in optional_files.items(): + optional_inputs[str(name)] = obj.from_file(directory / name) # type: ignore[attr-defined] + + return VaspInput( + incar=inputs["INCAR"], + kpoints=inputs["KPOINTS"], + poscar=inputs["POSCAR"], + potcar=inputs["POTCAR"], + optional_files=optional_inputs, # type: ignore[arg-type] + ) + + def _get_nedos(self, dedos: float) -> int: + """Automatic setting of nedos using the energy range and the energy step.""" + if self.prev_vasprun is None: + return 2000 + + emax = max(eigs.max() for eigs in self.prev_vasprun.eigenvalues.values()) + emin = min(eigs.min() for eigs in self.prev_vasprun.eigenvalues.values()) + return int((emax - emin) / dedos) + + +# create VaspInputGenerator alias to follow atomate2 terminology +VaspInputGenerator = VaspInputSet + + +class DictSet(VaspInputSet): + """Alias for VaspInputSet.""" + + def __post_init__(self): + super().__post_init__() + warnings.warn( + "DictSet is deprecated, and will be removed on 2025-12-31\n; use VaspInputSet", + category=FutureWarning, + stacklevel=2, + ) + # Helper functions to determine valid FFT grids for VASP def next_num_with_prime_factors(n: int, max_prime_factor: int, must_inc_2: bool = True) -> int: @@ -1194,7 +1202,7 @@ def primes_less_than(max_val: int) -> list[int]: description="A high-throughput infrastructure for density functional theory calculations", ) @dataclass -class MITRelaxSet(DictSet): +class MITRelaxSet(VaspInputSet): """ Standard implementation of VaspInputSet utilizing parameters in the MIT High-throughput project. @@ -1205,7 +1213,7 @@ class MITRelaxSet(DictSet): structure (Structure): The Structure to create inputs for. If None, the input set is initialized without a Structure but one must be set separately before the inputs are generated. - **kwargs: Same as those supported by DictSet. + **kwargs: Keywords supported by VaspInputSet. Please refer:: @@ -1219,7 +1227,7 @@ class MITRelaxSet(DictSet): @dataclass -class MPRelaxSet(DictSet): +class MPRelaxSet(VaspInputSet): """ Implementation of VaspInputSet utilizing parameters in the public Materials Project. Typically, the pseudopotentials chosen contain more @@ -1231,7 +1239,7 @@ class MPRelaxSet(DictSet): structure (Structure): The Structure to create inputs for. If None, the input set is initialized without a Structure but one must be set separately before the inputs are generated. - **kwargs: Same as those supported by DictSet. + **kwargs: Keywords supported by VaspInputSet. """ CONFIG = _load_yaml_config("MPRelaxSet") @@ -1250,7 +1258,7 @@ class MPRelaxSet(DictSet): description="Efficient generation of generalized Monkhorst-Pack grids through the use of informatics", ) @dataclass -class MPScanRelaxSet(DictSet): +class MPScanRelaxSet(VaspInputSet): """Write a relaxation input set using the accurate and numerically efficient r2SCAN variant of the Strongly Constrained and Appropriately Normed (SCAN) metaGGA density functional. @@ -1288,7 +1296,7 @@ class MPScanRelaxSet(DictSet): van der Waals density functional by combing the SCAN functional with the rVV10 non-local correlation functional. rvv10 is the only dispersion correction available for SCAN at this time. - **kwargs: Same as those supported by DictSet. + **kwargs: Keywords supported by VaspInputSet. References: [1] P. Wisesa, K.A. McGill, T. Mueller, Efficient generation of @@ -1302,10 +1310,11 @@ class MPScanRelaxSet(DictSet): """ bandgap: float | None = None + auto_kspacing: bool = True user_potcar_functional: UserPotcarFunctional = "PBE_54" auto_ismear: bool = True CONFIG = _load_yaml_config("MPSCANRelaxSet") - _valid_potcars = ("PBE_52", "PBE_54") + _valid_potcars: Sequence[str] | None = ("PBE_52", "PBE_54") def __post_init__(self): super().__post_init__() @@ -1316,14 +1325,9 @@ def __post_init__(self): for k in vdw_par[self.vdw]: self._config_dict["INCAR"].pop(k, None) - @property - def incar_updates(self) -> dict: - """Updates to the INCAR config for this calculation type.""" - return {"KSPACING": "auto"} # enable automatic KSPACING based on band gap - @dataclass -class MPMetalRelaxSet(DictSet): +class MPMetalRelaxSet(VaspInputSet): """ Implementation of VaspInputSet utilizing parameters in the public Materials Project, but with tuning for metals. Key things are a denser @@ -1344,14 +1348,14 @@ def kpoints_updates(self) -> dict | Kpoints: @dataclass -class MPHSERelaxSet(DictSet): +class MPHSERelaxSet(VaspInputSet): """Same as the MPRelaxSet, but with HSE parameters.""" CONFIG = _load_yaml_config("MPHSERelaxSet") @dataclass -class MPStaticSet(DictSet): +class MPStaticSet(VaspInputSet): """Create input files for a static calculation. Args: @@ -1366,7 +1370,7 @@ class MPStaticSet(DictSet): small_gap_multiply ([float, float]): If the gap is less than 1st index, multiply the default reciprocal_density by the 2nd index. - **kwargs: kwargs supported by MPRelaxSet. + **kwargs: Keywords supported by MPRelaxSet. """ lepsilon: bool = False @@ -1411,7 +1415,7 @@ def kpoints_updates(self) -> dict | Kpoints: @dataclass -class MatPESStaticSet(DictSet): +class MatPESStaticSet(VaspInputSet): """Create input files for a MatPES static calculation. The goal of MatPES is to generate potential energy surface data. This is a distinctly different @@ -1429,7 +1433,7 @@ class MatPESStaticSet(DictSet): set is initialized without a Structure but one must be set separately before the inputs are generated. xc_functional ('R2SCAN'|'PBE'): Exchange-correlation functional to use. Defaults to 'PBE'. - **kwargs: Same as those supported by DictSet. + **kwargs: Keywords supported by VaspInputSet. """ xc_functional: Literal["R2SCAN", "PBE", "PBE+U"] = "PBE" @@ -1471,22 +1475,17 @@ def __post_init__(self): f"Supported exchange-correlation functionals are {valid_xc_functionals}" ) - default_potcars = self.CONFIG["PARENT"].replace("PBE", "PBE_").replace("BASE", "") # PBE64BASE -> PBE_64 + default_potcars = self.CONFIG["PARENT"].replace("PBE", "PBE_").replace("Base", "") # PBE64Base -> PBE_64 self.user_potcar_functional = self.user_potcar_functional or default_potcars if self.user_potcar_functional.upper() != default_potcars: warnings.warn( f"{self.user_potcar_functional=} is inconsistent with the recommended {default_potcars}.", UserWarning ) - @property - def incar_updates(self) -> dict: - """Updates to the INCAR config for this calculation type.""" - updates: dict[str, Any] = {} if self.xc_functional.upper() == "R2SCAN": - updates.update({"METAGGA": "R2SCAN", "ALGO": "ALL", "GGA": None}) + self._config_dict["INCAR"].update({"METAGGA": "R2SCAN", "ALGO": "ALL", "GGA": None}) if self.xc_functional.upper().endswith("+U"): - updates["LDAU"] = True - return updates + self._config_dict["INCAR"]["LDAU"] = True @dataclass @@ -1502,12 +1501,13 @@ class MPScanStaticSet(MPScanRelaxSet): lepsilon (bool): Whether to add static dielectric calculation lcalcpol (bool): Whether to turn on evaluation of the Berry phase approximations for electronic polarization. - **kwargs: kwargs supported by MPScanRelaxSet. + **kwargs: Keywords supported by MPScanRelaxSet. """ lepsilon: bool = False lcalcpol: bool = False inherit_incar: bool = True + auto_kspacing: bool = True @property def incar_updates(self) -> dict: @@ -1518,7 +1518,6 @@ def incar_updates(self) -> dict: "LORBIT": 11, "LVHAR": True, "ISMEAR": -5, - "KSPACING": "auto", } if self.lepsilon: @@ -1534,7 +1533,7 @@ def incar_updates(self) -> dict: @dataclass -class MPHSEBSSet(DictSet): +class MPHSEBSSet(VaspInputSet): """ Implementation of a VaspInputSet for HSE band structure computations. @@ -1572,7 +1571,7 @@ class MPHSEBSSet(DictSet): nbands_factor (float): Multiplicative factor for NBANDS when starting from a previous calculation. Choose a higher number if you are doing an LOPTICS calculation. - **kwargs (dict): Any other parameters to pass into DictSet. + **kwargs: Keywords supported by VaspInputSet. """ added_kpoints: list[Vector3D] = field(default_factory=list) @@ -1655,7 +1654,7 @@ def incar_updates(self) -> dict: @dataclass -class MPNonSCFSet(DictSet): +class MPNonSCFSet(VaspInputSet): """ Init a MPNonSCFSet. Typically, you would use the classmethod from_prev_calc to initialize from a previous SCF run. @@ -1679,7 +1678,7 @@ class MPNonSCFSet(DictSet): small_gap_multiply ([float, float]): When starting from a previous calculation, if the gap is less than 1st index, multiply the default reciprocal_density by the 2nd index. - **kwargs: kwargs supported by MPRelaxSet. + **kwargs: Keywords supported by MPRelaxSet. """ mode: str = "line" @@ -1771,7 +1770,7 @@ def kpoints_updates(self) -> dict: @dataclass -class MPSOCSet(DictSet): +class MPSOCSet(VaspInputSet): """An input set for running spin-orbit coupling (SOC) calculations. Args: @@ -1790,7 +1789,7 @@ class MPSOCSet(DictSet): lcalcpol (bool): Whether to turn on evaluation of the Berry phase approximations for electronic polarization magmom (list[list[float]]): Override for the structure magmoms. - **kwargs: kwargs supported by DictSet. + **kwargs: Keywords supported by VaspInputSet. """ saxis: tuple[int, int, int] = (0, 0, 1) @@ -1854,7 +1853,7 @@ def kpoints_updates(self) -> dict: factor = self.small_gap_multiply[1] return {"reciprocal_density": self.reciprocal_density * factor} - @DictSet.structure.setter # type: ignore + @VaspInputSet.structure.setter # type: ignore def structure(self, structure: Structure | None) -> None: if structure is not None: if self.magmom: @@ -1868,11 +1867,11 @@ def structure(self, structure: Structure | None) -> None: else: raise ValueError("Neither the previous structure has magmom property nor magmom provided") - DictSet.structure.fset(self, structure) # type: ignore + VaspInputSet.structure.fset(self, structure) # type: ignore @dataclass -class MPNMRSet(DictSet): +class MPNMRSet(VaspInputSet): """Init a MPNMRSet. Args: @@ -1892,7 +1891,7 @@ class MPNMRSet(DictSet): small_gap_multiply ([float, float]): If the gap is less than 1st index, multiply the default reciprocal_density by the 2nd index. - **kwargs: kwargs supported by MPRelaxSet. + **kwargs: Keywords supported by MPRelaxSet. """ mode: Literal["cs", "efg"] = "cs" @@ -1950,7 +1949,7 @@ def kpoints_updates(self) -> dict: Doi("10.1149/2.0061602jes"), description="Elastic Properties of Alkali Superionic Conductor Electrolytes from First Principles Calculations", ) -class MVLElasticSet(DictSet): +class MVLElasticSet(VaspInputSet): """ MVL denotes VASP input sets that are implemented by the Materials Virtual Lab (http://materialsvirtuallab.org) for various research. @@ -1983,7 +1982,7 @@ def incar_updates(self) -> dict: @dataclass -class MVLGWSet(DictSet): +class MVLGWSet(VaspInputSet): """ MVL denotes VASP input sets that are implemented by the Materials Virtual Lab (http://materialsvirtuallab.org) for various research. This is a @@ -2017,7 +2016,7 @@ class MVLGWSet(DictSet): ncores (int): Numbers of cores used for the calculation. VASP will alter NBANDS if it was not dividable by ncores. Only applies if mode=="DIAG". - **kwargs: All kwargs supported by DictSet. Typically, + **kwargs: All kwargs supported by VaspInputSet. Typically, user_incar_settings is a commonly used option. """ @@ -2092,7 +2091,7 @@ def from_prev_calc(cls, prev_calc_dir: str, mode: str = "DIAG", **kwargs) -> Sel @dataclass -class MVLSlabSet(DictSet): +class MVLSlabSet(VaspInputSet): """Write a set of slab vasp runs, including both slabs (along the c direction) and orient unit cells (bulk), to ensure the same KPOINTS, POTCAR and INCAR criterion. @@ -2104,7 +2103,7 @@ class MVLSlabSet(DictSet): auto_dipole: set_mix: sort_structure: - **kwargs: Other kwargs supported by DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ k_product: int = 50 @@ -2168,7 +2167,7 @@ def as_dict(self, verbosity=2): @dataclass -class MVLGBSet(DictSet): +class MVLGBSet(VaspInputSet): """Write a vasp input files for grain boundary calculations, slab or bulk. Args: @@ -2230,7 +2229,7 @@ def incar_updates(self) -> dict: @dataclass -class MVLRelax52Set(DictSet): +class MVLRelax52Set(VaspInputSet): """ Implementation of VaspInputSet utilizing the public Materials Project parameters for INCAR & KPOINTS and VASP's recommended PAW potentials for @@ -2247,15 +2246,15 @@ class MVLRelax52Set(DictSet): Args: structure (Structure): input structure. user_potcar_functional (str): choose from "PBE_52" and "PBE_54". - **kwargs: Other kwargs supported by DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ user_potcar_functional: UserPotcarFunctional = "PBE_52" CONFIG = _load_yaml_config("MVLRelax52Set") - _valid_potcars = ("PBE_52", "PBE_54") + _valid_potcars: Sequence[str] | None = ("PBE_52", "PBE_54") -class MITNEBSet(DictSet): +class MITNEBSet(VaspInputSet): """Write NEB inputs. Note that EDIFF is not on a per atom basis for this input set. @@ -2266,7 +2265,7 @@ def __init__(self, structures, unset_encut=False, **kwargs) -> None: Args: structures: List of Structure objects. unset_encut (bool): Whether to unset ENCUT. - **kwargs: Other kwargs supported by DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ if len(structures) < 3: raise ValueError(f"You need at least 3 structures for an NEB, got {len(structures)}") @@ -2362,7 +2361,7 @@ def write_input( @dataclass -class MITMDSet(DictSet): +class MITMDSet(VaspInputSet): """Write a VASP MD run. This DOES NOT do multiple stage runs. Args: @@ -2374,7 +2373,7 @@ class MITMDSet(DictSet): parameter. Defaults to 2fs. spin_polarized (bool): Whether to do spin polarized calculations. The ISPIN parameter. Defaults to False. - **kwargs: Other kwargs supported by DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ structure: Structure | None = None @@ -2425,7 +2424,7 @@ def kpoints_updates(self) -> Kpoints | dict: @dataclass -class MPMDSet(DictSet): +class MPMDSet(VaspInputSet): """ This a modified version of the old MITMDSet pre 2018/03/12. @@ -2449,7 +2448,7 @@ class MPMDSet(DictSet): for hydrogen containing structures. spin_polarized (bool): Whether to do spin polarized calculations. The ISPIN parameter. Defaults to False. - **kwargs: Other kwargs supported by DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ start_temp: float = 0.0 @@ -2510,7 +2509,7 @@ def kpoints_updates(self) -> dict | Kpoints: @dataclass -class MVLNPTMDSet(DictSet): +class MVLNPTMDSet(VaspInputSet): """Write a VASP MD run in NPT ensemble. Notes: @@ -2574,7 +2573,7 @@ def kpoints_updates(self) -> Kpoints | dict: @dataclass -class MVLScanRelaxSet(DictSet): +class MVLScanRelaxSet(VaspInputSet): """Write a relax input set using Strongly Constrained and Appropriately Normed (SCAN) semilocal density functional. @@ -2596,17 +2595,17 @@ class MVLScanRelaxSet(DictSet): vdw (str): set "rVV10" to enable SCAN+rVV10, which is a versatile van der Waals density functional by combing the SCAN functional with the rVV10 non-local correlation functional. - **kwargs: Other kwargs supported by DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ user_potcar_functional: UserPotcarFunctional = "PBE_52" - _valid_potcars = ("PBE_52", "PBE_54") + _valid_potcars: Sequence[str] | None = ("PBE_52", "PBE_54") CONFIG = MPRelaxSet.CONFIG def __post_init__(self): super().__post_init__() if self.user_potcar_functional not in ("PBE_52", "PBE_54"): - raise ValueError("SCAN calculations required PBE_52 or PBE_54!") + raise ValueError("SCAN calculations require PBE_52 or PBE_54!") @property def incar_updates(self) -> dict: @@ -2626,7 +2625,7 @@ def incar_updates(self) -> dict: @dataclass -class LobsterSet(DictSet): +class LobsterSet(VaspInputSet): """Input set to prepare VASP runs that can be digested by Lobster (See cohp.de). Args: @@ -2642,7 +2641,7 @@ class LobsterSet(DictSet): 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 DictSet. + **kwargs: Other kwargs supported by VaspInputSet. """ isym: int = 0 @@ -2656,7 +2655,7 @@ class LobsterSet(DictSet): user_potcar_functional: UserPotcarFunctional = "PBE_54" CONFIG = MPRelaxSet.CONFIG - _valid_potcars = ("PBE_52", "PBE_54") + _valid_potcars: Sequence[str] | None = ("PBE_52", "PBE_54") def __post_init__(self): super().__post_init__() @@ -2943,7 +2942,7 @@ def get_valid_magmom_struct(structure: Structure, inplace: bool = True, spin_mod @dataclass -class MPAbsorptionSet(DictSet): +class MPAbsorptionSet(VaspInputSet): """ MP input set for generating frequency dependent dielectrics. @@ -2970,7 +2969,7 @@ class MPAbsorptionSet(DictSet): nkred: the reduced number of kpoints to calculate, equal to the k-mesh. Only applies in "RPA" mode because of the q->0 limit. nedos: the density of DOS, default: 2001. - **kwargs: All kwargs supported by DictSet. Typically, user_incar_settings is a + **kwargs: All kwargs supported by VaspInputSet. Typically, user_incar_settings is a commonly used option. """ @@ -3056,6 +3055,11 @@ def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None) -> int: 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 _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: """Combine multiple Kpoints objects.""" _labels: list[list[str]] = [] diff --git a/tests/io/vasp/test_inputs.py b/tests/io/vasp/test_inputs.py index f87e4453818..e99ecc9bc25 100644 --- a/tests/io/vasp/test_inputs.py +++ b/tests/io/vasp/test_inputs.py @@ -1364,6 +1364,20 @@ def test_from_directory(self): vasp_input = VaspInput.from_dict(vi.as_dict()) assert "CONTCAR_Li2O" in vasp_input + def test_input_attr(self): + assert all(v == getattr(self.vasp_input, k.lower()) for k, v in self.vasp_input.items()) + + vis_potcar_spec = VaspInput( + self.vasp_input.incar, + self.vasp_input.kpoints, + self.vasp_input.poscar, + "\n".join(self.vasp_input.potcar.symbols), + potcar_spec=True, + ) + assert all(k in vis_potcar_spec for k in ("INCAR", "KPOINTS", "POSCAR", "POTCAR.spec")) + assert all(self.vasp_input[k] == getattr(vis_potcar_spec, k.lower()) for k in ("INCAR", "KPOINTS", "POSCAR")) + assert isinstance(vis_potcar_spec.potcar, str) + def test_potcar_summary_stats() -> None: potcar_summary_stats = loadfn(POTCAR_STATS_PATH) diff --git a/tests/io/vasp/test_sets.py b/tests/io/vasp/test_sets.py index c296e81307d..fe181ae578d 100644 --- a/tests/io/vasp/test_sets.py +++ b/tests/io/vasp/test_sets.py @@ -128,7 +128,7 @@ def test_sets_changed(self): assert hashes[input_set] == known_hashes[input_set], f"{input_set=}\n{msg}" -class TestDictSet(PymatgenTest): +class TestVaspInputSet(PymatgenTest): @classmethod def setUpClass(cls): filepath = f"{VASP_IN_DIR}/POSCAR" @@ -136,7 +136,7 @@ def setUpClass(cls): def test_as_dict(self): # https://github.com/materialsproject/pymatgen/pull/3031 - dict_set = DictSet(self.structure, config_dict={"INCAR": {}}, user_potcar_functional="PBE_54") + dict_set = VaspInputSet(self.structure, config_dict={"INCAR": {}}, user_potcar_functional="PBE_54") assert {*dict_set.as_dict()} >= { "@class", "@module", @@ -228,7 +228,9 @@ def test_warnings(self): ) as warns_kspacing: vis = self.set(structure, user_incar_settings={"KSPACING": 1, "ISMEAR": -5}) _ = vis.incar - assert len(warns_kspacing) == 3 + for warn in warns_kspacing: + print("scoots:", warn.message) + assert len(warns_kspacing) == 2 def test_poscar(self): structure = Structure(self.lattice, ["Fe", "Mn"], self.coords) @@ -646,6 +648,29 @@ def test_valid_magmom_struct(self): ) vis.incar.items() + def test_write_input_and_from_directory(self): + structure = Structure.from_spacegroup("Fm-3m", Lattice.cubic(4.0), ["Fe"], [[0.0, 0.0, 0.0]]) + + vis = self.set(structure=structure) + input_set = vis.get_input_set() + + vis.write_input(output_dir=".") + assert all(os.path.isfile(file) for file in ("INCAR", "KPOINTS", "POSCAR", "POTCAR")) + input_set_from_dir = self.set().from_directory(".") + + assert all(input_set_from_dir[k] == input_set[k] for k in ("INCAR", "KPOINTS", "POTCAR")) + # for some reason the POSCARs are not identical, but their structures and as_dict()'s are + assert input_set_from_dir["POSCAR"].structure == input_set["POSCAR"].structure + assert input_set_from_dir["POSCAR"].as_dict() == input_set["POSCAR"].as_dict() + + def test_get_nedos(self): + vrun = Vasprun(f"{VASP_OUT_DIR}/vasprun.pbesol.xml.gz") + vis = self.set(structure=vrun.structures[-1]) + # no `prev_vasprun` --> default value of NEDOS + assert vis._get_nedos(0.1) == 2000 + vis.prev_vasprun = vrun + assert vis._get_nedos(0.1) == pytest.approx(741, abs=1) + class TestMPStaticSet(PymatgenTest): def setUp(self): @@ -1578,7 +1603,7 @@ def test_potcar(self): assert input_set.potcar.functional == "PBE_52" with pytest.raises( - ValueError, match=r"Invalid self.user_potcar_functional='PBE', must be one of \('PBE_52', 'PBE_54'\)" + ValueError, match=r"Invalid user_potcar_functional='PBE', must be one of \('PBE_52', 'PBE_54'\)" ): MVLScanRelaxSet(self.struct, user_potcar_functional="PBE") @@ -1595,7 +1620,7 @@ def test_potcar(self): # # # https://github.com/materialsproject/pymatgen/pull/3022 # # same test also in MITMPRelaxSetTest above (for redundancy, - # # should apply to all classes inheriting from DictSet) + # # should apply to all classes inheriting from VaspInputSet) # for user_potcar_settings in [{"Fe": "Fe_pv"}, {"W": "W_pv"}, None]: # for species in [("W", "W"), ("Fe", "W"), ("Fe", "Fe")]: # struct = Structure(lattice=Lattice.cubic(3), species=species, coords=[[0, 0, 0], [0.5, 0.5, 0.5]]) @@ -1631,9 +1656,9 @@ def test_incar(self): assert incar["ENAUG"] == 1360 assert incar["ENCUT"] == 680 assert incar["NSW"] == 500 - # the default POTCAR contains metals + # the default POTCAR contains metals, but no prev calc set --> bandgap unknown assert incar["KSPACING"] == 0.22 - assert incar["ISMEAR"] == 2 + assert incar["ISMEAR"] == 0 assert incar["SIGMA"] == 0.2 # https://github.com/materialsproject/pymatgen/pull/3036 @@ -1708,7 +1733,7 @@ def test_potcar(self): assert input_set.potcar.functional == "PBE_54" with pytest.raises( - ValueError, match=r"Invalid self.user_potcar_functional='PBE', must be one of \('PBE_52', 'PBE_54'\)" + ValueError, match=r"Invalid user_potcar_functional='PBE', must be one of \('PBE_52', 'PBE_54'\)" ): MPScanRelaxSet(self.struct, user_potcar_functional="PBE") @@ -1878,7 +1903,7 @@ def test_potcar(self): assert test_potcar_set_1.potcar.functional == "PBE_52" with pytest.raises( - ValueError, match=r"Invalid self.user_potcar_functional='PBE', must be one of \('PBE_52', 'PBE_54'\)" + ValueError, match=r"Invalid user_potcar_functional='PBE', must be one of \('PBE_52', 'PBE_54'\)" ): self.set(self.struct, user_potcar_functional="PBE") @@ -2092,3 +2117,11 @@ def test_as_from_dict(self): def test_vasp_input_set_alias(): assert VaspInputSet is VaspInputGenerator + + +def test_dict_set_alias(): + with pytest.warns( + FutureWarning, match="DictSet is deprecated, and will be removed on 2025-12-31\n; use VaspInputSet" + ): + DictSet() + assert isinstance(DictSet(), VaspInputSet)