# 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)