Source code for IMDgroup.pymatgen.io.vasp.inputs

# MIT License
#
# Copyright (c) 2024-2025 Inverse Materials Design Group
#
# Author: Ihor Radchenko <yantar92@posteo.net>
#
# This file is a part of IMDgroup-pymatgen package
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


"""This module implements IMD group-specific extensions to pymatgen.io.vasp.inputs module."""

import os
from monty.serialization import loadfn
from pymatgen.io.vasp.inputs import Incar as pmgIncar
from IMDgroup.common import groupby_cmp


MODULE_DIR = os.path.dirname(__file__)


# We copy this over from pymatgen.io.vasp.sets because we need our own
# MODULE_DIR
def _load_yaml_config(fname):
    config = loadfn(f"{MODULE_DIR}/{fname}.yaml")
    if "PARENT" in config:
        parent_config = _load_yaml_config(config["PARENT"])
        for k, v in parent_config.items():
            if k not in config:
                config[k] = v
            elif isinstance(v, dict):
                v_new = config.get(k, {})
                v_new.update(v)
                config[k] = v_new
    return config


[docs] class Incar(pmgIncar): """Modified version of pymatgen's Incar class. Extensions: 1. Readable constants for INCAR values (ISIF_*, IBRION_*). 2. Warning when ``IBRION=-1`` and ``NSW>0`` (useless combination). 3. Methods to retrieve standard setting combinations. """ # ISIF values ISIF_RELAX_POS_FAST = ISIF_FIX_SHAPE_VOL_FAST = 0 ISIF_RELAX_POS_TRACE = ISIF_FIX_SHAPE_VOL_TRACE = 1 ISIF_RELAX_POS = ISIF_FIX_SHAPE_VOL = 2 ISIF_RELAX_POS_SHAPE_VOL = ISIF_FIX_NONE = 3 ISIF_RELAX_POS_SHAPE = ISIF_FIX_VOL = 4 ISIF_RELAX_SHAPE = IFIX_FIX_POS_VOL = 5 ISIF_RELAX_SHAPE_VOL = ISIF_FIX_POS = 6 ISIF_RELAX_VOL = ISIF_FIX_POS_SHAPE = 7 ISIF_RELAX_POS_VOL = ISIF_FIX_SHAPE = 8 # IBRION values IBRION_NONE = -1 IBRION_MD = 0 IBRION_IONIC_RELAX_FORCE_FAST = 1 IBRION_IONIC_RELAX_CGA = 2 IBRION_IONIC_RELAX_DAMPED_MD = 3 IBRION_IONIC_RELAX_values = [ IBRION_IONIC_RELAX_FORCE_FAST, IBRION_IONIC_RELAX_CGA, IBRION_IONIC_RELAX_DAMPED_MD]
[docs] def image_dir_names(self, include_ends: bool = True) -> list[str] | None: """Return directory names for NEB images. Args: include_ends: When False, exclude the first (``00``) and last image directories. Returns: list[str] | None: Sorted list of directory names, or None when ``IMAGES`` is not set in the INCAR. """ if nimages := self.get('IMAGES'): rng = range(0, nimages + 2) dirs = [f"{n:02d}" for n in rng] return dirs if include_ends else dirs[1:-1] return None
# FIXME: This should better be contributed upstream as I cannot # override the checks in the Incar instances used from pymatgen # internals. # def proc_val(self, key: str, val: str) -> list | bool | float | int | str: # """Helper method to convert INCAR parameters to proper types # like ints, floats, lists, etc. # Args: # key (str): INCAR parameter key. # val (str): Value of INCAR parameter. # """ # result = pmgIncar.proc_val(key, val) # if self.get("IBRION", None) == self.IBRION_NONE and\ # self.get("NSW", 0) > 0: # warnings.warn( # f"NSW ({self.get('NSW', "N/A")}) > 0 is useless" # f" with IBRION = {self.IBRION_NONE}", # BadIncarWarning) # return result
[docs] @staticmethod def get_recipe(setup: str, name: str): """Retrieve INCAR settings for a given setup and name. Args: setup: ``"functional"`` to retrieve functional settings. name: Functional name. Supported values: PBE, PBEsol, PBE+D2, PBE+TS, vdW-DF, vdW-DF2, optB88-vdW, optB86b-vdW. Returns: dict: INCAR parameter dictionary. Raises: KeyError: If ``name`` is not a supported functional. ValueError: If ``setup`` is unknown. """ settings = None if setup == "functional": functional_config = _load_yaml_config("functionals") if name not in functional_config: raise KeyError( "Invalid or unsupported functional. " + "Supported functionals are " + ', '.join(functional_config) + "." ) settings = functional_config.get(name) if settings: for key, val in settings.items(): if val == "None": settings[key] = None return settings raise ValueError(f"Unknown setup: {setup}")
[docs] @staticmethod def group_incars(incars, ignore_fields=['SYSTEM', 'NELM', 'NELMIN', 'ALGO', 'SYMPREC']): """Group similar INCARs together. Differences in ``ignore_fields`` are not considered when comparing INCARs. Args: incars: List of Incar objects. ignore_fields: INCAR keys to ignore when grouping. Returns: tuple: ``(common_incar, groups)`` where ``common_incar`` is an Incar with parameters shared by all groups, and ``groups`` is a list of lists of Incar objects. """ def _incar_eq(incar1, incar2): """Return True when INCAR1 is equal to INCAR2. ignore_fileds fields are ignored. """ incar1 = incar1.copy() incar2 = incar2.copy() for f in ignore_fields: if f in incar1: del incar1[f] if f in incar2: del incar2[f] difference = incar1.diff(incar2) diff_fields = difference["Different"] if diff_fields is None: diff_fields = {} # for f in ignore_fields: # diff_fields.pop(f, None) return len(diff_fields) == 0 def _incar_name(incar): """Get INCAR name. Assume that name is stored in SYSTEM parameter. """ return incar['SYSTEM'] groups = groupby_cmp(incars, _incar_eq, _incar_name) common_incar = None for group in groups: if common_incar is None: common_incar = group[0].copy() for f in ignore_fields: common_incar.pop(f, None) else: for key, val in group[0].items(): if common_incar.get(key, None) != val: common_incar.pop(key, None) for key, val in common_incar.copy().items(): if group[0].get(key, None) != val: common_incar.pop(key, None) return (common_incar, groups)