diff --git a/src/pymatgen/io/jdftx/__init__.py b/src/pymatgen/io/jdftx/__init__.py new file mode 100644 index 00000000000..5c63a655806 --- /dev/null +++ b/src/pymatgen/io/jdftx/__init__.py @@ -0,0 +1 @@ +# Empty __init__.py file diff --git a/src/pymatgen/io/jdftx/generic_tags.py b/src/pymatgen/io/jdftx/generic_tags.py new file mode 100644 index 00000000000..391e040ee4f --- /dev/null +++ b/src/pymatgen/io/jdftx/generic_tags.py @@ -0,0 +1,1165 @@ +"""Module for class objects for containing JDFTx tags. + +This module contains class objects for containing JDFTx tags. These class objects +are used to validate the type of the value for the tag, read the value string for +the tag, write the tag and its value as a string, and get the token length of the tag. + +@mkhorton - This file is ready to review. +""" + +from __future__ import annotations + +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from dataclasses import dataclass, field +from typing import Any + +import numpy as np + +__author__ = "Jacob Clary, Ben Rich" + + +# This inheritable class is kept for use by AbstractTag instead of using pprint as +# the type conversions for tags are incredibly delicate and require strings to be +# printed in a very exact way. +class ClassPrintFormatter: + """Generic class for printing to command line in readable format. + + Generic class for printing to command line in readable format. + """ + + def __str__(self) -> str: + """Print the class to the command line in a readable format. + + Returns: + str: The class in a readable format. + """ + return f"{self.__class__}\n" + "\n".join(f"{item} = {self.__dict__[item]}" for item in sorted(self.__dict__)) + + +@dataclass +class AbstractTag(ClassPrintFormatter, ABC): + """Abstract base class for all tags.""" + + multiline_tag: bool = ( + False # set to True if what to print tags across multiple lines, typically like electronic-minimize + ) + can_repeat: bool = False # set to True for tags that can appear on multiple lines, like ion + write_tagname: bool = True # set to False to not print the tagname, like for subtags of elec-cutoff + write_value: bool = True # set to False to not print any value, like for dump-interval + optional: bool = ( + True # set to False if tag (usually a subtag of a TagContainer) must be set for the JDFTXInfile to be valid. + ) + # The lattice, ion, and ion-species are the main tags that are not optional + defer_until_struc: bool = False + is_tag_container: bool = False + allow_list_representation: bool = False # if True, allow this tag to exist as a list or list of lists + + @abstractmethod + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value. + Defaults to False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + + def _validate_value_type( + self, type_check: type, tag: str, value: Any, try_auto_type_fix: bool = False + ) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + This method is used to validate the type of the value for this tag. It is used to check if the value is of the + correct type and if it can be fixed automatically. If the value is not of the correct type and cannot be fixed + automatically, a warning is raised. + + Args: + type_check (type): The type to check the value against. + tag (str): The tag to check the value against. + value (Any): The value to check the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value. + Defaults to False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + if self.can_repeat: + self._validate_repeat(tag, value) + is_valid = all(isinstance(x, type_check) for x in value) + else: + is_valid = isinstance(value, type_check) + + if not is_valid and try_auto_type_fix: + try: + value = [self.read(tag, str(x)) for x in value] if self.can_repeat else self.read(tag, str(value)) + tag, is_valid, value = self._validate_value_type(type_check, tag, value) + except (TypeError, ValueError): + warning = f"Could not fix the typing for tag '{tag}'" + try: + warning += f"{value}!" + except (ValueError, TypeError): + warning += "(unstringable value)!" + warnings.warn("warning", stacklevel=2) + return tag, is_valid, value + + def _validate_repeat(self, tag: str, value: Any) -> None: + if not isinstance(value, list): + raise TypeError(f"The '{tag}' tag can repeat but is not a list: '{value}'") + + @abstractmethod + def read(self, tag: str, value_str: str) -> Any: + """Read and parse the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value_str (str): The value string to read. + + Returns: + Any: The parsed value. + """ + + def _general_read_validate(self, tag: str, value_str: Any) -> None: + """General validation for values to be passed to a read method.""" + try: + value = str(value_str) + except (ValueError, TypeError): + value = "(unstringable)" + if not isinstance(value_str, str): + raise TypeError(f"Value '{value}' for '{tag}' should be a string!") + + def _single_value_read_validate(self, tag: str, value: str) -> None: + """Validation for values to be passed to a read method for AbstractTag inheritors that only + read a single value.""" + self._general_read_validate(tag, value) + if len(value.split()) > 1: + raise ValueError(f"'{value}' for '{tag}' should not have a space in it!") + + def _check_unread_values(self, tag: str, unread_values: list[str]) -> None: + """Check for unread values and raise an error if any are found. Used in the read method of TagContainers.""" + if len(unread_values) > 0: + raise ValueError( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {unread_values}" + ) + + def _check_nonoptional_subtags(self, tag: str, subdict: dict[str, Any], subtags: dict[str, AbstractTag]) -> None: + """Check for non-optional subtags and raise an error if any are missing. + Used in the read method of TagContainers.""" + for subtag, subtag_type in subtags.items(): + if not subtag_type.optional and subtag not in subdict: + raise ValueError( + f"The subtag '{subtag}' for tag '{tag}' is not optional but was not populated during the read!" + ) + + @abstractmethod + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + + @abstractmethod + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + + def _write(self, tag: str, value: Any, multiline_override: bool = False, strip_override: bool = False) -> str: + tag_str = f"{tag} " if self.write_tagname else "" + if self.multiline_tag or multiline_override: + tag_str += "\\\n" + if self.write_value: + if not strip_override: + tag_str += f"{value}".strip() + " " + else: + tag_str += f"{value}" + return tag_str + + def _get_token_len(self) -> int: + return int(self.write_tagname) + int(self.write_value) + + def get_list_representation(self, tag: str, value: Any) -> list | list[list]: + """Convert the value to a list representation. + + Args: + tag (str): The tag to convert the value to a list representation for. + value (Any): The value to convert to a list representation. + + Returns: + list | list[list]: The value converted to a list representation. + """ + raise ValueError(f"Tag object with tag '{tag}' has no get_list_representation method") + + def get_dict_representation(self, tag: str, value: Any) -> dict | list[dict]: + """Convert the value to a dict representation. + + Args: + tag (str): The tag to convert the value to a dict representation for. + value (Any): The value to convert to a dict representation. + + Returns: + dict | list[dict]: The value converted to a dict representation. + """ + raise ValueError(f"Tag object with tag '{tag}' has no get_dict_representation method") + + +@dataclass +class BoolTag(AbstractTag): + """Tag for boolean values in JDFTx input files. + + Tag for boolean values in JDFTx input files. + """ + + _TF_read_options: dict[str, bool] = field(default_factory=lambda: {"yes": True, "no": False}) + _TF_write_options: dict[bool, str] = field(default_factory=lambda: {True: "yes", False: "no"}) + _TF_options: dict[str, dict] = field(init=False) + + def __post_init__(self) -> None: + """Initialize the _TF_options attribute. + + Initialize the _TF_options attribute. + """ + self._TF_options = { + "read": self._TF_read_options, + "write": self._TF_write_options, + } + + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value. + Defaults to False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + return self._validate_value_type(bool, tag, value, try_auto_type_fix=try_auto_type_fix) + + def raise_value_error(self, tag: str, value: str) -> None: + """Raise a ValueError for the value string. + + Args: + tag (str): The tag to raise the ValueError for. + value (str): The value string to raise the ValueError for. + """ + raise ValueError(f"The value '{value}' was provided to {tag}, it is not acting like a boolean") + + def read(self, tag: str, value: str) -> bool: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + bool: The parsed boolean value. + """ + self._single_value_read_validate(tag, value) + try: + if not self.write_value: + # accounts for exceptions where only the tagname is used, e.g. + # dump-only or dump-fermi-density (sometimes) tags + if not value: # then the string '' was passed in because no value was provided but the tag was present + value = "yes" + else: + self.raise_value_error(tag, value) + return self._TF_options["read"][value] + except (ValueError, TypeError, KeyError) as err: + raise ValueError(f"Could not set '{value}' as True/False for tag '{tag}'!") from err + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + value2 = self._TF_options["write"][value] + return self._write(tag, value2) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + return self._get_token_len() + + +@dataclass +class StrTag(AbstractTag): + """Tag for string values in JDFTx input files. + + Tag for string values in JDFTx input files. + """ + + options: list | None = None + + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value. Defaults to + False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + return self._validate_value_type(str, tag, value, try_auto_type_fix=try_auto_type_fix) + + def read(self, tag: str, value: str) -> str: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + str: The parsed string value. + """ + self._single_value_read_validate(tag, value) + if self.options is None or value in self.options: + return value + raise ValueError(f"The string value '{value}' must be one of {self.options} for tag '{tag}'") + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + return self._write(tag, value) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + return self._get_token_len() + + +@dataclass +class IntTag(AbstractTag): + """Tag for integer values in JDFTx input files. + + Tag for integer values in JDFTx input files. + """ + + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value, by default + False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + return self._validate_value_type(int, tag, value, try_auto_type_fix=try_auto_type_fix) + + def read(self, tag: str, value: str) -> int: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + int: The parsed integer value. + """ + self._single_value_read_validate(tag, value) + try: + return int(float(value)) + except (ValueError, TypeError) as err: + raise ValueError(f"Could not set value '{value}' to an int for tag '{tag}'!") from err + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + return self._write(tag, value) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + return self._get_token_len() + + +@dataclass +class FloatTag(AbstractTag): + """Tag for float values in JDFTx input files. + + Tag for float values in JDFTx input files. + """ + + prec: int | None = None + + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value, by default + False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + return self._validate_value_type(float, tag, value, try_auto_type_fix=try_auto_type_fix) + + def read(self, tag: str, value: str) -> float: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + float: The parsed float value. + """ + self._single_value_read_validate(tag, value) + try: + value_float = float(value) + except (ValueError, TypeError) as err: + raise ValueError(f"Could not set value '{value}' to a float for tag '{tag}'!") from err + return value_float + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + # pre-convert to string: self.prec+3 is minimum room for: + # - sign, 1 integer left of decimal, decimal, and precision. + # larger numbers auto add places to left of decimal + if self.prec is not None: + value = f"{value:{self.prec + 3}.{self.prec}f}" + return self._write(tag, value) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + return self._get_token_len() + + +@dataclass +class InitMagMomTag(AbstractTag): + """Tag for initial-magnetic-moments tag in JDFTx input files. + + Tag for initial-magnetic-moments tag in JDFTx input files. + """ + + # temporary fix to allow use of initial-magnetic-moments tag + # requires the user to set magnetic moments as a string with no extra + # validation. Processes input files as simply a string variable with + # no extra type conversion + + # the formatting of this tag's value depends on the species labels in the + # ion tags. These species labels are not necessarily element symbols. + # There are also multiple types of formatting options of the spins + # most robustly, this should be a MultiFormatTag, with each option + # being a StructureDeferredTag, because this tag needs to know the + # results of reading in the structure before being able to robustly + # parse the value of this tag + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value, by default + False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + return self._validate_value_type(str, tag, value, try_auto_type_fix=try_auto_type_fix) + + def read(self, tag: str, value: str) -> str: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + str: The parsed string value. + """ + self._general_read_validate(tag, value) + return str(value) + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + return self._write(tag, value) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + return self._get_token_len() + + +@dataclass +class TagContainer(AbstractTag): + """TagContainer class for handling tags that contain other tags. + + This class is used to handle tags that contain other tags. It is used to validate the type of the value for the tag, + read the value string for the tag, write the tag and its value as a string, and get the token length of the tag. + + Note: When constructing a TagContainer, all subtags must be able to return the correct token length without any + information about the value. + # TODO: Remove this assumption by changing the signature of get_token_len to take the value as an argument. + """ + + linebreak_nth_entry: int | None = None # handles special formatting for matrix tags, e.g. lattice tag + is_tag_container: bool = ( + True # used to ensure only TagContainers are converted between list and dict representations + ) + subtags: dict[str, AbstractTag] = field(default_factory=dict) + + def _validate_single_entry( + self, value: dict | list[dict], try_auto_type_fix: bool = False + ) -> tuple[list[str], list[bool], Any]: + if not isinstance(value, dict): + raise TypeError(f"The value '{value}' (of type {type(value)}) must be a dict for this TagContainer!") + tags_checked: list[str] = [] + types_checks: list[bool] = [] + updated_value = deepcopy(value) + for subtag, subtag_value in value.items(): + subtag_object = self.subtags[subtag] + tag, check, subtag_value2 = subtag_object.validate_value_type( + subtag, subtag_value, try_auto_type_fix=try_auto_type_fix + ) + if try_auto_type_fix: + updated_value[subtag] = subtag_value2 + tags_checked.append(tag) + types_checks.append(check) + return tags_checked, types_checks, updated_value + + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the type of the value for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value, by default + False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + value_dict = self.get_dict_representation(tag, value) + if self.can_repeat: + self._validate_repeat(tag, value_dict) + results = [self._validate_single_entry(x, try_auto_type_fix=try_auto_type_fix) for x in value_dict] + tags_list_list: list[list[str]] = [result[0] for result in results] + is_valids_list_list: list[list[bool]] = [result[1] for result in results] + updated_value: Any = [result[2] for result in results] + tag_out = ",".join([",".join(x) for x in tags_list_list]) + is_valid_out = all(all(x) for x in is_valids_list_list) + if not is_valid_out: + warnmsg = "Invalid value(s) found for: " + for i, x in enumerate(is_valids_list_list): + if not all(x): + for j, y in enumerate(x): + if not y: + warnmsg += f"{tags_list_list[i][j]} " + warnings.warn(warnmsg, stacklevel=2) + else: + tags, is_valids, updated_value = self._validate_single_entry( + value_dict, try_auto_type_fix=try_auto_type_fix + ) + tag_out = ",".join(tags) + is_valid_out = all(is_valids) + if not is_valid_out: + warnmsg = "Invalid value(s) found for: " + for ii, xx in enumerate(is_valids): + if not xx: + warnmsg += f"{tags[ii]} " + warnings.warn(warnmsg, stacklevel=2) + return tag_out, is_valid_out, updated_value + + def read(self, tag: str, value: str) -> dict: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + dict: The parsed value. + """ + self._general_read_validate(tag, value) + value_list = value.split() + if tag == "ion": + special_constraints = [x in ["HyperPlane", "Linear", "None", "Planar"] for x in value_list] + if any(special_constraints): + value_list = value_list[: special_constraints.index(True)] + warnings.warn( + "Found special constraints reading an 'ion' tag, these were dropped; reading them has not been " + "implemented!", + stacklevel=2, + ) + + tempdict = {} # temporarily store read tags out of order they are processed + + for subtag, subtag_type in ( + (subtag, subtag_type) for subtag, subtag_type in self.subtags.items() if subtag_type.write_tagname + ): + # every subtag with write_tagname=True in a TagContainer has a fixed length and can be immediately read in + # this loop if it is present + if subtag in value_list: # this subtag is present in the value string + subtag_count = value_list.count(subtag) # Get number of times subtag appears in line + if not subtag_type.can_repeat: + if subtag_count > 1: + raise ValueError( + f"Subtag '{subtag}' for tag '{tag}' is not allowed to repeat but repeats value {value}" + ) + idx_start = value_list.index(subtag) + token_len = subtag_type.get_token_len() + idx_end = idx_start + token_len + subtag_value = " ".join( + value_list[(idx_start + 1) : idx_end] + ) # add 1 so the subtag value string excludes the subtagname + tempdict[subtag] = subtag_type.read(subtag, subtag_value) + del value_list[idx_start:idx_end] + else: + tempdict[subtag] = [] + for _ in range(subtag_count): + idx_start = value_list.index(subtag) + idx_end = idx_start + subtag_type.get_token_len() + subtag_value = " ".join( + value_list[(idx_start + 1) : idx_end] + ) # add 1 so the subtag value string excludes the subtagname + tempdict[subtag].append(subtag_type.read(subtag, subtag_value)) + del value_list[idx_start:idx_end] + + for subtag, subtag_type in ( + (subtag, subtag_type) for subtag, subtag_type in self.subtags.items() if not subtag_type.write_tagname + ): + # now try to populate remaining subtags that do not use a keyword in order of appearance. + if len(value_list) == 0: + break + if subtag in tempdict or subtag_type.write_tagname: # this tag has already been read or requires a tagname + # keyword to be present + continue + tempdict[subtag] = subtag_type.read(subtag, value_list[0]) + del value_list[0] + + # reorder all tags to match order of __MASTER_TAG_LIST__ and do coarse-grained validation of read. + + subdict = {x: tempdict[x] for x in self.subtags if x in tempdict} + self._check_nonoptional_subtags(tag, subdict, self.subtags) + self._check_unread_values(tag, value_list) + return subdict + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + """ + if not isinstance(value, dict): + raise TypeError( + f"The value '{value}' (of type {type(value)}) for tag '{tag}' must be a dict for this TagContainer!" + ) + + final_value = "" + indent = " " + for count, (subtag, subvalue) in enumerate(value.items()): + if self.subtags[subtag].can_repeat and isinstance(subvalue, list): + # if a subtag.can_repeat, it is assumed that subvalue is a list + # the 2nd condition ensures this + # if it is not a list, then the tag will still be printed by the else + # this could be relevant if someone manually sets the tag's can_repeat value to a non-list. + print_str_list = [self.subtags[subtag].write(subtag, entry) for entry in subvalue] + print_str = " ".join([v.strip() for v in print_str_list]) + " " + else: + print_str = self.subtags[subtag].write(subtag, subvalue).strip() + " " + + if self.multiline_tag: + final_value += f"{indent}{print_str}\\\n" + elif self.linebreak_nth_entry is not None: + # handles special formatting with extra linebreak, e.g. for lattice tag + i_column = count % self.linebreak_nth_entry + if i_column == 0: + final_value += f"{indent}{print_str}" + elif i_column == self.linebreak_nth_entry - 1: + final_value += f"{print_str}\\\n" + else: + final_value += f"{print_str}" + else: + final_value += f"{print_str}" + if self.multiline_tag or self.linebreak_nth_entry is not None: # handles special formatting for lattice tag + final_value = final_value[:-2] # exclude final \\n from final print call + + return self._write( + tag, + final_value, + multiline_override=self.linebreak_nth_entry is not None, + strip_override=self.linebreak_nth_entry is not None, + ) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + """ + min_token_len = int(self.write_tagname) # length of value subtags added next + for subtag_type in self.subtags.values(): + subtag_token_len = subtag_type.get_token_len() # recursive for nested TagContainers + if not subtag_type.optional: # TagContainers could be longer with optional subtags included + min_token_len += subtag_token_len + return min_token_len + + def _make_list(self, value: dict) -> list: + if not isinstance(value, dict): + raise TypeError(f"The value '{value}' is not a dict, so could not be converted") + value_list = [] + for subtag, subtag_value in value.items(): + subtag_type = self.subtags[subtag] + if subtag_type.allow_list_representation: + # this block deals with making list representations of any nested TagContainers + if not isinstance(subtag_value, dict): + raise ValueError(f"The subtag {subtag} is not a dict: '{subtag_value}', so could not be converted") + subtag_value2 = subtag_type.get_list_representation(subtag, subtag_value) # recursive list generation + + if subtag_type.write_tagname: # needed to write 'v' subtag in 'ion' tag + value_list.append(subtag) + value_list.extend(subtag_value2) + elif isinstance(subtag_value, dict): + # this triggers if someone sets this tag using mixed dict/list representations + warnings.warn( + f"The {subtag} subtag does not allow list representation with a value {subtag_value}.\n " + "I added the dict to the list. Is this correct? You will not be able to convert back!", + stacklevel=2, + ) + value_list.append(subtag_value) + else: + # the subtag is simply of form {'subtag': subtag_value} and now adds concrete values to the list + value_list.append(subtag_value) + + # return list of lists for tags in matrix format, e.g. lattice tag + if (ncol := self.linebreak_nth_entry) is not None: + nrow = int(len(value_list) / ncol) + value_list = [[value_list[row * ncol + col] for col in range(ncol)] for row in range(nrow)] + return value_list + + def get_list_representation(self, tag: str, value: Any) -> list: + """Convert the value to a list representation. + + Args: + tag (str): The tag to convert the value to a list representation for. + value (Any): The value to convert to a list representation. + + Returns: + list: The value converted to a list representation. + """ + # convert dict representation into list representation by writing (nested) dicts into list or list of lists. + # there are 4 types of TagContainers in the list representation: + # can_repeat: list of bool/str/int/float (ion-species) + # can_repeat: list of lists (ion) + # cannot repeat: list of bool/str/int/float (elec-cutoff) + # cannot repeat: list of lists (lattice) + if self.can_repeat and not isinstance(value, list): + raise ValueError( + f"Value '{value}' must be a list when passed to 'get_list_representation' since " + f"tag '{tag}' is repeatable." + ) + if self.can_repeat: + if all(isinstance(entry, list) for entry in value): + return value # no conversion needed + if any(not isinstance(entry, dict) for entry in value): + raise ValueError( + f"The tag '{tag}' set to value '{value}' must be a list of dicts when passed to " + "'get_list_representation' since the tag is repeatable." + ) + tag_as_list = [self._make_list(entry) for entry in value] + else: + tag_as_list = self._make_list(value) + return tag_as_list + + @staticmethod + def _check_for_mixed_nesting(tag: str, value: Any) -> None: + has_nested_dict = any(isinstance(x, dict) for x in value) + has_nested_list = any(isinstance(x, list) for x in value) + if has_nested_dict and has_nested_list: + raise ValueError( + f"tag '{tag}' with value '{value}' cannot have nested lists/dicts mixed with bool/str/int/floats!" + ) + if has_nested_dict: + raise ValueError( + f"tag '{tag}' with value '{value}' cannot have nested dicts mixed with bool/str/int/floats!" + ) + if has_nested_list: + raise ValueError( + f"tag '{tag}' with value '{value}' cannot have nested lists mixed with bool/str/int/floats!" + ) + + def _make_str_for_dict(self, tag: str, value_list: list) -> str: + """Convert the value to a string representation. + + Args: + tag (str): The tag to convert the value to a string representation for. + value_list (list): The value to convert to a string representation. + + Returns: + str: The value converted to a string representation for dict representation. + """ + value = _flatten_list(tag, value_list) + self._check_for_mixed_nesting(tag, value) + return " ".join([str(x) for x in value]) + + def get_dict_representation(self, tag: str, value: list) -> dict | list[dict]: + """Convert the value to a dict representation. + + Args: + tag (str): The tag to convert the value to a dict representation for. + value (list): The value to convert to a dict representation. + + Returns: + dict | list[dict]: The value converted to a dict representation. + """ + # convert list or list of lists representation into string the TagContainer can process back into (nested) dict + + if self.can_repeat and not isinstance(value, list): + raise ValueError( + f"Value '{value}' must be a list when passed to 'get_dict_representation' since " + f"tag '{tag}' is repeatable." + ) + if ( + self.can_repeat and len({len(x) for x in value}) > 1 + ): # Creates a list of every unique length of the subdicts + # TODO: Populate subdicts with fewer entries with JDFTx defaults to make compatible + raise ValueError(f"The values '{value}' for tag '{tag}' provided in a list of lists have different lengths") + value = value.tolist() if isinstance(value, np.ndarray) else value + + # there are 4 types of TagContainers in the list representation: + # can_repeat: list of bool/str/int/float (ion-species) + # can_repeat: list of lists (ion) + # cannot repeat: list of bool/str/int/float (elec-cutoff) + # cannot repeat: list of lists (lattice) + + # the .read() method automatically handles regenerating any nesting because is just like reading a file + if self.can_repeat: + if all(isinstance(entry, dict) for entry in value): + return value # no conversion needed + string_value = [self._make_str_for_dict(tag, entry) for entry in value] + return [self.read(tag, entry) for entry in string_value] + + if isinstance(value, dict): + return value # no conversion needed + list_value = self._make_str_for_dict(tag, value) + return self.read(tag, list_value) + + +# TODO: Write StructureDefferedTagContainer back in (commented out code block removed +# on 11/4/24) and make usable for tags like initial-magnetic-moments + + +@dataclass +class MultiformatTag(AbstractTag): + """Class for tags with multiple format options. + + Class for tags that could have different types of input values given to them or tags where different subtag options + directly impact how many expected arguments are provided e.g. the coulomb-truncation or van-der-waals tags. + + This class should not be used for tags with simply some combination of mandatory and optional args because the + TagContainer class can handle those cases by itself. + """ + + format_options: list[AbstractTag] = field(default_factory=list) + + def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]: + """Validate the type of the value for this tag. + + Args: + tag (str): The tag to validate the value type for. + value (Any): The value to validate the type of. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value. Defaults to + False. + + Returns: + tuple[str, bool, Any]: The tag, whether the value is of the correct type, and the possibly fixed value. + """ + format_index, value = self._determine_format_option(tag, value, try_auto_type_fix=try_auto_type_fix) + is_valid = format_index is not None + return tag, is_valid, value + + def read(self, tag: str, value: str) -> None: + """Read the value string for this tag. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Raises: + RuntimeError: If the method is called directly on MultiformatTag. + """ + err_str = "The read method is not supposed to be called directly on MultiformatTag." + "Get the proper format option first." + raise RuntimeError(err_str) + + def write(self, tag: str, value: Any) -> str: + """Write the tag and its value as a string. + + Args: + tag (str): The tag to write. + value (Any): The value to write. + + Returns: + str: The tag and its value as a string. + + Raises: + RuntimeError: If the method is called directly on MultiformatTag. + """ + err_str = "The write method is not supposed to be called directly on MultiformatTag." + "Get the proper format option first." + raise RuntimeError(err_str) + + def get_format_index_for_str_value(self, tag: str, value: str) -> int: + """Get the format index from string representation of value. + + Args: + tag (str): The tag to read the value string for. + value (str): The value string to read. + + Returns: + int: The index of the format option for the value of this tag. + + Raises: + ValueError: If no valid read format is found for the tag. + """ + problem_log = [] + for i, trial_format in enumerate(self.format_options): + try: + _ = trial_format.read(tag, value) + return i + except (ValueError, TypeError) as e: + problem_log.append(f"Format {i}: {e}") + raise ValueError( + f"No valid read format for tag '{tag}' with value '{value}'\n" + "Add option to format_options or double-check the value string and retry!\n\n" + ) + + def raise_invalid_format_option_error(self, tag: str, i: int) -> None: + """Raise an error for an invalid format option. + + Args: + tag (str): The tag to raise the error for. + i (int): The index of the format option to raise the error for. + + Raises: + ValueError: If the format option is invalid. + """ + raise ValueError(f"tag '{tag}' failed to validate for option {i}") + + def _determine_format_option(self, tag: str, value_any: Any, try_auto_type_fix: bool = False) -> tuple[int, Any]: + """Determine the format option for the value of this tag. + + Args: + tag (str): The tag to determine the format option for. + value_any (Any): The value to determine the format option for. + try_auto_type_fix (bool, optional): Whether to try to automatically fix the type of the value. Defaults to + False. + + Returns: + tuple[int, Any]: The index of the format option for the value of this tag and the value of this tag. + + Raises: + ValueError: If the format for the tag could not be determined from the available options. + """ + exceptions = [] + for i, format_option in enumerate(self.format_options): + if format_option.can_repeat: + value = [value_any] if not isinstance(value_any, list) else value_any + else: + value = value_any + try: + _, is_tag_valid, value = format_option.validate_value_type( + tag, value, try_auto_type_fix=try_auto_type_fix + ) + if not is_tag_valid: + self.raise_invalid_format_option_error(tag, i) + else: + return i, value + except (ValueError, TypeError, KeyError) as e: + exceptions.append(e) + raise ValueError( + f"The format for tag '{tag}' with value '{value_any}' could not be determined from the available options! " + "Check your inputs and/or MASTER_TAG_LIST!" + ) + + def get_token_len(self) -> int: + """Get the token length of the tag. + + Returns: + int: The token length of the tag. + + Raises: + NotImplementedError: If the method is called directly on MultiformatTag objects. + """ + raise NotImplementedError("This method is not supposed to be called directly on MultiformatTag objects!") + + +@dataclass +class BoolTagContainer(TagContainer): + """BoolTagContainer class for handling the subtags to the "dump" tag. + + This class is used to handle the subtags to the "dump" tag. All subtags are freqs for dump, and all values for these + tags are boolean values that are read given the existence of their "var" name. + """ + + def read(self, tag: str, value_str: str) -> dict: + """Read the value string for this tag. + + This method reads the value string for this tag. It is used to parse the value string for the tag and return the + parsed value. + + Args: + tag (str): The tag to read the value string for. + value_str (str): The value string to read. + + Returns: + dict: The parsed value. + """ + self._general_read_validate(tag, value_str) + value = value_str.split() + tempdict = {} + for subtag, subtag_type in self.subtags.items(): + if subtag in value: + idx_start = value.index(subtag) + idx_end = idx_start + subtag_type.get_token_len() + subtag_value = " ".join(value[(idx_start + 1) : idx_end]) + tempdict[subtag] = subtag_type.read(subtag, subtag_value) + del value[idx_start:idx_end] + subdict = {x: tempdict[x] for x in self.subtags if x in tempdict} + self._check_nonoptional_subtags(tag, subdict, self.subtags) + self._check_unread_values(tag, value) + return subdict + + +@dataclass +class DumpTagContainer(TagContainer): + """DumpTagContainer class for handling the "dump" tag. + + This class is used to handle the "dump" tag. + """ + + def read(self, tag: str, value_str: str) -> dict: + """Read the value string for this tag. + + This method reads the value string for this tag. It is used to parse the value string for the tag and return the + parsed value. + + Args: + tag (str): The tag to read the value string for. + value_str (str): The value string to read. + + Returns: + dict: The parsed value. + """ + self._general_read_validate(tag, value_str) + value = value_str.split() + tempdict = {} + # Each subtag is a freq, which will be a BoolTagContainer + for subtag, subtag_type in self.subtags.items(): + if subtag in value: + idx_start = value.index(subtag) + subtag_value = " ".join(value[(idx_start + 1) :]) + tempdict[subtag] = subtag_type.read(subtag, subtag_value) + del value[idx_start:] + # reorder all tags to match order of __MASTER_TAG_LIST__ and do coarse-grained validation of read + subdict = {x: tempdict[x] for x in self.subtags if x in tempdict} + # There are no forced subtags for dump + self._check_unread_values(tag, value) + return subdict + + +def _flatten_list(tag: str, list_of_lists: list[Any]) -> list[Any]: + """Flatten list of lists into a single list, then stop. + + Flatten list of lists into a single list, then stop. + + Parameters + ---------- + tag : str + The tag to flatten the list of lists for. + list_of_lists : list[Any] + The list of lists to flatten. + + Returns + ------- + list[Any] + The flattened list. + """ + if not isinstance(list_of_lists, list): + raise TypeError(f"{tag}: You must provide a list to flatten_list()!") + flist = [] + for v in list_of_lists: + if isinstance(v, list): + flist.extend(_flatten_list(tag, v)) + else: + flist.append(v) + return flist diff --git a/src/pymatgen/io/jdftx/inputs.py b/src/pymatgen/io/jdftx/inputs.py new file mode 100644 index 00000000000..a156d83b146 --- /dev/null +++ b/src/pymatgen/io/jdftx/inputs.py @@ -0,0 +1,919 @@ +"""Classes for reading/manipulating/writing JDFTx input files. + +Classes for reading/manipulating/writing JDFTx input files. + +Note: JDFTXInfile will be moved back to its own module once a more broad inputs +class is written. + +@mkhorton - This file is ready to review. +""" + +from __future__ import annotations + +import warnings +from copy import deepcopy +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +import numpy as np +import scipy.constants as const +from monty.json import MSONable + +from pymatgen.core import Structure +from pymatgen.core.periodic_table import Element +from pymatgen.core.units import bohr_to_ang +from pymatgen.io.jdftx.generic_tags import AbstractTag, BoolTagContainer, DumpTagContainer, MultiformatTag, TagContainer +from pymatgen.io.jdftx.jdftxinfile_master_format import ( + __PHONON_TAGS__, + __TAG_LIST__, + __WANNIER_TAGS__, + MASTER_TAG_LIST, + get_tag_object, +) +from pymatgen.util.io_utils import clean_lines +from pymatgen.util.typing import SpeciesLike + +if TYPE_CHECKING: + from typing import Any + + from numpy.typing import ArrayLike + from typing_extensions import Self + + from pymatgen.util.typing import PathLike + +__author__ = "Jacob Clary, Ben Rich" + +# TODO: Add check for whether all ions have or lack velocities. +# TODO: Add default value filling like JDFTx does. + + +class JDFTXInfile(dict, MSONable): + """Class for reading/writing JDFtx input files. + + JDFTxInfile object for reading and writing JDFTx input files. + Essentially a dictionary with some helper functions. + """ + + path_parent: str | None = None # Only gets a value if JDFTXInfile is initializedf with from_file + + def __init__(self, params: dict[str, Any] | None = None) -> None: + """ + Create a JDFTXInfile object. + + Args: + params (dict): Input parameters as a dictionary. + """ + super().__init__() + if params is not None: + self.update(params) + + def __str__(self) -> str: + """Return str representation of JDFTXInfile. + + Returns: + str: String representation of JDFTXInfile. + """ + return "".join([line + "\n" for line in self.get_text_list()]) + + def __add__(self, other: JDFTXInfile) -> JDFTXInfile: + """Add existing JDFTXInfile object to method caller JDFTXInfile object. + + Add all the values of another JDFTXInfile object to this object. Facilitate the use of "standard" JDFTXInfiles. + + Args: + other (JDFTXInfile): JDFTXInfile object to add to the method caller object. + + Returns: + JDFTXInfile: The combined JDFTXInfile object. + """ + params: dict[str, Any] = dict(self.items()) + for key, val in other.items(): + if key in self and val != self[key]: + raise ValueError(f"JDFTXInfiles have conflicting values for {key}: {self[key]} != {val}") + params[key] = val + return type(self)(params) + + def as_dict(self, sort_tags: bool = True, skip_module_keys: bool = False) -> dict: + """Return JDFTXInfile as MSONable dict. + + Args: + sort_tags (bool, optional): Whether to sort the tags. Defaults to True. + skip_module_keys (bool, optional): Whether to skip the module keys. Defaults to False. + + Returns: + dict: JDFTXInfile as MSONable dict. + """ + params = dict(self) + if sort_tags: + params = {tag: params[tag] for tag in __TAG_LIST__ if tag in params} + if not skip_module_keys: + params["@module"] = type(self).__module__ + params["@class"] = type(self).__name__ + return params + + @classmethod + def _from_dict(cls, dct: dict[str, Any]) -> JDFTXInfile: + """Parse a dictionary to create a JDFTXInfile object. + + Args: + dct (dict): Dictionary to parse. + + Returns: + JDFTXInfile: The created JDFTXInfile object. + """ + temp = cls({k: v for k, v in dct.items() if k not in ("@module", "@class")}) + temp = cls.get_dict_representation(cls.get_list_representation(temp)) + return cls.get_list_representation(temp) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> JDFTXInfile: + """Create JDFTXInfile from a dictionary. + + Args: + d (dict): Dictionary to create JDFTXInfile from. + + Returns: + JDFTXInfile: The created JDFTXInfile object. + """ + instance = cls() + for k, v in d.items(): + if k not in ("@module", "@class"): + instance[k] = v + return instance + + def copy(self) -> JDFTXInfile: + """Return a copy of the JDFTXInfile object. + + Returns: + JDFTXInfile: Copy of the JDFTXInfile object. + """ + return type(self)(self) + + def get_text_list(self) -> list[str]: + """Get a list of strings representation of the JDFTXInfile. + + Returns: + list[str]: List of strings representation of the JDFTXInfile. + """ + self_as_dict = self.get_dict_representation(self) + + text: list[str] = [] + for tag_group in MASTER_TAG_LIST: + added_tag_in_group = False + for tag in MASTER_TAG_LIST[tag_group]: + if tag not in self: + continue + added_tag_in_group = True + tag_object: AbstractTag = MASTER_TAG_LIST[tag_group][tag] + if isinstance(tag_object, MultiformatTag): + i, _ = tag_object._determine_format_option(tag, self_as_dict[tag]) + tag_object = tag_object.format_options[i] + if tag_object.can_repeat and isinstance(self_as_dict[tag], list): + # text += " ".join([tag_object.write(tag, entry) for entry in self_as_dict[tag]]) + text += [tag_object.write(tag, entry) for entry in self_as_dict[tag]] + else: + text.append(tag_object.write(tag, self_as_dict[tag])) + if added_tag_in_group: + text.append("") + return text + + def write_file(self, filename: PathLike) -> None: + """Write JDFTXInfile to an in file. + + Args: + filename (PathLike): Filename to write to. + """ + with open(filename, mode="w") as file: + file.write(str(self)) + + @classmethod + def from_file( + cls, + filename: PathLike, + dont_require_structure: bool = False, + sort_tags: bool = True, + assign_path_parent: bool = True, + ) -> Self: + """Read a JDFTXInfile object from a file. + + Args: + filename (PathLike): Filename to read from. + dont_require_structure (bool, optional): Whether to require structure tags. Defaults to False. + sort_tags (bool, optional): Whether to sort the tags. Defaults to True. + assign_path_parent (bool, optional): Whether to assign the parent directory of the input file for include + tags. Defaults to True. + + Returns: + JDFTXInfile: The created JDFTXInfile object. + """ + path_parent = None + if assign_path_parent: + path_parent = Path(filename).parents[0] + with open(filename) as file: + return cls.from_str( + file.read(), + dont_require_structure=dont_require_structure, + sort_tags=sort_tags, + path_parent=path_parent, + ) + + @staticmethod + def _preprocess_line(line: str) -> tuple[AbstractTag, str, str]: + """Preprocess a line from a JDFTXInfile, splitting it into a tag object, tag name, and value. + + Args: + line (str): Line from the input file. + + Returns: + tuple[AbstractTag, str, str]: Tag object, tag name, and value. + """ + line_list = line.strip().split(maxsplit=1) + tag: str = line_list[0].strip() + if tag in __PHONON_TAGS__: + raise ValueError("Phonon functionality has not been added!") + if tag in __WANNIER_TAGS__: + raise ValueError("Wannier functionality has not been added!") + if tag not in __TAG_LIST__: + err_str = f"The {tag} tag in {line_list} is not in MASTER_TAG_LIST and is not a comment, " + err_str += "something is wrong with this input data!" + raise ValueError(err_str) + tag_object = get_tag_object(tag) + value: str = "" + if len(line_list) == 2: + value = line_list[1].strip() + elif len(line_list) == 1: + value = "" # exception for tags where only tagname is used, e.g. dump-only tag + if isinstance(tag_object, MultiformatTag): + i = tag_object.get_format_index_for_str_value(tag, value) + tag_object = tag_object.format_options[i] + return tag_object, tag, value + + @staticmethod + def _store_value( + params: dict[str, list | list[dict[str, dict]] | Any], + tag_object: AbstractTag, + tag: str, + value: Any, + ) -> dict: + """Store the value in the params dictionary. + + Args: + params (dict): Dictionary to store the value in. + tag_object (AbstractTag): Tag object for holding tag/value pair. This tag object should be the tag object + which the "tag" string maps to in MASTER_TAG_LIST. + tag (str): Tag name. + value (Any): Value to store. + + Returns: + dict: Updated dictionary with the stored value. + """ + if tag_object.can_repeat: # store tags that can repeat in a list + if tag not in params: + params[tag] = [] + params[tag].append(value) + else: + if tag in params: + raise ValueError(f"The '{tag}' tag appears multiple times in this input when it should not!") + params[tag] = value + return params + + @staticmethod + def _gather_tags(lines: list[str]) -> list[str]: + """Gather broken lines into single string for processing later. + + Args: + lines (list[str]): List of lines from the input file. + + Returns: + list[str]: List of strings with tags broken across lines combined into single string. + """ + total_tag = "" + gathered_strings = [] + for line in lines: + if line[-1] == "\\": # "\" indicates line continuation (the first "\" needed to be escaped) + total_tag += line[:-1].strip() + " " # remove \ and any extra whitespace + elif total_tag: # then finished with line continuations + total_tag += line + gathered_strings.append(total_tag) + total_tag = "" + else: # then append line like normal + gathered_strings.append(line) + return gathered_strings + + @property + def structure(self) -> Structure: + """Return a pymatgen Structure object. + + Returns: + Structure: Pymatgen structure object. + """ + return self.to_pmg_structure(self) + + @classmethod + def from_structure( + cls, + structure: Structure, + selective_dynamics: ArrayLike | None = None, + write_cart_coords: bool = False, + ) -> JDFTXInfile: + """Create a JDFTXInfile object from a pymatgen Structure. + + Args: + structure (Structure): Structure to convert. + selective_dynamics (ArrayLike, optional): Selective dynamics attribute for each site if available. + Shape Nx1, by default None. + + Returns: + JDFTXInfile: The created JDFTXInfile object. + """ + jdftxstructure = JDFTXStructure(structure, selective_dynamics) + jdftxstructure.write_cart_coords = write_cart_coords + return cls.from_jdftxstructure(jdftxstructure) + + @classmethod + def from_jdftxstructure( + cls, + jdftxstructure: JDFTXStructure, + ) -> JDFTXInfile: + """Create a JDFTXInfile object from a JDFTXStructure object. + + Args: + jdftxstructure (JDFTXStructure): JDFTXStructure object to convert. + + Returns: + JDFTXInfile: The created JDFTXInfile object. + """ + jstr = jdftxstructure.get_str() + return cls.from_str(jstr) + + @classmethod + def from_str( + cls, + string: str, + dont_require_structure: bool = False, + sort_tags: bool = True, + path_parent: Path | None = None, + ) -> JDFTXInfile: + """Read a JDFTXInfile object from a string. + + Args: + string (str): String to read from. + dont_require_structure (bool, optional): Whether to require structure tags. Defaults to False. + sort_tags (bool, optional): Whether to sort the tags. Defaults to True. + path_parent (Path, optional): Path to the parent directory of the input file for include tags. + Defaults to None. + + Returns: + JDFTXInfile: The created JDFTXInfile object. + """ + lines: list[str] = list(clean_lines(string.splitlines())) + lines = cls._gather_tags(lines) + + params: dict[str, Any] = {} + # process all tag value lines using specified tag formats in MASTER_TAG_LIST + for line in lines: + tag_object, tag, value = cls._preprocess_line(line) + processed_value = tag_object.read(tag, value) + params = cls._store_value(params, tag_object, tag, processed_value) # this will change with tag categories + + if "include" in params: + for filename in params["include"]: + _filename = filename + if not Path(_filename).exists(): + if path_parent is not None: + _filename = path_parent / filename + if not Path(_filename).exists(): + raise ValueError(f"The include file {filename} ({_filename}) does not exist!") + params.update(cls.from_file(_filename, dont_require_structure=True, assign_path_parent=False)) + del params["include"] + + if ( + not dont_require_structure + and "lattice" not in params + and "ion" not in params + and "ion-species" not in params + ): + raise ValueError("This input file is missing required structure tags") + if sort_tags: + params = {tag: params[tag] for tag in __TAG_LIST__ if tag in params} + return cls(params) + + @classmethod + def to_jdftxstructure(cls, jdftxinfile: JDFTXInfile, sort_structure: bool = False) -> JDFTXStructure: + """Convert JDFTXInfile to JDFTXStructure object. + + Converts JDFTx lattice, lattice-scale, ion tags into JDFTXStructure, with Pymatgen structure as attribute. + + Args: + jdftxinfile (JDFTXInfile): JDFTXInfile object to convert. + sort_structure (bool, optional): Whether to sort the structure. Useful if species are not grouped properly + together. Defaults to False. + """ + # use dict representation so it's easy to get the right column for moveScale, + # rather than checking for velocities + jdftxinfile_dict = cls.get_dict_representation(jdftxinfile) + return JDFTXStructure.from_jdftxinfile(jdftxinfile_dict, sort_structure=sort_structure) + + @classmethod + def to_pmg_structure(cls, jdftxinfile: JDFTXInfile, sort_structure: bool = False) -> Structure: + """Convert JDFTXInfile to pymatgen Structure object. + + Converts JDFTx lattice, lattice-scale, ion tags into pymatgen Structure. + + Args: + jdftxinfile (JDFTXInfile): JDFTXInfile object to convert. + sort_structure (bool, optional): Whether to sort the structure. Useful if species are not grouped properly + together. Defaults to False. + + Returns: + Structure: The created pymatgen Structure object. + """ + # use dict representation so it's easy to get the right column for + # moveScale, rather than checking for velocities + print(jdftxinfile.get_dict_representation(jdftxinfile), "INPUT DICT REP") + jdftxstructure = JDFTXStructure.from_jdftxinfile( + jdftxinfile.get_dict_representation(jdftxinfile), + sort_structure=sort_structure, + ) + return jdftxstructure.structure + + @staticmethod + def _needs_conversion(conversion: str, value: dict | list[dict] | list | list[list]) -> bool: + """Determine if a value needs to be converted. + + This method is only ever called by cls.get_list/dict_representation. + + Args: + conversion (str): Conversion type. ('dict-to-list' (value : dict | list[dict]) or + 'list-to-dict' (value : list | list[list])) + value (dict | list[dict] | list | list[list]): Value to check. + + Returns: + bool: Whether the value needs to be converted. + """ + # Check if value is not iterable + try: + iter(value) + except TypeError: + # This is triggered when JDFTXInfile is attempting to convert a non-tagcontainer to list/dict representation + # The return boolean is meaningless in this case, so just returning False to avoid the value hitting the + # "for x in value" loop below. + return False + if conversion == "list-to-dict": + flag = False + elif conversion == "dict-to-list": + flag = True + else: + raise ValueError(f"Conversion type {conversion} is not 'list-to-dict' or 'dict-to-list'") + if isinstance(value, dict) or all(isinstance(x, dict) for x in value): + return flag + return not flag + + @classmethod + def get_list_representation(cls, jdftxinfile: JDFTXInfile) -> JDFTXInfile: + """Convert JDFTXInfile object properties into list representation. + + Args: + jdftxinfile (JDFTXInfile): JDFTXInfile object to convert. + """ + reformatted_params = deepcopy(jdftxinfile.as_dict(skip_module_keys=True)) + # rest of code assumes lists are lists and not np.arrays + reformatted_params = {k: v.tolist() if isinstance(v, np.ndarray) else v for k, v in reformatted_params.items()} + for tag, value in reformatted_params.items(): + tag_object = get_tag_object(tag) + if all( + [ + tag_object.allow_list_representation, + tag_object.is_tag_container, + cls._needs_conversion("dict-to-list", value), + ] + ): + reformatted_params.update({tag: tag_object.get_list_representation(tag, value)}) + return cls(reformatted_params) + + @classmethod + def get_dict_representation(cls, jdftxinfile: JDFTXInfile) -> JDFTXInfile: + """Convert JDFTXInfile object properties into dict representation. + + Args: + jdftxinfile (JDFTXInfile): JDFTXInfile object to convert. + """ + reformatted_params = deepcopy(jdftxinfile.as_dict(skip_module_keys=True)) + # Just to make sure only passing lists and no more numpy arrays + reformatted_params = { + k: v.tolist() if isinstance(v, np.ndarray) else v for k, v in reformatted_params.items() + } # rest of code assumes lists are lists and not np.arrays + for tag, value in reformatted_params.items(): + tag_object = get_tag_object(tag) + if all( + [ + tag_object.allow_list_representation, + tag_object.is_tag_container, + cls._needs_conversion("list-to-dict", value), + ] + ): + reformatted_params.update({tag: tag_object.get_dict_representation(tag, value)}) + return cls(reformatted_params) + + # This method is called by setitem, but setitem is circumvented by update, + # so this method's parameters is still necessary + def validate_tags( + self, + try_auto_type_fix: bool = False, + error_on_failed_fix: bool = True, + return_list_rep: bool = False, + ) -> None: + """Validate the tags in the JDFTXInfile. + + Validate the tags in the JDFTXInfile. If try_auto_type_fix is True, will attempt to fix the tags. If + error_on_failed_fix is True, will raise an error if the tags cannot be fixed. If return_list_rep is True, will + return the tags in list representation. + + Args: + try_auto_type_fix (bool, optional): Whether to attempt to fix the tags. Defaults to False. + error_on_failed_fix (bool, optional): Whether to raise an error if the tags cannot be fixed. + Defaults to True. + return_list_rep (bool, optional): Whether to return the tags in list representation. Defaults to False. + """ + for tag in self: + tag_object = get_tag_object(tag) + checked_tag, is_tag_valid, value = tag_object.validate_value_type( + tag, self[tag], try_auto_type_fix=try_auto_type_fix + ) + should_warn = not is_tag_valid + if return_list_rep and tag_object.allow_list_representation: + value = tag_object.get_list_representation(tag, value) + if error_on_failed_fix and should_warn and try_auto_type_fix: + raise ValueError(f"The {tag} tag with value:\n{self[tag]}\ncould not be fixed!") + if try_auto_type_fix and is_tag_valid: + self.update({tag: value}) + if should_warn: + warnmsg = f"The {tag} tag with value:\n{self[tag]}\nhas incorrect typing!" + if any(isinstance(tag_object, tc) for tc in [TagContainer, DumpTagContainer, BoolTagContainer]): + warnmsg += "(Check earlier warnings for more details)\n" + warnings.warn(warnmsg, stacklevel=2) + + def __setitem__(self, key: str, value: Any) -> None: + """Set an item in the JDFTXInfile. + + Set an item in the JDFTXInfile. This magic method is set explicitly to immediately validate when a user sets a + tag's value, and to perform any conversion necessary. + + Args: + key (str): Key to set. + value (Any): Value to set. + """ + if not isinstance(key, str): + raise TypeError(f"{key} is not a string!") + try: + tag_object = get_tag_object(key) + except KeyError: + raise KeyError(f"The {key} tag is not in MASTER_TAG_LIST") + # if tag_object.can_repeat and not isinstance(value, list): + # value = [value] + if isinstance(tag_object, MultiformatTag): + if isinstance(value, str): + i = tag_object.get_format_index_for_str_value(key, value) + else: + i, _ = tag_object._determine_format_option(key, value) + tag_object = tag_object.format_options[i] + if tag_object.can_repeat and key in self: + del self[key] + if tag_object.can_repeat and not isinstance(value, list): + value = [value] + params: dict[str, Any] = {} + if self._is_numeric(value): + value = str(value) + if not tag_object.can_repeat: + value = [value] + for v in value: + processed_value = tag_object.read(key, v) if isinstance(v, str) else v + params = self._store_value(params, tag_object, key, processed_value) + self.update(params) + self.validate_tags(try_auto_type_fix=True, error_on_failed_fix=True) + # processed_value = tag_object.read(key, value) if isinstance(value, str) else value + # params = self._store_value(params, tag_object, key, processed_value) + # self.update(params) + # self.validate_tags(try_auto_type_fix=True, error_on_failed_fix=True) + + def _is_numeric(self, value: Any) -> bool: + """Check if a value is numeric. + + Args: + value (Any): Value to check. + + Returns: + bool: Whether the value is numeric. + """ + # data-types that might accidentally be identified as numeric + if type(value) in [bool]: + return False + try: + float(value) + is_numeric = True + except (ValueError, TypeError): + is_numeric = False + return is_numeric + + def append_tag(self, tag: str, value: Any) -> None: + """Append a value to a tag. + + Append a value to a tag. Use this method instead of directly appending the list contained in the tag, such that + the value is properly processed. + + Args: + tag (str): Tag to append to. + value (Any): Value to append. + """ + tag_object = get_tag_object(tag) + if isinstance(tag_object, MultiformatTag): + if isinstance(value, str): + i = tag_object.get_format_index_for_str_value(tag, value) + else: + i, _ = tag_object._determine_format_option(tag, value) + tag_object = tag_object.format_options[i] + if not tag_object.can_repeat: + raise ValueError(f"The tag '{tag}' cannot be repeated and thus cannot be appended") + params: dict[str, Any] = self.as_dict(skip_module_keys=True) + processed_value = tag_object.read(tag, value) if isinstance(value, str) else value + params = self._store_value(params, tag_object, tag, processed_value) + self.update(params) + + +@dataclass +class JDFTXStructure(MSONable): + """Object for representing the data in JDFTXStructure tags. + + Attributes: + structure (Structure): Associated Structure. + selective_dynamics (ArrayLike): Selective dynamics attribute for each site if available. Shape Nx1. + sort_structure (bool): Whether to sort the structure. Useful if species are not grouped properly together. + Defaults to False. + """ + + structure: Structure = None + selective_dynamics: ArrayLike | None = None + sort_structure: bool = False + write_cart_coords: bool = False + + def __post_init__(self) -> None: + """Post init function for JDFTXStructure. + + Asserts self.structure is ordered, and adds selective dynamics if needed. + """ + for i in range(len(self.structure.species)): + name = "" + if isinstance(self.structure.species[i], str): + name_str = self.structure.species[i] + elif isinstance(self.structure.species[i], SpeciesLike): + name_str = self.structure.species[i].symbol + if not isinstance(name_str, str): + name_str = name_str.symbol + else: + raise TypeError("Species must be a string or SpeciesLike object") + for j in range(len(name_str)): + if not name_str[j].isdigit(): + name += name_str[j] + self.structure.species[i] = name + if self.structure.is_ordered: + site_properties = {} + if self.selective_dynamics is not None: + selective_dynamics = np.array(self.selective_dynamics) + if not selective_dynamics.all(): + site_properties["selective_dynamics"] = selective_dynamics + + # create new copy of structure so can add selective dynamics and sort atoms if needed + structure = Structure.from_sites(self.structure) + self.structure = structure.copy(site_properties=site_properties) + if self.sort_structure: + self.structure = self.structure.get_sorted_structure() + else: + raise ValueError("Disordered structure with partial occupancies cannot be converted into JDFTXStructure!") + + def __repr__(self) -> str: + """Return representation of JDFTXStructure file. + + Returns: + str: Representation of JDFTXStructure file. + """ + return f"JDFTXStructure({self.get_str()})" + + def __str__(self) -> str: + """Return string representation of JDFTXStructure file. + + Returns: + str: String representation of JDFTXStructure file. + """ + return self.get_str() + + @property + def natoms(self) -> int: + """Return number of atoms. + + Returns: + int: Number of sites. + """ + return len(self.structure.species) + + @classmethod + def from_str(cls, data: str) -> JDFTXStructure: + """Read JDFTXStructure from string. + + Args: + data (str): String to read from. + + Returns: + JDFTXStructure: The created JDFTXStructure object. + """ + return cls.from_jdftxinfile(JDFTXInfile.from_str(data)) + + @classmethod + def from_file(cls, filename: str) -> JDFTXStructure: + """Read JDFTXStructure from file. + + Args: + filename (str): Filename to read from. + + Returns: + JDFTXStructure: The created JDFTXStructure object. + """ + return cls.from_jdftxinfile(JDFTXInfile.from_file(filename)) + + @classmethod + def from_jdftxinfile(cls, jdftxinfile: JDFTXInfile, sort_structure: bool = False) -> JDFTXStructure: + """Get JDFTXStructure from JDFTXInfile. + + Args: + jdftxinfile (JDFTXInfile): JDFTXInfile object. + sort_structure (bool, optional): Whether to sort the structure. Useful if species are not grouped properly + together as JDFTx output will have species sorted. + + Returns: + JDFTXStructure: The created JDFTXStructure object. + """ + jl = jdftxinfile["lattice"] + lattice = np.zeros([3, 3]) + for i in range(3): + for j in range(3): + lattice[i][j] += float(jl[f"R{i}{j}"]) + if "latt-scale" in jdftxinfile: + latt_scale = np.array([jdftxinfile["latt-scale"][x] for x in ["s0", "s1", "s2"]]) + lattice *= latt_scale + lattice = lattice.T # convert to row vector format + lattice *= const.value("Bohr radius") * 10**10 # Bohr radius in Ang; convert to Ang + + atomic_symbols = [x["species-id"] for x in jdftxinfile["ion"]] + coords = np.array([[x["x0"], x["x1"], x["x2"]] for x in jdftxinfile["ion"]]) + coords *= const.value("Bohr radius") * 10**10 # Bohr radius in Ang; convert to Ang + selective_dynamics = np.array([x["moveScale"] for x in jdftxinfile["ion"]]) + + coords_are_cartesian = False # is default for JDFTx + if "coords-type" in jdftxinfile: + coords_are_cartesian = jdftxinfile["coords-type"] == "Cartesian" + + struct: Structure = Structure( + lattice, + atomic_symbols, + coords, + to_unit_cell=False, + validate_proximity=False, + coords_are_cartesian=coords_are_cartesian, + ) + return cls(struct, selective_dynamics, sort_structure=sort_structure) + + def get_str(self, in_cart_coords: bool | None = None) -> str: + """Return a string to be written as JDFTXInfile tags. + + Allows extra options as compared to calling str(JDFTXStructure) directly. + + Args: + in_cart_coords (bool, optional): Whether coordinates are output in direct or Cartesian. + + Returns: + str: Representation of JDFTXInfile structure tags. + """ + jdftx_tag_dict: dict[str, Any] = {} + + if in_cart_coords is None: + in_cart_coords = self.write_cart_coords + + lattice = np.copy(self.structure.lattice.matrix) + lattice = lattice.T # transpose to get into column-vector format + lattice /= const.value("Bohr radius") * 10**10 # Bohr radius in Ang; convert to Bohr + + jdftx_tag_dict["lattice"] = lattice + jdftx_tag_dict["ion"] = [] + jdftx_tag_dict["coords-type"] = "Cartesian" if in_cart_coords else "Lattice" + valid_labels = [ + value.symbol for key, value in Element.__dict__.items() if not key.startswith("_") and not callable(value) + ] + for i, site in enumerate(self.structure): + coords = site.coords * (1 / bohr_to_ang) if in_cart_coords else site.frac_coords + sd = self.selective_dynamics[i] if self.selective_dynamics is not None else 1 + label = site.label + # TODO: This is needlessly complicated, simplify this + if label not in valid_labels: + for varname in ["species_string", "specie.name"]: + if _multi_hasattr(site, varname) and _multi_getattr(site, varname) in valid_labels: + label = _multi_getattr(site, varname) + break + if label not in valid_labels: + raise ValueError(f"Could not correct site label {label} for site (index {i})") + jdftx_tag_dict["ion"].append([label, *coords, sd]) + + return str(JDFTXInfile._from_dict(jdftx_tag_dict)) + + def write_file(self, filename: PathLike, **kwargs) -> None: + """Write JDFTXStructure to a file. + + The supported kwargs are the same as those for the JDFTXStructure.get_str method and are passed through + directly. + + Args: + filename (PathLike): Filename to write to. + **kwargs: Kwargs to pass to JDFTXStructure.get_str. + """ + with open(filename, mode="w") as file: + file.write(self.get_str(**kwargs)) + + def as_dict(self) -> dict: + """MSONable dict. + + Returns: + dict: MSONable dictionary representation of the JDFTXStructure. + """ + return { + "@module": type(self).__module__, + "@class": type(self).__name__, + "structure": self.structure.as_dict(), + "selective_dynamics": np.array(self.selective_dynamics).tolist(), + } + + @classmethod + def from_dict(cls, params: dict) -> Self: + """Get JDFTXStructure from dict. + + Args: + params (dict): Serialized JDFTXStructure. + + Returns: + JDFTXStructure: The created JDFTXStructure object. + """ + return cls( + Structure.from_dict(params["structure"]), + selective_dynamics=params["selective_dynamics"], + ) + + +def _multi_hasattr(varbase: Any, varname: str): + """Check if object has an attribute (capable of nesting with . splits). + + Check if object has an attribute (capable of nesting with . splits). + + Parameters + ---------- + varbase + Object to check. + varname + Attribute to check for. + + Returns + ------- + bool + Whether the object has the attribute. + """ + varlist = varname.split(".") + for i, var in enumerate(varlist): + if i == len(varlist) - 1: + return hasattr(varbase, var) + if hasattr(varbase, var): + varbase = getattr(varbase, var) + else: + return False + return None + + +def _multi_getattr(varbase: Any, varname: str): + """Check if object has an attribute (capable of nesting with . splits). + + Check if object has an attribute (capable of nesting with . splits). + + Parameters + ---------- + varbase + Object to check. + varname + Attribute to check for. + + Returns + ------- + Any + Attribute of the object. + """ + if not _multi_hasattr(varbase, varname): + raise AttributeError(f"{varbase} does not have attribute {varname}") + varlist = varname.split(".") + for var in varlist: + varbase = getattr(varbase, var) + return varbase diff --git a/src/pymatgen/io/jdftx/jdftxinfile_master_format.py b/src/pymatgen/io/jdftx/jdftxinfile_master_format.py new file mode 100644 index 00000000000..20d82f4fe4b --- /dev/null +++ b/src/pymatgen/io/jdftx/jdftxinfile_master_format.py @@ -0,0 +1,1196 @@ +"""Master list of AbstractTag-type objects for JDFTx input file generation. + +This module contains; +- MASTER_TAG_LIST: a dictionary mapping tag categories to dictionaries mapping + tag names to AbstractTag-type objects. +- get_tag_object: a function that returns an AbstractTag-type object from + MASTER_TAG_LIST given a tag name. + +@mkhorton - this file is ready to review. +""" + +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +from pymatgen.core.periodic_table import Element +from pymatgen.io.jdftx.generic_tags import ( + AbstractTag, + BoolTag, + BoolTagContainer, + DumpTagContainer, + FloatTag, + InitMagMomTag, + IntTag, + MultiformatTag, + StrTag, + TagContainer, +) +from pymatgen.io.jdftx.jdftxinfile_ref_options import ( + fluid_solvent_options, + func_c_options, + func_options, + func_x_options, + func_xc_options, + jdftxdumpfreqoptions, + jdftxdumpvaroptions, + jdftxfluid_subtagdict, + jdftxminimize_subtagdict, + kinetic_functionals, +) + +__author__ = "Jacob Clary, Ben Rich" + +MASTER_TAG_LIST: dict[str, dict[str, Any]] = { + "extrafiles": { + "include": StrTag(can_repeat=True), + }, + "structure": { + "latt-scale": TagContainer( + allow_list_representation=True, + subtags={ + "s0": IntTag(write_tagname=False, optional=False), + "s1": IntTag(write_tagname=False, optional=False), + "s2": IntTag(write_tagname=False, optional=False), + }, + ), + "latt-move-scale": TagContainer( + allow_list_representation=True, + subtags={ + "s0": FloatTag(write_tagname=False, optional=False), + "s1": FloatTag(write_tagname=False, optional=False), + "s2": FloatTag(write_tagname=False, optional=False), + }, + ), + "coords-type": StrTag(options=["Cartesian", "Lattice"]), + # TODO: change lattice tag into MultiformatTag for different symmetry options + "lattice": TagContainer( + linebreak_nth_entry=3, + optional=False, + allow_list_representation=True, + subtags={ + "R00": FloatTag(write_tagname=False, optional=False, prec=12), + "R01": FloatTag(write_tagname=False, optional=False, prec=12), + "R02": FloatTag(write_tagname=False, optional=False, prec=12), + "R10": FloatTag(write_tagname=False, optional=False, prec=12), + "R11": FloatTag(write_tagname=False, optional=False, prec=12), + "R12": FloatTag(write_tagname=False, optional=False, prec=12), + "R20": FloatTag(write_tagname=False, optional=False, prec=12), + "R21": FloatTag(write_tagname=False, optional=False, prec=12), + "R22": FloatTag(write_tagname=False, optional=False, prec=12), + }, + ), + "ion": TagContainer( + can_repeat=True, + optional=False, + allow_list_representation=True, + subtags={ + "species-id": StrTag( + write_tagname=False, + optional=False, + options=[ + value.symbol + for key, value in Element.__dict__.items() + if not key.startswith("_") and not callable(value) + ], # Required in case bad species names gets fed + ), + "x0": FloatTag(write_tagname=False, optional=False, prec=12), + "x1": FloatTag(write_tagname=False, optional=False, prec=12), + "x2": FloatTag(write_tagname=False, optional=False, prec=12), + "v": TagContainer( + allow_list_representation=True, + subtags={ + "vx0": FloatTag(write_tagname=False, optional=False, prec=12), + "vx1": FloatTag(write_tagname=False, optional=False, prec=12), + "vx2": FloatTag(write_tagname=False, optional=False, prec=12), + }, + ), + "moveScale": IntTag(write_tagname=False, optional=False), + }, + ), + "perturb-ion": TagContainer( + subtags={ + "species": StrTag(write_tagname=False, optional=False), + "atom": IntTag(write_tagname=False, optional=False), + "dx0": FloatTag(write_tagname=False, optional=False), + "dx1": FloatTag(write_tagname=False, optional=False), + "dx2": FloatTag(write_tagname=False, optional=False), + } + ), + "core-overlap-check": StrTag(options=["additive", "vector", "none"]), + "ion-species": StrTag(can_repeat=True, optional=False), + "cache-projectors": BoolTag(), + "ion-width": MultiformatTag( + format_options=[ + StrTag(options=["Ecut", "fftbox"]), + FloatTag(), + ] + ), + }, + "symmetries": { + "symmetries": StrTag(options=["automatic", "manual", "none"]), + "symmetry-threshold": FloatTag(), + "symmetry-matrix": TagContainer( + linebreak_nth_entry=3, + can_repeat=True, + allow_list_representation=True, + subtags={ + "s00": IntTag(write_tagname=False, optional=False), + "s01": IntTag(write_tagname=False, optional=False), + "s02": IntTag(write_tagname=False, optional=False), + "s10": IntTag(write_tagname=False, optional=False), + "s11": IntTag(write_tagname=False, optional=False), + "s12": IntTag(write_tagname=False, optional=False), + "s20": IntTag(write_tagname=False, optional=False), + "s21": IntTag(write_tagname=False, optional=False), + "s22": IntTag(write_tagname=False, optional=False), + "a0": FloatTag(write_tagname=False, optional=False, prec=12), + "a1": FloatTag(write_tagname=False, optional=False, prec=12), + "a2": FloatTag(write_tagname=False, optional=False, prec=12), + }, + ), + }, + "k-mesh": { + "kpoint": TagContainer( + can_repeat=True, + allow_list_representation=True, + subtags={ + "k0": FloatTag(write_tagname=False, optional=False, prec=12), + "k1": FloatTag(write_tagname=False, optional=False, prec=12), + "k2": FloatTag(write_tagname=False, optional=False, prec=12), + "weight": FloatTag(write_tagname=False, optional=False, prec=12), + }, + ), + "kpoint-folding": TagContainer( + allow_list_representation=True, + subtags={ + "n0": IntTag(write_tagname=False, optional=False), + "n1": IntTag(write_tagname=False, optional=False), + "n2": IntTag(write_tagname=False, optional=False), + }, + ), + "kpoint-reduce-inversion": BoolTag(), + }, + "electronic": { + "elec-ex-corr": MultiformatTag( + format_options=[ + # note that hyb-HSE06 has a bug in JDFTx and should not be + # used and is excluded here use the LibXC version instead + # (hyb-gga-HSE06) + StrTag( + write_tagname=True, + options=deepcopy(func_options), + ), + TagContainer( + subtags={ + "funcX": StrTag( + write_tagname=False, + optional=False, + options=deepcopy(func_x_options), + ), + "funcC": StrTag( + write_tagname=False, + optional=False, + options=deepcopy(func_c_options), + ), + } + ), + TagContainer( + subtags={ + "funcXC": StrTag( + write_tagname=False, + optional=False, + options=deepcopy(func_xc_options), + ) + } + ), + ] + ), + "elec-ex-corr-compare": MultiformatTag( + can_repeat=True, + format_options=[ + # note that hyb-HSE06 has a bug in JDFTx and should not be used + # and is excluded here use the LibXC version instead + # (hyb-gga-HSE06) + StrTag( + write_tagname=True, + options=func_options, + ), + TagContainer( + subtags={ + "funcX": StrTag( + write_tagname=False, + optional=False, + options=deepcopy(func_x_options), + ), + "funcC": StrTag( + write_tagname=False, + optional=False, + options=deepcopy(func_c_options), + ), + } + ), + TagContainer( + subtags={ + "funcXC": StrTag( + write_tagname=False, + optional=False, + options=deepcopy(func_xc_options), + ) + } + ), + ], + ), + "exchange-block-size": IntTag(), + "exchange-outer-loop": IntTag(), + "exchange-parameters": TagContainer( + subtags={ + "exxScale": FloatTag(write_tagname=False, optional=False), + "exxOmega": FloatTag(write_tagname=False), + } + ), + "exchange-params": TagContainer( + multiline_tag=True, + subtags={ + "blockSize": IntTag(), + "nOuterVxx": IntTag(), + }, + ), + "exchange-regularization": StrTag( + options=[ + "AuxiliaryFunction", + "None", + "ProbeChargeEwald", + "SphericalTruncated", + "WignerSeitzTruncated", + ] + ), + "tau-core": TagContainer( + subtags={ + "species-id": StrTag(write_tagname=False, optional=False), + "rCut": FloatTag(write_tagname=False), + "plot": BoolTag(write_tagname=False), + } + ), + "lj-override": FloatTag(), + "van-der-waals": MultiformatTag( + format_options=[ + StrTag(options=["D3"]), + FloatTag(), + ] + ), + "elec-cutoff": TagContainer( + allow_list_representation=True, + subtags={ + "Ecut": FloatTag(write_tagname=False, optional=False), + "EcutRho": FloatTag(write_tagname=False), + }, + ), + "elec-smearing": TagContainer( + allow_list_representation=True, + subtags={ + "smearingType": StrTag( + options=["Cold", "Fermi", "Gauss", "MP1"], + write_tagname=False, + optional=False, + ), + "smearingWidth": FloatTag(write_tagname=False, optional=False), + }, + ), + "elec-n-bands": IntTag(), + "spintype": StrTag(options=["no-spin", "spin-orbit", "vector-spin", "z-spin"]), + "initial-magnetic-moments": InitMagMomTag(), + "elec-initial-magnetization": TagContainer( + subtags={ + "M": FloatTag(write_tagname=False, optional=False), + "constrain": BoolTag(write_tagname=False, optional=False), + } + ), + "target-Bz": FloatTag(), + "elec-initial-charge": FloatTag(), + "converge-empty-states": BoolTag(), + "band-unfold": TagContainer( + linebreak_nth_entry=3, + allow_list_representation=True, + subtags={ + "M00": IntTag(write_tagname=False, optional=False), + "M01": IntTag(write_tagname=False, optional=False), + "M02": IntTag(write_tagname=False, optional=False), + "M10": IntTag(write_tagname=False, optional=False), + "M11": IntTag(write_tagname=False, optional=False), + "M12": IntTag(write_tagname=False, optional=False), + "M20": IntTag(write_tagname=False, optional=False), + "M21": IntTag(write_tagname=False, optional=False), + "M22": IntTag(write_tagname=False, optional=False), + }, + ), + "basis": StrTag(options=["kpoint-dependent", "single"]), + "fftbox": TagContainer( + allow_list_representation=True, + subtags={ + "S0": IntTag(write_tagname=False, optional=False), + "S1": IntTag(write_tagname=False, optional=False), + "S2": IntTag(write_tagname=False, optional=False), + }, + ), + "electric-field": TagContainer( + allow_list_representation=True, + subtags={ + "Ex": IntTag(write_tagname=False, optional=False), + "Ey": IntTag(write_tagname=False, optional=False), + "Ez": IntTag(write_tagname=False, optional=False), + }, + ), + "perturb-electric-field": TagContainer( + allow_list_representation=True, + subtags={ + "Ex": IntTag(write_tagname=False, optional=False), + "Ey": IntTag(write_tagname=False, optional=False), + "Ez": IntTag(write_tagname=False, optional=False), + }, + ), + "box-potential": TagContainer( + can_repeat=True, + subtags={ + "xmin": FloatTag(write_tagname=False, optional=False), + "xmax": FloatTag(write_tagname=False, optional=False), + "ymin": FloatTag(write_tagname=False, optional=False), + "ymax": FloatTag(write_tagname=False, optional=False), + "zmin": FloatTag(write_tagname=False, optional=False), + "zmax": FloatTag(write_tagname=False, optional=False), + "Vin": FloatTag(write_tagname=False, optional=False), + "Vout": FloatTag(write_tagname=False, optional=False), + "convolve_radius": FloatTag(write_tagname=False), + }, + ), + "ionic-gaussian-potential": TagContainer( + can_repeat=True, + subtags={ + "species": StrTag(write_tagname=False, optional=False), + "U0": FloatTag(write_tagname=False, optional=False), + "sigma": FloatTag(write_tagname=False, optional=False), + "geometry": StrTag( + options=["Spherical", "Cylindrical", "Planar"], + write_tagname=False, + optional=False, + ), + }, + ), + "bulk-epsilon": TagContainer( + subtags={ + "DtotFile": StrTag(write_tagname=False, optional=False), + "Ex": FloatTag(write_tagname=False), + "Ey": FloatTag(write_tagname=False), + "Ez": FloatTag(write_tagname=False), + } + ), + "charged-defect": TagContainer( + can_repeat=True, + subtags={ + "x0": FloatTag(write_tagname=False, optional=False), + "x1": FloatTag(write_tagname=False, optional=False), + "x2": FloatTag(write_tagname=False, optional=False), + "q": FloatTag(write_tagname=False, optional=False), + "sigma": FloatTag(write_tagname=False, optional=False), + }, + ), + "charged-defect-correction": TagContainer( + subtags={ + "Slab": TagContainer( + subtags={ + "dir": StrTag(options=["100", "010", "001"], write_tagname=False), + } + ), + "DtotFile": StrTag(write_tagname=False, optional=False), + "Eps": MultiformatTag( + format_options=[ + FloatTag(write_tagname=False, optional=False), + StrTag(write_tagname=False, optional=False), + ] + ), + "rMin": FloatTag(write_tagname=False, optional=False), + "rSigma": FloatTag(write_tagname=False, optional=False), + } + ), + "Cprime-params": TagContainer( + subtags={ + "dk": FloatTag(write_tagname=False), + "degeneracyThreshold": FloatTag(write_tagname=False), + "vThreshold": FloatTag(write_tagname=False), + "realSpaceTruncated": BoolTag(write_tagname=False), + } + ), + "electron-scattering": TagContainer( + multiline_tag=True, + subtags={ + "eta": FloatTag(optional=False), + "Ecut": FloatTag(), + "fCut": FloatTag(), + "omegaMax": FloatTag(), + "RPA": BoolTag(), + "dumpEpsilon": BoolTag(), + "slabResponse": BoolTag(), + "EcutTransverse": FloatTag(), + "computeRange": TagContainer( + subtags={ + "iqStart": FloatTag(write_tagname=False, optional=False), + "iqStop": FloatTag(write_tagname=False, optional=False), + } + ), + }, + ), + "perturb-test": BoolTag(write_value=False), + "perturb-wavevector": TagContainer( + subtags={ + "q0": FloatTag(write_tagname=False, optional=False), + "q1": FloatTag(write_tagname=False, optional=False), + "q2": FloatTag(write_tagname=False, optional=False), + } + ), + }, + "truncation": { + "coulomb-interaction": MultiformatTag( + format_options=[ + # note that the first 2 and last 2 TagContainers could be + # combined, but keep separate so there is less ambiguity on + # formatting + TagContainer( + subtags={ + "truncationType": StrTag( + options=["Periodic", "Isolated"], + write_tagname=False, + optional=False, + ) + } + ), + TagContainer( + subtags={ + "truncationType": StrTag(options=["Spherical"], write_tagname=False, optional=False), + "Rc": FloatTag(write_tagname=False), + } + ), + TagContainer( + subtags={ + "truncationType": StrTag( + options=["Slab", "Wire"], + write_tagname=False, + optional=False, + ), + "dir": StrTag( + options=["001", "010", "100"], + write_tagname=False, + optional=False, + ), + } + ), + TagContainer( + subtags={ + "truncationType": StrTag(options=["Cylindrical"], write_tagname=False, optional=False), + "dir": StrTag( + options=["001", "010", "100"], + write_tagname=False, + optional=False, + ), + "Rc": FloatTag(write_tagname=False), + } + ), + ] + ), + "coulomb-truncation-embed": TagContainer( + subtags={ + "c0": FloatTag(write_tagname=False, optional=False), + "c1": FloatTag(write_tagname=False, optional=False), + "c2": FloatTag(write_tagname=False, optional=False), + } + ), + "coulomb-truncation-ion-margin": FloatTag(), + }, + "restart": { + "initial-state": StrTag(), + "elec-initial-eigenvals": StrTag(), + "elec-initial-fillings": TagContainer( + subtags={ + "read": BoolTag(write_value=False, optional=False), + "filename": StrTag(write_tagname=False, optional=False), + "nBandsOld": IntTag(write_tagname=False), + } + ), + "wavefunction": MultiformatTag( + format_options=[ + TagContainer(subtags={"lcao": BoolTag(write_value=False, optional=False)}), + TagContainer(subtags={"random": BoolTag(write_value=False, optional=False)}), + TagContainer( + subtags={ + "read": StrTag(write_value=False, optional=False), + "nBandsOld": IntTag(write_tagname=False), + "EcutOld": FloatTag(write_tagname=False), + } + ), + TagContainer( + subtags={ + "read-rs": StrTag(write_value=False, optional=False), + "nBandsOld": IntTag(write_tagname=False), + "NxOld": IntTag(write_tagname=False), + "NyOld": IntTag(write_tagname=False), + "NzOld": IntTag(write_tagname=False), + } + ), + ] + ), + "fluid-initial-state": StrTag(), + "perturb-incommensurate-wavefunctions": TagContainer( + subtags={ + "filename": StrTag(write_tagname=False, optional=False), + "EcutOld": IntTag(write_tagname=False), + } + ), + "perturb-rhoExternal": StrTag(), + "perturb-Vexternal": StrTag(), + "fix-electron-density": StrTag(), + "fix-electron-potential": StrTag(), + "Vexternal": MultiformatTag( + format_options=[ + TagContainer(subtags={"filename": StrTag(write_value=False, optional=False)}), + TagContainer( + subtags={ + "filenameUp": StrTag(write_value=False, optional=False), + "filenameDn": StrTag(write_tagname=False, optional=False), + } + ), + ] + ), + "rhoExternal": TagContainer( + subtags={ + "filename": StrTag(write_tagname=False, optional=False), + "includeSelfEnergy": FloatTag(write_tagname=False), + } + ), + "slab-epsilon": TagContainer( + subtags={ + "DtotFile": StrTag(write_tagname=False, optional=False), + "sigma": FloatTag(write_tagname=False, optional=False), + "Ex": FloatTag(write_tagname=False), + "Ey": FloatTag(write_tagname=False), + "Ez": FloatTag(write_tagname=False), + } + ), + }, + "minimization": { + "lcao-params": TagContainer( + subtags={ + "nIter": IntTag(write_tagname=False), + "Ediff": FloatTag(write_tagname=False), + "smearingWidth": FloatTag(write_tagname=False), + } + ), + "elec-eigen-algo": StrTag(options=["CG", "Davidson"]), + "ionic-minimize": TagContainer( + multiline_tag=True, + subtags={ + **deepcopy(jdftxminimize_subtagdict), + }, + ), + "lattice-minimize": TagContainer( + multiline_tag=True, + subtags={ + **deepcopy(jdftxminimize_subtagdict), + }, + ), + "electronic-minimize": TagContainer( + multiline_tag=True, + subtags={ + **deepcopy(jdftxminimize_subtagdict), + }, + ), + "electronic-scf": TagContainer( + multiline_tag=True, + subtags={ + "energyDiffThreshold": FloatTag(), + "history": IntTag(), + "mixFraction": FloatTag(), + "nIterations": IntTag(), + "qMetric": FloatTag(), + "residualThreshold": FloatTag(), + "eigDiffThreshold": FloatTag(), + "mixedVariable": StrTag(), + "mixFractionMag": FloatTag(), + "nEigSteps": IntTag(), + "qKappa": FloatTag(), + "qKerker": FloatTag(), + "verbose": BoolTag(), + }, + ), + "fluid-minimize": TagContainer( + multiline_tag=True, + subtags={ + **deepcopy(jdftxminimize_subtagdict), + }, + ), + "davidson-band-ratio": FloatTag(), + "wavefunction-drag": BoolTag(), + "subspace-rotation-factor": TagContainer( + subtags={ + "factor": FloatTag(write_tagname=False, optional=False), + "adjust": BoolTag(write_tagname=False, optional=False), + } + ), + "perturb-minimize": TagContainer( + multiline_tag=True, + subtags={ + "algorithm": StrTag(options=["MINRES", "CGIMINRES"]), + "CGBypass": BoolTag(), + "nIterations": IntTag(), + "recomputeResidual": BoolTag(), + "residualDiffThreshold": FloatTag(), + "residualTol": FloatTag(), + }, + ), + }, + "fluid": { + "target-mu": TagContainer( + allow_list_representation=True, + subtags={ + "mu": FloatTag(write_tagname=False, optional=False), + "outerLoop": BoolTag(write_tagname=False), + }, + ), + "fluid": TagContainer( + subtags={ + "type": StrTag( + options=[ + "None", + "LinearPCM", + "NonlinearPCM", + "SaLSA", + "ClassicalDFT", + ], + write_tagname=False, + optional=False, + ), + "Temperature": FloatTag(write_tagname=False), + "Pressure": FloatTag(write_tagname=False), + } + ), + "fluid-solvent": MultiformatTag( + can_repeat=True, # 11/27 + format_options=[ + TagContainer( + can_repeat=True, + subtags={ + "name": StrTag( + options=fluid_solvent_options, + write_tagname=False, + ), + "concentration": FloatTag(write_tagname=False), + "functional": StrTag( + options=[ + "BondedVoids", + "FittedCorrelations", + "MeanFieldLJ", + "ScalarEOS", + ], + write_tagname=False, + ), + **deepcopy(jdftxfluid_subtagdict), + }, + ), + TagContainer( + can_repeat=True, + subtags={ + "name": StrTag( + options=fluid_solvent_options, + write_tagname=False, + ), + "concentration": StrTag(options=["bulk"], write_tagname=False), + "functional": StrTag( + options=[ + "BondedVoids", + "FittedCorrelations", + "MeanFieldLJ", + "ScalarEOS", + ], + write_tagname=False, + ), + **deepcopy(jdftxfluid_subtagdict), + }, + ), + ], + ), + "fluid-anion": TagContainer( + subtags={ + "name": StrTag(options=["Cl-", "ClO4-", "F-"], write_tagname=False, optional=False), + "concentration": FloatTag(write_tagname=False, optional=False), + "functional": StrTag( + options=[ + "BondedVoids", + "FittedCorrelations", + "MeanFieldLJ", + "ScalarEOS", + ], + write_tagname=False, + ), + **deepcopy(jdftxfluid_subtagdict), + } + ), + "fluid-cation": TagContainer( + subtags={ + "name": StrTag(options=["K+", "Na+"], write_tagname=False, optional=False), + "concentration": FloatTag(write_tagname=False, optional=False), + "functional": StrTag( + options=[ + "BondedVoids", + "FittedCorrelations", + "MeanFieldLJ", + "ScalarEOS", + ], + write_tagname=False, + ), + **deepcopy(jdftxfluid_subtagdict), + } + ), + "fluid-dielectric-constant": TagContainer( + subtags={ + "epsBulkOverride": FloatTag(write_tagname=False), + "epsInfOverride": FloatTag(write_tagname=False), + } + ), + "fluid-dielectric-tensor": TagContainer( + subtags={ + "epsBulkXX": FloatTag(write_tagname=False, optional=False), + "epsBulkYY": FloatTag(write_tagname=False, optional=False), + "epsBulkZZ": FloatTag(write_tagname=False, optional=False), + } + ), + "fluid-ex-corr": TagContainer( + subtags={ + "kinetic": StrTag(write_tagname=False, optional=False, options=deepcopy(kinetic_functionals)), + "exchange-correlation": StrTag(write_tagname=False, options=deepcopy(func_options)), + } + ), + "fluid-mixing-functional": TagContainer( + can_repeat=True, + subtags={ + "fluid1": StrTag( + options=[ + "CCl4", + "CH3CN", + "CHCl3", + "Cl-", + "ClO4-", + "CustomAnion", + "CustomCation", + "F-", + "H2O", + "Na(H2O)4+", + "Na+", + ], + write_tagname=False, + optional=False, + ), + "fluid2": StrTag( + options=[ + "CCl4", + "CH3CN", + "CHCl3", + "Cl-", + "ClO4-", + "CustomAnion", + "CustomCation", + "F-", + "H2O", + "Na(H2O)4+", + "Na+", + ], + write_tagname=False, + optional=False, + ), + "energyScale": FloatTag(write_tagname=False, optional=False), + "lengthScale": FloatTag(write_tagname=False), + "FMixType": StrTag(options=["LJPotential", "GaussianKernel"], write_tagname=False), + }, + ), + "fluid-vdwScale": FloatTag(), + "fluid-gummel-loop": TagContainer( + subtags={ + "maxIterations": IntTag(write_tagname=False, optional=False), + "Atol": FloatTag(write_tagname=False, optional=False), + } + ), + "fluid-solve-frequency": StrTag(options=["Default", "Gummel", "Inner"]), + "fluid-site-params": TagContainer( + multiline_tag=True, + can_repeat=True, + subtags={ + "component": StrTag( + options=[ + "CCl4", + "CH3CN", + "CHCl3", + "Cl-", + "ClO4-", + "CustomAnion", + "CustomCation", + "F-", + "H2O", + "Na(H2O)4+", + "Na+", + ], + optional=False, + ), + "siteName": StrTag(optional=False), + "aElec": FloatTag(), + "alpha": FloatTag(), + "aPol": FloatTag(), + "elecFilename": StrTag(), + "elecFilenameG": StrTag(), + "rcElec": FloatTag(), + "Rhs": FloatTag(), + "sigmaElec": FloatTag(), + "sigmaNuc": FloatTag(), + "Zelec": FloatTag(), + "Znuc": FloatTag(), + }, + ), + "pcm-variant": StrTag( + options=[ + "CANDLE", + "CANON", + "FixedCavity", + "GLSSA13", + "LA12", + "SCCS_anion", + "SCCS_cation", + "SCCS_g03", + "SCCS_g03beta", + "SCCS_g03p", + "SCCS_g03pbeta", + "SCCS_g09", + "SCCS_g09beta", + "SGA13", + "SoftSphere", + ] + ), + "pcm-nonlinear-scf": TagContainer( + multiline_tag=True, + subtags={ + "energyDiffThreshold": FloatTag(), + "history": IntTag(), + "mixFraction": FloatTag(), + "nIterations": IntTag(), + "qMetric": FloatTag(), + "residualThreshold": FloatTag(), + }, + ), + "pcm-params": TagContainer( + multiline_tag=True, + subtags={ + "cavityFile": StrTag(), + "cavityPressure": FloatTag(), + "cavityScale": FloatTag(), + "cavityTension": FloatTag(), + "eta_wDiel": FloatTag(), + "ionSpacing": FloatTag(), + "lMax": FloatTag(), + "nc": FloatTag(), + "pCavity": FloatTag(), + "rhoDelta": FloatTag(), + "rhoMax": FloatTag(), + "rhoMin": FloatTag(), + "screenOverride": FloatTag(), + "sigma": FloatTag(), + "sqrtC6eff": FloatTag(), + "Zcenter": FloatTag(), + "zMask0": FloatTag(), + "zMaskH": FloatTag(), + "zMaskIonH": FloatTag(), + "zMaskSigma": FloatTag(), + "Ztot": FloatTag(), + }, + ), + }, + "dynamics": { + "vibrations": TagContainer( + subtags={ + "dr": FloatTag(), + "centralDiff": BoolTag(), + "useConstraints": BoolTag(), + "translationSym": BoolTag(), + "rotationSym": BoolTag(), + "omegaMin": FloatTag(), + "T": FloatTag(), + "omegaResolution": FloatTag(), + } + ), + "barostat-velocity": TagContainer( + subtags={ + "v1": FloatTag(write_tagname=False, optional=False), + "v2": FloatTag(write_tagname=False, optional=False), + "v3": FloatTag(write_tagname=False, optional=False), + "v4": FloatTag(write_tagname=False, optional=False), + "v5": FloatTag(write_tagname=False, optional=False), + "v6": FloatTag(write_tagname=False, optional=False), + "v7": FloatTag(write_tagname=False, optional=False), + "v8": FloatTag(write_tagname=False, optional=False), + "v9": FloatTag(write_tagname=False, optional=False), + } + ), + "thermostat-velocity": TagContainer( + subtags={ + "v1": FloatTag(write_tagname=False, optional=False), + "v2": FloatTag(write_tagname=False, optional=False), + "v3": FloatTag(write_tagname=False, optional=False), + } + ), + "ionic-dynamics": TagContainer( + multiline_tag=True, + subtags={ + "B0": FloatTag(), + "chainLengthP": FloatTag(), + "chainLengthT": FloatTag(), + "dt": FloatTag(), + "nSteps": IntTag(), + "P0": FloatTag(), # can accept numpy.nan + "statMethod": StrTag(options=["Berendsen", "None", "NoseHoover"]), + "stress0": TagContainer( # can accept numpy.nan + subtags={ + "xx": FloatTag(write_tagname=False, optional=False), + "yy": FloatTag(write_tagname=False, optional=False), + "zz": FloatTag(write_tagname=False, optional=False), + "yz": FloatTag(write_tagname=False, optional=False), + "zx": FloatTag(write_tagname=False, optional=False), + "xy": FloatTag(write_tagname=False, optional=False), + } + ), + "T0": FloatTag(), + "tDampP": FloatTag(), + "tDampT": FloatTag(), + }, + ), + }, + "export": { + "dump-name": StrTag(), + "dump-interval": TagContainer( + can_repeat=True, + subtags={ + "freq": StrTag( + options=["Ionic", "Electronic", "Fluid", "Gummel"], + write_tagname=False, + optional=False, + ), + "var": IntTag(write_tagname=False, optional=False), + }, + ), + "dump-only": BoolTag(write_value=False), + "band-projection-params": TagContainer( + subtags={ + "ortho": BoolTag(write_tagname=False, optional=False), + "norm": BoolTag(write_tagname=False, optional=False), + } + ), + "density-of-states": TagContainer( + multiline_tag=True, + subtags={ + "Total": BoolTag(write_value=False), + "Slice": TagContainer( + can_repeat=True, + subtags={ + "c0": FloatTag(write_tagname=False, optional=False), + "c1": FloatTag(write_tagname=False, optional=False), + "c2": FloatTag(write_tagname=False, optional=False), + "r": FloatTag(write_tagname=False, optional=False), + "i0": FloatTag(write_tagname=False, optional=False), + "i1": FloatTag(write_tagname=False, optional=False), + "i2": FloatTag(write_tagname=False, optional=False), + }, + ), + "Sphere": TagContainer( + can_repeat=True, + subtags={ + "c0": FloatTag(write_tagname=False, optional=False), + "c1": FloatTag(write_tagname=False, optional=False), + "c2": FloatTag(write_tagname=False, optional=False), + "r": FloatTag(write_tagname=False, optional=False), + }, + ), + "AtomSlice": TagContainer( + can_repeat=True, + subtags={ + "species": StrTag(write_tagname=False, optional=False), + "atomIndex": IntTag(write_tagname=False, optional=False), + "r": FloatTag(write_tagname=False, optional=False), + "i0": FloatTag(write_tagname=False, optional=False), + "i1": FloatTag(write_tagname=False, optional=False), + "i2": FloatTag(write_tagname=False, optional=False), + }, + ), + "AtomSphere": TagContainer( + can_repeat=True, + subtags={ + "species": StrTag(write_tagname=False, optional=False), + "atomIndex": IntTag(write_tagname=False, optional=False), + "r": FloatTag(write_tagname=False, optional=False), + }, + ), + "File": StrTag(), + "Orbital": TagContainer( + can_repeat=True, + subtags={ + "species": StrTag(write_tagname=False, optional=False), + "atomIndex": IntTag(write_tagname=False, optional=False), + "orbDesc": StrTag(write_tagname=False, optional=False), + }, + ), + "OrthoOrbital": TagContainer( + can_repeat=True, + subtags={ + "species": StrTag(write_tagname=False, optional=False), + "atomIndex": IntTag(write_tagname=False, optional=False), + "orbDesc": StrTag(write_tagname=False, optional=False), + }, + ), + "Etol": FloatTag(), + "Esigma": FloatTag(), + "EigsOverride": StrTag(), + "Occupied": BoolTag(write_value=False), + "Complete": BoolTag(write_value=False), + "SpinProjected": TagContainer( + can_repeat=True, + subtags={ + "theta": FloatTag(write_tagname=False, optional=False), + "phi": FloatTag(write_tagname=False, optional=False), + }, + ), + "SpinTotal": BoolTag(write_value=False), + }, + ), + "dump-Eresolved-density": TagContainer( + subtags={ + "Emin": FloatTag(write_tagname=False, optional=False), + "Emax": FloatTag(write_tagname=False, optional=False), + } + ), + "dump-fermi-density": MultiformatTag( + can_repeat=True, + format_options=[ + BoolTag(write_value=False), + FloatTag(), + ], + ), + "bgw-params": TagContainer( + multiline_tag=True, + subtags={ + "nBandsDense": IntTag(), + "nBandsV": IntTag(), + "blockSize": IntTag(), + "clusterSize": IntTag(), + "Ecut_rALDA": FloatTag(), + "EcutChiFluid": FloatTag(), + "rpaExx": BoolTag(), + "saveVxc": BoolTag(), + "saveVxx": BoolTag(), + "offDiagV": BoolTag(), + "elecOnly": BoolTag(), + "freqBroaden_eV": FloatTag(), + "freqNimag": IntTag(), + "freqPlasma": FloatTag(), + "freqReMax_eV": FloatTag(), + "freqReStep_eV": FloatTag(), + "kernelSym_rALDA": BoolTag(), + "kFcut_rALDA": FloatTag(), + "q0": TagContainer( + subtags={ + "q0x": FloatTag(write_tagname=False, optional=False), + "q0y": FloatTag(write_tagname=False, optional=False), + "q0z": FloatTag(write_tagname=False, optional=False), + } + ), + }, + ), + "forces-output-coords": StrTag(options=["Cartesian", "Contravariant", "Lattice", "Positions"]), + "polarizability": TagContainer( + subtags={ + "eigenBasis": StrTag( + options=["External", "NonInteracting", "Total"], + write_tagname=False, + optional=False, + ), + "Ecut": FloatTag(write_tagname=False), + "nEigs": IntTag(write_tagname=False), + } + ), + "polarizability-kdiff": TagContainer( + subtags={ + "dk0": FloatTag(write_tagname=False, optional=False), + "dk1": FloatTag(write_tagname=False, optional=False), + "dk2": FloatTag(write_tagname=False, optional=False), + "dkFilenamePattern": StrTag(write_tagname=False), + } + ), + "potential-subtraction": BoolTag(), + }, + "misc": { + "debug": StrTag( + options=[ + "Ecomponents", + "EigsFillings", + "Fluid", + "Forces", + "KpointsBasis", + "MuSearch", + "Symmetries", + ], + can_repeat=True, + ), + "pcm-nonlinear-debug": TagContainer( + subtags={ + "linearDielectric": BoolTag(write_tagname=False, optional=False), + "linearScreening": BoolTag(write_tagname=False, optional=False), + } + ), + }, +} + + +def get_dump_tag_container() -> DumpTagContainer: + """ + Initialize a dump tag container. + + Returns: + DumpTagContainer: The dump tag container. + """ + subtags2: dict[str, AbstractTag] = {} # Called "subtags2" to avoid name conflict with the "subtags" variable + for freq in jdftxdumpfreqoptions: + subsubtags: dict[str, AbstractTag] = {} + for var in jdftxdumpvaroptions: + subsubtags[var] = BoolTag(write_value=False) + subtags2[freq] = BoolTagContainer(subtags=subsubtags, write_tagname=True) + return DumpTagContainer(subtags=subtags2, write_tagname=True, can_repeat=True) + + +MASTER_TAG_LIST["export"]["dump"] = get_dump_tag_container() + + +__PHONON_TAGS__: list[str] = ["phonon"] +__WANNIER_TAGS__: list[str] = [ + "wannier", + "wannier-center-pinned", + "wannier-dump-name", + "wannier-initial-state", + "wannier-minimize", + "defect-supercell", +] +__TAG_LIST__ = [tag for group in MASTER_TAG_LIST for tag in MASTER_TAG_LIST[group]] +__TAG_GROUPS__ = {tag: group for group in MASTER_TAG_LIST for tag in MASTER_TAG_LIST[group]} + + +def get_tag_object(tag: str) -> AbstractTag: + """Get the tag object for a given tag name. + + Args: + tag (str): The tag name. + + Returns: + AbstractTag: The tag object. + """ + return MASTER_TAG_LIST[__TAG_GROUPS__[tag]][tag] diff --git a/src/pymatgen/io/jdftx/jdftxinfile_ref_options.py b/src/pymatgen/io/jdftx/jdftxinfile_ref_options.py new file mode 100644 index 00000000000..3b8055a5239 --- /dev/null +++ b/src/pymatgen/io/jdftx/jdftxinfile_ref_options.py @@ -0,0 +1,735 @@ +"""Module for containing reference data for JDFTx tags. + +This module contains reference data for JDFTx tags, such as valid options for +functionals, pseudopotentials, etc. + +@mkhorton - this file is ready to review. +""" + +from __future__ import annotations + +from pymatgen.io.jdftx.generic_tags import BoolTag, FloatTag, IntTag, StrTag, TagContainer + +fluid_solvent_options = [ + "CarbonDisulfide", + "CCl4", + "CH2Cl2", + "CH3CN", + "Chlorobenzene", + "DMC", + "DMF", + "DMSO", + "EC", + "Ethanol", + "EthyleneGlycol", + "EthylEther", + "Glyme", + "H2O", + "Isobutanol", + "Methanol", + "Octanol", + "PC", + "THF", +] + +# Generated by ChatGPT to include all options for in https://jdftx.org/CommandElecExCorr.html +func_options = [ + "gga", # Perdew-Burke-Ernzerhof GGA + "gga-PBE", # Perdew-Burke-Ernzerhof GGA + "gga-PBEsol", # Perdew-Burke-Ernzerhof GGA reparametrized for solids + "gga-PW91", # Perdew-Wang GGA + "Hartree-Fock", # Full exact exchange with no correlation + # "hyb-HSE06", # HSE06 'wPBEh' hybrid with 1/4 screened exact exchange + # ^ Kept for completeness, but commented out due to bug in JDFTx, use (hyb-gga-HSE06) instead + "hyb-HSE12", # Reparametrized screened exchange functional for accuracy (w=0.185 A^-1 and a=0.313) + "hyb-HSE12s", # Reparametrized screened exchange functional for k-point convergence (w=0.408 A^-1 and a=0.425) + "hyb-PBE0", # Hybrid PBE with 1/4 exact exchange + "lda", # Perdew-Zunger LDA + "lda-PW", # Perdew-Wang LDA + "lda-PW-prec", # Perdew-Wang LDA with extended precision (used by PBE) + "lda-PZ", # Perdew-Zunger LDA + "lda-Teter", # Teter93 LSDA + "lda-VWN", # Vosko-Wilk-Nusair LDA + "mgga-revTPSS", # revised Tao-Perdew-Staroverov-Scuseria meta GGA + "mgga-TPSS", # Tao-Perdew-Staroverov-Scuseria meta GGA + "orb-GLLBsc", # Orbital-dependent GLLB-sc potential (no total energy) + "pot-LB94", # van Leeuwen-Baerends model potential (no total energy) +] + +# Generated by ChatGPT to include all options for in https://jdftx.org/CommandElecExCorr.html +func_x_options = [ + "gga-x-2d-b86", # Becke 86 in 2D + "gga-x-2d-b86-mgc", # Becke 86 with modified gradient correction for 2D + "gga-x-2d-b88", # Becke 88 in 2D + "gga-x-2d-pbe", # Perdew, Burke & Ernzerhof in 2D + "gga-x-airy", # Constantin et al based on the Airy gas + "gga-x-ak13", # Armiento & Kuemmel 2013 + "gga-x-am05", # Armiento & Mattsson 05 + "gga-x-apbe", # mu fixed from the semiclassical neutral atom + "gga-x-b86", # Becke 86 + "gga-x-b86-mgc", # Becke 86 with modified gradient correction + "gga-x-b86-r", # Revised Becke 86 with modified gradient correction + "gga-x-b88", # Becke 88 + "gga-x-b88-6311g", # Becke 88 reoptimized with the 6-311G** basis set + "gga-x-b88m", # Becke 88 reoptimized to be used with tau1 + "gga-x-bayesian", # Bayesian best fit for the enhancement factor + "gga-x-bcgp", # Burke, Cancio, Gould, and Pittalis + "gga-x-beefvdw", # BEEF-vdW exchange + "gga-x-bpccac", # BPCCAC (GRAC for the energy) + "gga-x-c09x", # C09x to be used with the VdW of Rutgers-Chalmers + "gga-x-cap", # Correct Asymptotic Potential + "gga-x-chachiyo", # Chachiyo exchange + "gga-x-dk87-r1", # dePristo & Kress 87 version R1 + "gga-x-dk87-r2", # dePristo & Kress 87 version R2 + "gga-x-eb88", # Non-empirical (excogitated) B88 functional of Becke and Elliott + "gga-x-ecmv92", # Engel, Chevary, Macdonald and Vosko + "gga-x-ev93", # Engel and Vosko + "gga-x-fd-lb94", # Functional derivative recovered from the stray LB94 potential + "gga-x-fd-revlb94", # Revised FD_LB94 + "gga-x-ft97-a", # Filatov & Thiel 97 (version A) + "gga-x-ft97-b", # Filatov & Thiel 97 (version B) + "gga-x-g96", # Gill 96 + "gga-x-gam", # Minnesota GAM exchange functional + "gga-x-gg99", # Gilbert and Gill 1999 + "gga-x-hcth-a", # HCTH-A + "gga-x-herman", # Herman Xalphabeta GGA + "gga-x-hjs-b88", # HJS screened exchange B88 version + "gga-x-hjs-b88-v2", # HJS screened exchange B88 corrected version + "gga-x-hjs-b97x", # HJS screened exchange B97x version + "gga-x-hjs-pbe", # HJS screened exchange PBE version + "gga-x-hjs-pbe-sol", # HJS screened exchange PBE_SOL version + "gga-x-htbs", # Haas, Tran, Blaha, and Schwarz + "gga-x-ityh", # Short-range recipe for B88 functional - erf + "gga-x-ityh-optx", # Short-range recipe for OPTX functional + "gga-x-ityh-pbe", # Short-range recipe for PBE functional + "gga-x-kgg99", # Gilbert and Gill 1999 (mixed) + "gga-x-kt1", # Exchange part of Keal and Tozer version 1 + "gga-x-lag", # Local Airy Gas + "gga-x-lambda-ch-n", # lambda_CH(N) version of PBE + "gga-x-lambda-lo-n", # lambda_LO(N) version of PBE + "gga-x-lambda-oc2-n", # lambda_OC2(N) version of PBE + "gga-x-lb", # van Leeuwen & Baerends + "gga-x-lbm", # van Leeuwen & Baerends modified + "gga-x-lg93", # Lacks & Gordon 93 + "gga-x-lspbe", # lsPBE, a PW91-like modification of PBE exchange + "gga-x-lsrpbe", # lsRPBE, a PW91-like modification of RPBE + "gga-x-lv-rpw86", # Berland and Hyldgaard + "gga-x-mb88", # Modified Becke 88 for proton transfer + "gga-x-mpbe", # Adamo & Barone modification to PBE + "gga-x-mpw91", # mPW91 of Adamo & Barone + "gga-x-n12", # Minnesota N12 exchange functional + "gga-x-ncap", # Nearly correct asymptotic potential + "gga-x-ncapr", # Nearly correct asymptotic potential revised + "gga-x-ol2", # Exchange form based on Ou-Yang and Levy v.2 + "gga-x-optb86b-vdw", # Becke 86 reoptimized for use with vdW functional of Dion et al + "gga-x-optb88-vdw", # opt-Becke 88 for vdW + "gga-x-optpbe-vdw", # Reparametrized PBE for vdW + "gga-x-optx", # Handy & Cohen OPTX 01 + "gga-x-pbe", # Perdew, Burke & Ernzerhof + "gga-x-pbe-gaussian", # Perdew, Burke & Ernzerhof with parameter values used in Gaussian + "gga-x-pbe-jsjr", # Reparametrized PBE by Pedroza, Silva & Capelle + "gga-x-pbe-mod", # Perdew, Burke & Ernzerhof with less precise value for beta + "gga-x-pbe-mol", # Reparametrized PBE by del Campo, Gazquez, Trickey & Vela + "gga-x-pbe-r", # Revised PBE from Zhang & Yang + "gga-x-pbe-sol", # Perdew, Burke & Ernzerhof SOL + "gga-x-pbe-tca", # PBE revised by Tognetti et al + "gga-x-pbea", # Madsen 07 + "gga-x-pbefe", # PBE for formation energies + "gga-x-pbeint", # PBE for hybrid interfaces + "gga-x-pbek1-vdw", # Reparametrized PBE for vdW + "gga-x-pbepow", # PBE power + "gga-x-pbetrans", # Gradient-regulated connection-based correction for the PBE exchange + "gga-x-pw86", # Perdew & Wang 86 + "gga-x-pw91", # Perdew & Wang 91 + "gga-x-pw91-mod", # PW91, alternate version with more digits + "gga-x-q1d", # Functional for quasi-1D systems + "gga-x-q2d", # Chiodo et al + "gga-x-revssb-d", # Revised Swart, Sola and Bickelhaupt dispersion + "gga-x-rge2", # Regularized PBE + "gga-x-rpbe", # Hammer, Hansen, and Norskov + "gga-x-rpw86", # Refitted Perdew & Wang 86 + "gga-x-s12g", # Swart 2012 GGA exchange + "gga-x-sfat", # Short-range recipe for B88 functional - Yukawa + "gga-x-sfat-pbe", # Short-range recipe for PBE functional - Yukawa + "gga-x-sg4", # Semiclassical GGA at fourth order + "gga-x-sogga", # Second-order generalized gradient approximation + "gga-x-sogga11", # Second-order generalized gradient approximation 2011 + "gga-x-ssb", # Swart, Sola and Bickelhaupt + "gga-x-ssb-d", # Swart, Sola and Bickelhaupt dispersion + "gga-x-ssb-sw", # Swart, Sola and Bickelhaupt correction to PBE + "gga-x-vmt-ge", # Vela, Medel, and Trickey with mu = mu_GE + "gga-x-vmt-pbe", # Vela, Medel, and Trickey with mu = mu_PBE +] + +# Generated by ChatGPT to include all options for in https://jdftx.org/CommandElecExCorr.html +func_c_options = [ + "c-none", # no correlation + "gga-c-acgga", # acGGA, asymptotically corrected GGA correlation + "gga-c-acggap", # acGGA+, asymptotically corrected GGA correlation+ + "gga-c-am05", # Armiento & Mattsson 05 + "gga-c-apbe", # mu fixed from the semiclassical neutral atom + "gga-c-bmk", # Boese-Martin correlation for kinetics + "gga-c-ccdf", # ccDF: coupled-cluster motivated density functional + "gga-c-chachiyo", # Chachiyo simple GGA correlation + "gga-c-cs1", # A dynamical correlation functional + "gga-c-ft97", # Filatov & Thiel correlation + "gga-c-gam", # Minnesota GAM correlation functional + "gga-c-gapc", # GapC + "gga-c-gaploc", # Gaploc + "gga-c-hcth-a", # HCTH-A + "gga-c-lm", # Langreth & Mehl + "gga-c-lyp", # Lee, Yang & Parr + "gga-c-lypr", # Short-range LYP by Ai, Fang, and Su + "gga-c-mggac", # beta fitted to LC20 to be used with MGGAC + "gga-c-n12", # Minnesota N12 correlation functional + "gga-c-n12-sx", # Minnesota N12-SX correlation functional + "gga-c-op-b88", # one-parameter progressive functional (B88 version) + "gga-c-op-g96", # one-parameter progressive functional (G96 version) + "gga-c-op-pbe", # one-parameter progressive functional (PBE version) + "gga-c-op-pw91", # one-parameter progressive functional (PW91 version) + "gga-c-op-xalpha", # one-parameter progressive functional (Xalpha version) + "gga-c-optc", # Optimized correlation functional of Cohen and Handy + "gga-c-p86", # Perdew 86 + "gga-c-p86-ft", # Perdew 86 with more accurate value for ftilde + "gga-c-p86vwn", # Perdew 86 based on VWN5 correlation + "gga-c-p86vwn-ft", # Perdew 86 based on VWN5 correlation, with more accurate value for ftilde + "gga-c-pbe", # Perdew, Burke & Ernzerhof + "gga-c-pbe-gaussian", # Perdew, Burke & Ernzerhof with parameters from Gaussian + "gga-c-pbe-jrgx", # Reparametrized PBE by Pedroza, Silva & Capelle + "gga-c-pbe-mol", # Reparametrized PBE by del Campo, Gazquez, Trickey & Vela + "gga-c-pbe-sol", # Perdew, Burke & Ernzerhof SOL + "gga-c-pbe-vwn", # Perdew, Burke & Ernzerhof based on VWN correlation + "gga-c-pbefe", # PBE for formation energies + "gga-c-pbeint", # PBE for hybrid interfaces + "gga-c-pbeloc", # Semilocal dynamical correlation + "gga-c-pw91", # Perdew & Wang 91 + "gga-c-q2d", # Chiodo et al + "gga-c-regtpss", # regularized TPSS correlation + "gga-c-revtca", # Tognetti, Cortona, Adamo (revised) + "gga-c-rge2", # Regularized PBE + "gga-c-scan-e0", # GGA component of SCAN + "gga-c-sg4", # Semiclassical GGA at fourth order + "gga-c-sogga11", # Second-order generalized gradient approximation 2011 + "gga-c-sogga11-x", # To be used with HYB_GGA_X_SOGGA11_X + "gga-c-spbe", # PBE correlation to be used with the SSB exchange + "gga-c-tau-hcth", # correlation part of tau-hcth + "gga-c-tca", # Tognetti, Cortona, Adamo + "gga-c-tm-lyp", # Takkar and McCarthy reparametrization + "gga-c-tm-pbe", # Thakkar and McCarthy reparametrization + "gga-c-w94", # Wilson 94 (Eq. 25) + "gga-c-wi", # Wilson & Ivanov + "gga-c-wi0", # Wilson & Ivanov initial version + "gga-c-wl", # Wilson & Levy + "gga-c-xpbe", # Extended PBE by Xu & Goddard III + "gga-c-zpbeint", # spin-dependent gradient correction to PBEint + "gga-c-zpbesol", # spin-dependent gradient correction to PBEsol + "gga-c-zvpbeint", # another spin-dependent correction to PBEint + "gga-c-zvpbeloc", # PBEloc variation with enhanced compatibility with exact exchange + "gga-c-zvpbesol", # another spin-dependent correction to PBEsol + "lda-c-1d-csc", # Casula, Sorella & Senatore + "lda-c-1d-loos", # P-F _Loos correlation LDA + "lda-c-2d-amgb", # AMGB (for 2D systems) + "lda-c-2d-prm", # PRM (for 2D systems) + "lda-c-br78", # Brual & Rothstein 78 + "lda-c-chachiyo", # Chachiyo simple 2 parameter correlation + "lda-c-chachiyo-mod", # Chachiyo simple 2 parameter correlation with modified spin scaling + "lda-c-gk72", # Gordon and Kim 1972 + "lda-c-gl", # Gunnarson & Lundqvist + "lda-c-gombas", # Gombas + "lda-c-hl", # Hedin & Lundqvist + "lda-c-karasiev", # Karasiev reparameterization of Chachiyo + "lda-c-karasiev-mod", # Karasiev reparameterization of Chachiyo + "lda-c-lp96", # Liu-Parr correlation + "lda-c-mcweeny", # McWeeny 76 + "lda-c-ml1", # Modified LSD (version 1) of Proynov and Salahub + "lda-c-ml2", # Modified LSD (version 2) of Proynov and Salahub + "lda-c-ob-pw", # Ortiz & Ballone (PW parametrization) + "lda-c-ob-pz", # Ortiz & Ballone (PZ parametrization) + "lda-c-ow", # Optimized Wigner + "lda-c-ow-lyp", # Wigner with corresponding LYP parameters + "lda-c-pk09", # Proynov and Kong 2009 + "lda-c-pmgb06", # Long-range LDA correlation functional + "lda-c-pw", # Perdew & Wang + "lda-c-pw-mod", # Perdew & Wang (modified) + "lda-c-pw-rpa", # Perdew & Wang (fit to the RPA energy) + "lda-c-pz", # Perdew & Zunger + "lda-c-pz-mod", # Perdew & Zunger (Modified) + "lda-c-rc04", # Ragot-Cortona + "lda-c-rpa", # Random Phase Approximation (RPA) + "lda-c-rpw92", # Ruggeri, Rios, and Alavi restricted fit + "lda-c-upw92", # Ruggeri, Rios, and Alavi unrestricted fit + "lda-c-vbh", # von Barth & Hedin + "lda-c-vwn", # Vosko, Wilk & Nusair (VWN5) + "lda-c-vwn-1", # Vosko, Wilk & Nusair (VWN1) + "lda-c-vwn-2", # Vosko, Wilk & Nusair (VWN2) + "lda-c-vwn-3", # Vosko, Wilk & Nusair (VWN3) + "lda-c-vwn-4", # Vosko, Wilk & Nusair (VWN4) + "lda-c-vwn-rpa", # Vosko, Wilk & Nusair (VWN5_RPA) + "lda-c-w20", # Xie, Wu, and Zhao interpolation ansatz without fitting parameters + "lda-c-wigner", # Wigner + "lda-c-xalpha", # Slater's Xalpha + "mgga-c-b88", # Meta-GGA correlation by Becke + "mgga-c-b94", # Becke 1994 meta-GGA correlation + "mgga-c-bc95", # Becke correlation 95 + "mgga-c-cc", # Self-interaction corrected correlation functional by Schmidt et al + "mgga-c-ccalda", # Iso-orbital corrected LDA correlation by Lebeda et al + "mgga-c-cs", # Colle and Salvetti + "mgga-c-dldf", # Dispersionless Density Functional + "mgga-c-hltapw", # Half-and-half meta-LDAized PW correlation by Lehtola and Marques + "mgga-c-kcis", # Krieger, Chen, Iafrate, and _Savin + "mgga-c-kcisk", # Krieger, Chen, and Kurth + "mgga-c-m05", # Minnesota M05 correlation functional + "mgga-c-m05-2x", # Minnesota M05-2X correlation functional + "mgga-c-m06", # Minnesota M06 correlation functional + "mgga-c-m06-2x", # Minnesota M06-2X correlation functional + "mgga-c-m06-hf", # Minnesota M06-HF correlation functional + "mgga-c-m06-l", # Minnesota M06-L correlation functional + "mgga-c-m06-sx", # Minnesota M06-SX correlation functional + "mgga-c-m08-hx", # Minnesota M08 correlation functional + "mgga-c-m08-so", # Minnesota M08-SO correlation functional + "mgga-c-m11", # Minnesota M11 correlation functional + "mgga-c-m11-l", # Minnesota M11-L correlation functional + "mgga-c-mn12-l", # Minnesota MN12-L correlation functional + "mgga-c-mn12-sx", # Minnesota MN12-SX correlation functional + "mgga-c-mn15", # Minnesota MN15 correlation functional + "mgga-c-mn15-l", # Minnesota MN15-L correlation functional + "mgga-c-pkzb", # Perdew, Kurth, Zupan, and Blaha + "mgga-c-r2scan", # Re-regularized SCAN correlation by Furness et al + "mgga-c-r2scan01", # Re-regularized SCAN correlation with larger value for eta + "mgga-c-r2scanl", # Deorbitalized re-regularized SCAN (r2SCAN-L) correlation + "mgga-c-revm06", # Revised Minnesota M06 correlation functional + "mgga-c-revm06-l", # Minnesota revM06-L correlation functional + "mgga-c-revm11", # Revised Minnesota M11 correlation functional + "mgga-c-revscan", # revised SCAN + "mgga-c-revscan-vv10", # REVSCAN + VV10 correlation + "mgga-c-revtm", # revised Tao and Mo 2016 exchange + "mgga-c-revtpss", # revised TPSS correlation + "mgga-c-rmggac", # Revised correlation energy for MGGAC exchange functional + "mgga-c-rppscan", # r++SCAN: rSCAN with uniform density limit and coordinate scaling behavior + "mgga-c-rregtm", # Revised regTM correlation by Jana et al + "mgga-c-rscan", # Regularized SCAN correlation by Bartok and Yates + "mgga-c-scan", # SCAN correlation of Sun, Ruzsinszky, and Perdew + "mgga-c-scan-rvv10", # SCAN + rVV10 correlation + "mgga-c-scan-vv10", # SCAN + VV10 correlation + "mgga-c-scanl", # Deorbitalized SCAN (SCAN-L) correlation + "mgga-c-scanl-rvv10", # SCAN-L + rVV10 correlation + "mgga-c-scanl-vv10", # SCAN-L + VV10 correlation + "mgga-c-tm", # Tao and Mo 2016 correlation + "mgga-c-tpss", # Tao, Perdew, Staroverov & Scuseria + "mgga-c-tpss-gaussian", # Tao, Perdew, Staroverov & Scuseria with parameters from Gaussian + "mgga-c-tpssloc", # Semilocal dynamical correlation + "mgga-c-vsxc", # VSXC (correlation part) +] + +# Generated by ChatGPT to include all options for in https://jdftx.org/CommandElecExCorr.html +func_xc_options = [ + "gga-b97-3c", # Becke 97-3c by Grimme et. al. + "gga-b97-d", # Becke 97-D + "gga-b97-gga1", # Becke 97 GGA-1 + "gga-beefvdw", # BEEF-vdW exchange-correlation + "gga-edf1", # EDF1 + "gga-hcth-120", # HCTH/120 + "gga-hcth-147", # HCTH/147 + "gga-hcth-407", # HCTH/407 + "gga-hcth-407p", # HCTH/407+ + "gga-hcth-93", # HCTH/93 + "gga-hcth-p14", # HCTH p=1/4 + "gga-hcth-p76", # HCTH p=7/6 + "gga-hle16", # High local exchange 2016 + "gga-kt1", # Keal and Tozer, version 1 + "gga-kt2", # Keal and Tozer, version 2 + "gga-kt3", # Keal and Tozer, version 3 + "gga-mohlyp", # Functional for organometallic chemistry + "gga-mohlyp2", # Functional for barrier heights + "gga-mpwlyp1w", # mPWLYP1w + "gga-ncap", # NCAP exchange + P86 correlation + "gga-oblyp-d", # oBLYP-D functional of Goerigk and Grimme + "gga-opbe-d", # oPBE-D functional of Goerigk and Grimme + "gga-opwlyp-d", # oPWLYP-D functional of Goerigk and Grimme + "gga-pbe1w", # PBE1W + "gga-pbelyp1w", # PBELYP1W + "gga-th-fc", # Tozer and Handy v. FC + "gga-th-fcfo", # Tozer and Handy v. FCFO + "gga-th-fco", # Tozer and Handy v. FCO + "gga-th-fl", # Tozer and Handy v. FL + "gga-th1", # Tozer and Handy v. 1 + "gga-th2", # Tozer and Handy v. 2 + "gga-th3", # Tozer and Handy v. 3 + "gga-th4", # Tozer and Handy v. 4 + "gga-vv10", # Vydrov and Van Voorhis + "gga-xlyp", # XLYP + "hyb-gga-apbe0", # Hybrid based on APBE + "hyb-gga-apf", # APF hybrid functional + "hyb-gga-b1lyp", # B1LYP + "hyb-gga-b1pw91", # B1PW91 + "hyb-gga-b1wc", # B1WC + "hyb-gga-b3lyp", # B3LYP + "hyb-gga-b3lyp-mcm1", # B3LYP-MCM1 + "hyb-gga-b3lyp-mcm2", # B3LYP-MCM2 + "hyb-gga-b3lyp3", # B3LYP with VWN functional 3 instead of RPA + "hyb-gga-b3lyp5", # B3LYP with VWN functional 5 instead of RPA + "hyb-gga-b3lyps", # B3LYP* + "hyb-gga-b3p86", # B3P86 + "hyb-gga-b3p86-nwchem", # B3P86, NWChem version + "hyb-gga-b3pw91", # The original (ACM, B3PW91) hybrid of Becke + "hyb-gga-b5050lyp", # B5050LYP + "hyb-gga-b97", # Becke 97 + "hyb-gga-b97-1", # Becke 97-1 + "hyb-gga-b97-1p", # Version of B97 by Cohen and Handy + "hyb-gga-b97-2", # Becke 97-2 + "hyb-gga-b97-3", # Becke 97-3 + "hyb-gga-b97-k", # Boese-Martin for Kinetics + "hyb-gga-bhandh", # BHandH i.e. BHLYP + "hyb-gga-bhandhlyp", # BHandHLYP + "hyb-gga-blyp35", # BLYP35 + "hyb-gga-cam-b3lyp", # CAM version of B3LYP + "hyb-gga-cam-o3lyp", # CAM-O3LYP + "hyb-gga-cam-pbeh", # CAM hybrid screened exchange PBE version + "hyb-gga-cam-qtp-00", # CAM-B3LYP re-tuned using ionization potentials of water + "hyb-gga-cam-qtp-01", # CAM-B3LYP re-tuned using ionization potentials of water + "hyb-gga-cam-qtp-02", # CAM-B3LYP re-tuned using ionization potentials of water + "hyb-gga-camh-b3lyp", # CAM version of B3LYP, tuned for TDDFT + "hyb-gga-camy-b3lyp", # CAMY version of B3LYP + "hyb-gga-camy-blyp", # CAMY version of BLYP + "hyb-gga-camy-pbeh", # CAMY hybrid screened exchange PBE version + "hyb-gga-cap0", # Correct Asymptotic Potential hybrid + "hyb-gga-case21", # CASE21: Constrained And Smoothed semi-Empirical 2021 functional + "hyb-gga-edf2", # EDF2 + "hyb-gga-hapbe", # Hybrid based in APBE and zvPBEloc + "hyb-gga-hflyp", # HF + LYP correlation + "hyb-gga-hjs-b88", # HJS hybrid screened exchange B88 version + "hyb-gga-hjs-b97x", # HJS hybrid screened exchange B97x version + "hyb-gga-hjs-pbe", # HJS hybrid screened exchange PBE version + "hyb-gga-hjs-pbe-sol", # HJS hybrid screened exchange PBE_SOL version + "hyb-gga-hpbeint", # hPBEint + "hyb-gga-hse-sol", # HSEsol + "hyb-gga-hse03", # HSE03 + "hyb-gga-hse06", # HSE06 + "hyb-gga-hse12", # HSE12 + "hyb-gga-hse12s", # HSE12 (short-range version) + "hyb-gga-kmlyp", # Kang-Musgrave hybrid + "hyb-gga-lb07", # Livshits and Baer, empirical functional also used for IP tuning + "hyb-gga-lc-blyp", # LC version of BLYP + "hyb-gga-lc-blyp-ea", # LC version of BLYP for electron affinities + "hyb-gga-lc-blypr", # LC version of BLYP with correlation only in the short range + "hyb-gga-lc-bop", # LC version of B88 + "hyb-gga-lc-pbeop", # LC version of PBE + "hyb-gga-lc-qtp", # CAM-B3LYP re-tuned using ionization potentials of water + "hyb-gga-lc-vv10", # Vydrov and Van Voorhis + "hyb-gga-lc-wpbe", # Long-range corrected PBE (LC-wPBE) by Vydrov and Scuseria + "hyb-gga-lc-wpbe-whs", # Long-range corrected PBE (LC-wPBE) by Weintraub, Henderson and Scuseria + "hyb-gga-lc-wpbe08-whs", # Long-range corrected PBE (LC-wPBE) by Weintraub, Henderson and Scuseria + "hyb-gga-lc-wpbeh-whs", # Long-range corrected short-range hybrid PBE (LC-wPBE) by Weintraub, Henderson and + # Scuseria + "hyb-gga-lc-wpbesol-whs", # Long-range corrected PBE (LC-wPBE) by Weintraub, Henderson and Scuseria + "hyb-gga-lcy-blyp", # LCY version of BLYP + "hyb-gga-lcy-pbe", # LCY version of PBE + "hyb-gga-lrc-wpbe", # Long-range corrected PBE (LRC-wPBE) by Rohrdanz, Martins and Herbert + "hyb-gga-lrc-wpbeh", # Long-range corrected short-range hybrid PBE (LRC-wPBEh) by Rohrdanz, Martins and Herbert + "hyb-gga-mb3lyp-rc04", # B3LYP with RC04 LDA + "hyb-gga-mcam-b3lyp", # Modified CAM-B3LYP by Day, Nguyen and Pachter + "hyb-gga-mpw1k", # mPW1K + "hyb-gga-mpw1lyp", # mPW1LYP + "hyb-gga-mpw1pbe", # mPW1PBE + "hyb-gga-mpw1pw", # mPW1PW + "hyb-gga-mpw3lyp", # MPW3LYP + "hyb-gga-mpw3pw", # MPW3PW of Adamo & Barone + "hyb-gga-mpwlyp1m", # MPW with 1 par. for metals/LYP + "hyb-gga-o3lyp", # O3LYP + "hyb-gga-pbe-2x", # PBE-2X: PBE0 with 56% exact exchange + "hyb-gga-pbe-mol0", # PBEmol0 + "hyb-gga-pbe-molb0", # PBEmolbeta0 + "hyb-gga-pbe-sol0", # PBEsol0 + "hyb-gga-pbe0-13", # PBE0-1/3 + "hyb-gga-pbe38", # PBE38: PBE0 with 3/8 = 37.5% exact exchange + "hyb-gga-pbe50", # PBE50 + "hyb-gga-pbeb0", # PBEbeta0 + "hyb-gga-pbeh", # PBEH (PBE0) + "hyb-gga-qtp17", # Global hybrid for vertical ionization potentials + "hyb-gga-rcam-b3lyp", # Similar to CAM-B3LYP, but trying to reduce the many-electron self-interaction + "hyb-gga-revb3lyp", # Revised B3LYP + "hyb-gga-sb98-1a", # SB98 (1a) + "hyb-gga-sb98-1b", # SB98 (1b) + "hyb-gga-sb98-1c", # SB98 (1c) + "hyb-gga-sb98-2a", # SB98 (2a) + "hyb-gga-sb98-2b", # SB98 (2b) + "hyb-gga-sb98-2c", # SB98 (2c) + "hyb-gga-tuned-cam-b3lyp", # CAM version of B3LYP, tuned for excitations and properties + "hyb-gga-wb97", # wB97 range-separated functional + "hyb-gga-wb97x", # wB97X range-separated functional + "hyb-gga-wb97x-d", # wB97X-D range-separated functional + "hyb-gga-wb97x-d3", # wB97X-D3 range-separated functional + "hyb-gga-wb97x-v", # wB97X-V range-separated functional + "hyb-gga-wc04", # Hybrid fitted to carbon NMR shifts + "hyb-gga-whpbe0", # Long-range corrected short-range hybrid PBE (whPBE0) by Shao et al + "hyb-gga-wp04", # Hybrid fitted to proton NMR shifts + "hyb-gga-x3lyp", # X3LYP + "hyb-lda-bn05", # Baer and Neuhauser, gamma=1 + "hyb-lda-cam-lda0", # CAM version of LDA0 + "hyb-lda-lda0", # LDA hybrid exchange (LDA0) + "hyb-mgga-b0kcis", # Hybrid based on KCIS + "hyb-mgga-b86b95", # Mixture of B86 with BC95 + "hyb-mgga-b88b95", # Mixture of B88 with BC95 (B1B95) + "hyb-mgga-b94-hyb", # Becke 1994 hybrid meta-GGA + "hyb-mgga-b98", # Becke 98 + "hyb-mgga-bb1k", # Mixture of B88 with BC95 from Zhao and Truhlar + "hyb-mgga-br3p86", # BR3P86 hybrid meta-GGA from Neumann and Handy + "hyb-mgga-edmggah", # EDMGGA hybrid + "hyb-mgga-lc-tmlyp", # Long-range corrected TM-LYP by Jana et al + "hyb-mgga-mpw1b95", # Mixture of mPW91 with BC95 from Zhao and Truhlar + "hyb-mgga-mpw1kcis", # MPW1KCIS for barrier heights + "hyb-mgga-mpwb1k", # Mixture of mPW91 with BC95 for kinetics + "hyb-mgga-mpwkcis1k", # MPWKCIS1K for barrier heights + "hyb-mgga-pbe1kcis", # PBE1KCIS for binding energies + "hyb-mgga-pw6b95", # Mixture of PW91 with BC95 from Zhao and Truhlar + "hyb-mgga-pw86b95", # Mixture of PW86 with BC95 + "hyb-mgga-pwb6k", # Mixture of PW91 with BC95 from Zhao and Truhlar for kinetics + "hyb-mgga-revtpssh", # revTPSSh + "hyb-mgga-tpss0", # TPSS0 with 25% exact exchange + "hyb-mgga-tpss1kcis", # TPSS1KCIS for thermochemistry and kinetics + "hyb-mgga-tpssh", # TPSSh + "hyb-mgga-wb97m-v", # wB97M-V exchange-correlation functional + "hyb-mgga-x1b95", # Mixture of X with BC95 + "hyb-mgga-xb1k", # Mixture of X with BC95 for kinetics + "lda-1d-ehwlrg-1", # LDA constructed from slab-like systems of 1 electron + "lda-1d-ehwlrg-2", # LDA constructed from slab-like systems of 2 electrons + "lda-1d-ehwlrg-3", # LDA constructed from slab-like systems of 3 electrons + "lda-corrksdt", # Corrected KSDT by Karasiev, Dufty and Trickey + "lda-gdsmfb", # _Groth, Dornheim, Sjostrom, Malone, Foulkes, Bonitz + "lda-ksdt", # Karasiev, Sjostrom, Dufty & Trickey + "lda-lp-a", # Lee-Parr reparametrization A + "lda-lp-b", # Lee-Parr reparametrization B + "lda-teter93", # Teter 93 + "lda-tih", # Neural network LDA from Tozer et al + "lda-zlp", # Zhao, Levy & Parr, Eq. (20) + "mgga-b97m-v", # B97M-V exchange-correlation functional + "mgga-cc06", # Cancio and Chou 2006 + "mgga-hle17", # High local exchange 2017 + "mgga-lp90", # Lee & Parr, Eq. (56) + "mgga-otpss-d", # oTPSS-D functional of Goerigk and Grimme + "mgga-tpsslyp1w", # TPSSLYP1W + "mgga-vcml-rvv10", # VCML-rVV10 by Trepte and Voss + "mgga-zlp", # Zhao, Levy & Parr, Eq. (21) +] + +# Generated by ChatGPT to include all options for in https://jdftx.org/CommandFluidExCorr.html +kinetic_functionals = [ + "gga-PW91k", # Perdew-Wang GGA kinetic energy parameterized by Lembarki and Chermette + "gga-vW", # von Weisacker correction to LDA kinetic energy + "lda-TF", # Thomas-Fermi LDA kinetic energy + "gga-k-absp1", # gamma-TFvW form by Acharya et al [g = 1 - 1.412/N^{1/3}] + "gga-k-absp2", # gamma-TFvW form by Acharya et al [g = 1 - 1.332/N^{1/3}] + "gga-k-absp3", # gamma-TFvW form by Acharya et al [g = 1 - 1.513/N^{0.35}] + "gga-k-absp4", # gamma-TFvW form by Acharya et al [g = l = 1/(1 + 1.332/N^{1/3})] + "gga-k-apbe", # mu fixed from the semiclassical neutral atom + "gga-k-apbeint", # interpolated version of APBE + "gga-k-baltin", # TF-lambda-vW form by Baltin (l = 5/9) + "gga-k-dk", # DePristo and Kress + "gga-k-ernzerhof", # Ernzerhof + "gga-k-exp4", # Intermediate form between PBE3 and PBE4 + "gga-k-fr-b88", # Fuentealba & Reyes (B88 version) + "gga-k-fr-pw86", # Fuentealba & Reyes (PW86 version) + "gga-k-gds08", # Combined analytical theory with Monte Carlo sampling + "gga-k-ge2", # Second-order gradient expansion of the kinetic energy density + "gga-k-ghds10", # As GDS08 but for an electron gas with spin + "gga-k-ghds10r", # Reparametrized GHDS10 + "gga-k-golden", # TF-lambda-vW form by Golden (l = 13/45) + "gga-k-gp85", # gamma-TFvW form by Ghosh and Parr + "gga-k-gr", # gamma-TFvW form by Gazquez and Robles + "gga-k-lc94", # Lembarki & Chermette + "gga-k-lgap", # LGAP by Constantin et al + "gga-k-lgap-ge", # LGAP-GE by Constantin et al + "gga-k-lieb", # TF-lambda-vW form by Lieb (l = 0.185909191) + "gga-k-lkt", # Luo-Karasiev-Trickey GGA kinetic + "gga-k-llp", # Lee, Lee & Parr + "gga-k-ludena", # gamma-TFvW form by Ludena + "gga-k-meyer", # Meyer, Wang, and Young + "gga-k-ol1", # Ou-Yang and Levy v.1 + "gga-k-ol2", # Ou-Yang and Levy v.2 + "gga-k-pbe2", # Three parameter PBE-like expansion + "gga-k-pbe3", # Three parameter PBE-like expansion + "gga-k-pbe4", # Four parameter PBE-like expansion + "gga-k-pearson", # Pearson 1992 + "gga-k-perdew", # Perdew + "gga-k-pg1", # PG1 (Pauli-Gaussian) functional by Constantin, Fabiano, and Della Sala + "gga-k-rational-p", # RATIONAL^{p} by Lehtomaki and Lopez-Acevedo (by default p=3/2, C_{2}=0.7687) + "gga-k-revapbe", # revised APBE + "gga-k-revapbeint", # interpolated version of revAPBE + "gga-k-tfvw", # Thomas-Fermi plus von Weiszaecker correction + "gga-k-tfvw-opt", # empirically optimized gamma-TFvW form + "gga-k-thakkar", # Thakkar 1992 + "gga-k-tkvln", # Trickey, Karasiev, and Vela + "gga-k-tw1", # Tran and Wesolowski set 1 (Table II) + "gga-k-tw2", # Tran and Wesolowski set 2 (Table II) + "gga-k-tw3", # Tran and Wesolowski set 3 (Table II) + "gga-k-tw4", # Tran and Wesolowski set 4 (Table II) + "gga-k-vjks", # Vitos, Johansson, Kollar, and Skriver + "gga-k-vsk", # Vitos, Skriver, and Kollar + "gga-k-vt84f", # VT84F by Karasiev et al + "gga-k-vw", # von Weiszaecker correction to Thomas-Fermi + "gga-k-yt65", # TF-lambda-vW form by Yonei and Tomishima (l = 1/5) + "lda-k-lp", # Lee and Parr Gaussian ansatz for the kinetic energy + "lda-k-lp96", # Liu-Parr kinetic + "lda-k-tf", # Thomas-Fermi kinetic energy + "lda-k-zlp", # Wigner including kinetic energy contribution + "mgga-k-csk-loc1", # mGGAloc-rev functional by Cancio, Stewart, and Kuna (a=1) + "mgga-k-csk-loc4", # mGGAloc-rev functional by Cancio, Stewart, and Kuna (a=4) + "mgga-k-csk1", # mGGA-rev functional by Cancio, Stewart, and Kuna (a=1) + "mgga-k-csk4", # mGGA-rev functional by Cancio, Stewart, and Kuna (a=4) + "mgga-k-gea2", # Second-order gradient expansion + "mgga-k-gea4", # Fourth-order gradient expansion + "mgga-k-l04", # L0.4 by Laricchia et al + "mgga-k-l06", # L0.6 by Laricchia et al + "mgga-k-pc07", # Perdew and Constantin 2007 + "mgga-k-pc07-opt", # Reoptimized PC07 by Mejia-Rodriguez and Trickey + "mgga-k-pgsl025", # PGSL025 (Pauli-Gaussian) functional by Constantin, Fabiano, and Della Sala + "mgga-k-rda", # Reduced derivative approximation by Karasiev et al. +] + + +jdftxdumpfreqoptions = ["Electronic", "End", "Fluid", "Gummel", "Init", "Ionic"] +jdftxdumpvaroptions = [ + "BandEigs", # Band Eigenvalues + "BandProjections", # Projections of each band state against each atomic orbital + "BandUnfold", # Unfold band structure from supercell to unit cell (see command band-unfold) + "Berry", # Berry curvature i , only allowed at End (see command Cprime-params) + "BGW", # G-space wavefunctions, density and potential for Berkeley GW (requires HDF5 support) + "BoundCharge", # Bound charge in the fluid + "BulkEpsilon", # Dielectric constant of a periodic solid (see command bulk-epsilon) + "ChargedDefect", # Calculate energy correction for charged defect (see command charged-defect) + "CoreDensity", # Total core electron density (from partial core corrections) + "Dfluid", # Electrostatic potential due to fluid alone + "Dipole", # Dipole moment of explicit charges (ionic and electronic) + "Dn", # First order change in electronic density + "DOS", # Density of States (see command density-of-states) + "Dtot", # Total electrostatic potential + "Dvac", # Electrostatic potential due to explicit system alone + "DVext", # External perturbation + "DVscloc", # First order change in local self-consistent potential + "DWfns", # Perturbation Wavefunctions + "Ecomponents", # Components of the energy + "EigStats", # Band eigenvalue statistics: HOMO, LUMO, min, max and Fermi level + "ElecDensity", # Electronic densities (n or nup,ndn) + "ElecDensityAccum", # Electronic densities (n or nup,ndn) accumulated over MD trajectory + "EresolvedDensity", # Electron density from bands within specified energy ranges + "ExcCompare", # Energies for other exchange-correlation functionals (see command elec-ex-corr-compare) + "Excitations", # Dumps dipole moments and transition strength (electric-dipole) of excitations + "FCI", # Output Coulomb matrix elements in FCIDUMP format + "FermiDensity", # Electron density from fermi-derivative at specified energy + "FermiVelocity", # Fermi velocity, density of states at Fermi level and related quantities + "Fillings", # Fillings + "FluidDebug", # Fluid specific debug output if any + "FluidDensity", # Fluid densities (NO,NH,nWater for explicit fluids, cavity function for PCMs) + "Forces", # Forces on the ions in the coordinate system selected by command forces-output-coords + "Gvectors", # List of G vectors in reciprocal lattice basis, for each k-point + "IonicDensity", # Nuclear charge density (with gaussians) + "IonicPositions", # Ionic positions in the same format (and coordinate system) as the input file + "KEdensity", # Kinetic energy density of the valence electrons + "Kpoints", # List of reduced k-points in calculation, and mapping to the unreduced k-point mesh + "L", # Angular momentum matrix elements, only allowed at End (see command Cprime-params) + "Lattice", # Lattice vectors in the same format as the input file + "Momenta", # Momentum matrix elements in a binary file (indices outer to inner: state, cartesian direction, band1, + # band2) + "None", # Dump nothing + "Ocean", # Wave functions for Ocean code + "OrbitalDep", # Custom output from orbital-dependent functionals (eg. quasi-particle energies, + # discontinuity potential) + "Q", # Quadrupole r*p matrix elements, only allowed at End (see command Cprime-params) + "QMC", # Blip'd orbitals and potential for CASINO [27] + "R", # Position operator matrix elements, only allowed at End (see command Cprime-params) + "RealSpaceWfns", # Real-space wavefunctions (one column per file) + "RhoAtom", # Atomic-orbital projected density matrices (only for species with +U enabled) + "SelfInteractionCorrection", # Calculates Perdew-Zunger self-interaction corrected Kohn-Sham eigenvalues + "SlabEpsilon", # Local dielectric function of a slab (see command slab-epsilon) + "SolvationRadii", # Effective solvation radii based on fluid bound charge distribution + "Spin", # Spin matrix elements from non-collinear calculations in a binary file (indices outer to inner: state, + # cartesian direction, band1, band2) + "State", # All variables needed to restart calculation: wavefunction and fluid state/fillings if any + "Stress", # Dumps dE/dR_ij where R_ij is the i'th component of the j'th lattice vector + "Symmetries", # List of symmetry matrices (in covariant lattice coordinates) + "Vcavity", # Fluid cavitation potential on the electron density that determines the cavity + "Velocities", # Diagonal momentum/velocity matrix elements in a binary file (indices outer to inner: state, band, + # cartesian direction) + "VfluidTot", # Total contribution of fluid to the electron potential + "Vlocps", # Local part of pseudopotentials + "Vscloc", # Self-consistent potential + "XCanalysis", # Debug VW KE density, single-particle-ness and spin-polarzied Hartree potential +] + +# simple dictionaries deepcopied multiple times into MASTER_TAG_LIST later for +# different tags +jdftxminimize_subtagdict = { + "alphaTincreaseFactor": FloatTag(), + "alphaTmin": FloatTag(), + "alphaTreduceFactor": FloatTag(), + "alphaTstart": FloatTag(), + "dirUpdateScheme": StrTag( + options=[ + "FletcherReeves", + "HestenesStiefel", + "L-BFGS", + "PolakRibiere", + "SteepestDescent", + ] + ), + "energyDiffThreshold": FloatTag(), + "fdTest": BoolTag(), + "history": IntTag(), + "knormThreshold": FloatTag(), + "linminMethod": StrTag(options=["CubicWolfe", "DirUpdateRecommended", "Quad", "Relax"]), + "nAlphaAdjustMax": FloatTag(), + "nEnergyDiff": IntTag(), + "nIterations": IntTag(), + "updateTestStepSize": BoolTag(), + "wolfeEnergy": FloatTag(), + "wolfeGradient": FloatTag(), +} +jdftxfluid_subtagdict = { + "epsBulk": FloatTag(), + "epsInf": FloatTag(), + "epsLJ": FloatTag(), + "Nnorm": FloatTag(), + "pMol": FloatTag(), + "poleEl": TagContainer( + can_repeat=True, + write_tagname=True, + subtags={ + "omega0": FloatTag(write_tagname=False, optional=False), + "gamma0": FloatTag(write_tagname=False, optional=False), + "A0": FloatTag(write_tagname=False, optional=False), + }, + ), + "Pvap": FloatTag(), + "quad_nAlpha": FloatTag(), + "quad_nBeta": FloatTag(), + "quad_nGamma": FloatTag(), + "representation": TagContainer(subtags={"MuEps": FloatTag(), "Pomega": FloatTag(), "PsiAlpha": FloatTag()}), + "Res": FloatTag(), + "Rvdw": FloatTag(), + "s2quadType": StrTag( + options=[ + "10design60", + "11design70", + "12design84", + "13design94", + "14design108", + "15design120", + "16design144", + "17design156", + "18design180", + "19design204", + "20design216", + "21design240", + "7design24", + "8design36", + "9design48", + "Euler", + "Icosahedron", + "Octahedron", + "Tetrahedron", + ] + ), + "sigmaBulk": FloatTag(), + "tauNuc": FloatTag(), + "translation": StrTag(options=["ConstantSpline", "Fourier", "LinearSpline"]), +} diff --git a/tests/files/io/jdftx/test_jdftx_in_files/CO.in b/tests/files/io/jdftx/test_jdftx_in_files/CO.in new file mode 100644 index 00000000000..7feae54724e --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/CO.in @@ -0,0 +1,50 @@ +#Testing JDFTx input file for CO molecule. Intended to test input parsers + +lattice \ +18.897261 0.000000 0.000000 \ +0.000000 18.897261 0.000000 \ +0.000000 0.000000 18.897261 + +dump-name $VAR +initial-state $VAR +elec-ex-corr gga +van-der-waals D3 +elec-cutoff 20 100 +elec-n-bands 15 +kpoint-folding 1 1 1 +electronic-minimize nIterations 100 energyDiffThreshold 1e-07 +initial-magnetic-moments C 1 O 1 +elec-smearing Fermi 0.001 +spintype z-spin +core-overlap-check none +converge-empty-states yes +latt-move-scale 0 0 0 +symmetries none +fluid LinearPCM +pcm-variant CANDLE +fluid-solvent H2O +fluid-cation Na+ 0.5 +fluid-anion F- 0.5 +vibrations useConstraints no rotationSym no +dump End Dtot +dump End BoundCharge +dump End State +dump End Forces +dump End Ecomponents +dump End VfluidTot +dump End ElecDensity +dump End KEdensity +dump End EigStats +dump End BandEigs +dump End DOS + +coords-type Cartesian +include CO_ion.in +# ion O -0.235981 -0.237621 2.242580 1 +# ion C -0.011521 -0.011600 0.109935 1 + +ion-species GBRV_v1.5/$ID_pbe_v1.uspp + +coulomb-interaction Periodic +dump End Forces +dump End Ecomponents diff --git a/tests/files/io/jdftx/test_jdftx_in_files/CO_ion.in b/tests/files/io/jdftx/test_jdftx_in_files/CO_ion.in new file mode 100644 index 00000000000..7c35a672cc5 --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/CO_ion.in @@ -0,0 +1,2 @@ +ion O -0.235981 -0.237621 2.242580 1 +ion C -0.011521 -0.011600 0.109935 1 diff --git a/tests/files/io/jdftx/test_jdftx_in_files/Si.cif b/tests/files/io/jdftx/test_jdftx_in_files/Si.cif new file mode 100644 index 00000000000..007e86c61f9 --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/Si.cif @@ -0,0 +1,28 @@ +# generated using pymatgen +data_Si +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 3.86697465 +_cell_length_b 3.86697465 +_cell_length_c 3.86697465 +_cell_angle_alpha 60.00000000 +_cell_angle_beta 60.00000000 +_cell_angle_gamma 60.00000000 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural Si +_chemical_formula_sum Si2 +_cell_volume 40.88829285 +_cell_formula_units_Z 2 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Si Si0 1 0.25000000 0.25000000 0.25000000 1 + Si Si1 1 0.00000000 0.00000000 0.00000000 1 diff --git a/tests/files/io/jdftx/test_jdftx_in_files/ct_slab_001.in b/tests/files/io/jdftx/test_jdftx_in_files/ct_slab_001.in new file mode 100644 index 00000000000..3e2d89a75d6 --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/ct_slab_001.in @@ -0,0 +1,82 @@ +lattice \ +14.514261 -4.847013 0.000000 \ +0.000000 13.681017 0.000000 \ +-0.311720 -0.311720 55.734182 + +dump-name $VAR +initial-state $VAR +kpoint-folding 4 4 1 +elec-cutoff 20 100 +elec-ex-corr gga +van-der-waals D3 +elec-n-bands 280 +electronic-minimize nIterations 100 energyDiffThreshold 1e-07 +elec-smearing Fermi 0.001 +elec-initial-magnetization 0 no +spintype z-spin +core-overlap-check none +converge-empty-states yes +latt-move-scale 0 0 0 +lattice-minimize nIterations 00 +symmetries none +fluid LinearPCM +pcm-variant CANDLE +fluid-solvent H2O +fluid-cation Na+ 0.5 +fluid-anion F- 0.5 +dump End Dtot BoundCharge +dump End State +dump End Forces +dump End Ecomponents +dump End VfluidTot +dump End ElecDensity +dump End KEdensity +dump End EigStats +dump End BandEigs +dump End DOS +ionic-minimize nIterations 0 energyDiffThreshold 3.6749322474956637e-06 + +coords-type Cartesian +ion V 0.678403 0.960072 21.892449 0 +ion V 2.371992 3.354685 25.582791 1 +ion V 0.843582 1.194610 29.651005 1 +ion V 2.536830 3.590669 33.353918 1 +ion V -0.937268 5.520411 21.788542 0 +ion V 0.756794 7.916046 25.478773 1 +ion V -0.771563 5.755358 29.545533 1 +ion V 0.921018 8.154277 33.251833 1 +ion V -2.552939 10.080751 21.684635 0 +ion V -0.860346 12.477024 25.375366 1 +ion V -2.387365 10.316536 29.442070 1 +ion V -0.693724 12.711665 33.146884 1 +ion V 5.516490 0.960072 21.788542 0 +ion V 7.207463 3.356037 25.478607 1 +ion V 5.682688 1.195510 29.545511 1 +ion V 7.378808 3.591369 33.251459 1 +ion V 3.900819 5.520411 21.684635 0 +ion V 5.592992 7.915295 25.374477 1 +ion V 4.067238 5.755727 29.440557 1 +ion V 5.761975 8.154086 33.146983 1 +ion V 2.285148 10.080751 21.580729 0 +ion V 3.977586 12.475895 25.271228 1 +ion V 2.452022 10.317513 29.338325 1 +ion V 4.145988 12.713041 33.043373 1 +ion V 10.354577 0.960072 21.684635 0 +ion V 12.047744 3.355226 25.376707 1 +ion V 10.522137 1.196040 29.442931 1 +ion V 12.213490 3.591121 33.146512 1 +ion V 8.738906 5.520411 21.580729 0 +ion V 10.430916 7.917004 25.272242 1 +ion V 8.907020 5.756950 29.337712 1 +ion V 10.598715 8.153155 33.043089 1 +ion V 7.123235 10.080751 21.476822 0 +ion V 8.817218 12.476861 25.169528 1 +ion V 7.291334 10.317893 29.233430 1 +ion V 8.983171 12.713048 32.937581 1 + +ion-species GBRV_v1.5/$ID_pbe_v1.uspp + +coulomb-interaction Slab 001 +coulomb-truncation-embed 4.83064 6.83629 27.4122 +dump End Forces +dump End Ecomponents diff --git a/tests/files/io/jdftx/test_jdftx_in_files/example_sp.in b/tests/files/io/jdftx/test_jdftx_in_files/example_sp.in new file mode 100644 index 00000000000..f22be039449 --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/example_sp.in @@ -0,0 +1,185 @@ +basis kpoint-dependent +converge-empty-states yes +coords-type Lattice +core-overlap-check vector +coulomb-interaction Slab 001 +coulomb-truncation-embed 0.5 0.5 0.5 +coulomb-truncation-ion-margin 5 +davidson-band-ratio 1.1 +density-of-states Etol 1.000000e-06 Esigma 1.000000e-03 \ + Complete \ + Total \ + OrthoOrbital Pt 1 s \ + OrthoOrbital Pt 1 p \ + OrthoOrbital Pt 1 d \ + OrthoOrbital Pt 2 s \ + OrthoOrbital Pt 2 p \ + OrthoOrbital Pt 2 d \ + OrthoOrbital Pt 3 s \ + OrthoOrbital Pt 3 p \ + OrthoOrbital Pt 3 d \ + OrthoOrbital Pt 4 s \ + OrthoOrbital Pt 4 p \ + OrthoOrbital Pt 4 d \ + OrthoOrbital Pt 5 s \ + OrthoOrbital Pt 5 p \ + OrthoOrbital Pt 5 d \ + OrthoOrbital Pt 6 s \ + OrthoOrbital Pt 6 p \ + OrthoOrbital Pt 6 d \ + OrthoOrbital Pt 7 s \ + OrthoOrbital Pt 7 p \ + OrthoOrbital Pt 7 d \ + OrthoOrbital Pt 8 s \ + OrthoOrbital Pt 8 p \ + OrthoOrbital Pt 8 d \ + OrthoOrbital Pt 9 s \ + OrthoOrbital Pt 9 p \ + OrthoOrbital Pt 9 d \ + OrthoOrbital Pt 10 s \ + OrthoOrbital Pt 10 p \ + OrthoOrbital Pt 10 d \ + OrthoOrbital Pt 11 s \ + OrthoOrbital Pt 11 p \ + OrthoOrbital Pt 11 d \ + OrthoOrbital Pt 12 s \ + OrthoOrbital Pt 12 p \ + OrthoOrbital Pt 12 d \ + OrthoOrbital Pt 13 s \ + OrthoOrbital Pt 13 p \ + OrthoOrbital Pt 13 d \ + OrthoOrbital Pt 14 s \ + OrthoOrbital Pt 14 p \ + OrthoOrbital Pt 14 d \ + OrthoOrbital Pt 15 s \ + OrthoOrbital Pt 15 p \ + OrthoOrbital Pt 15 d \ + OrthoOrbital Pt 16 s \ + OrthoOrbital Pt 16 p \ + OrthoOrbital Pt 16 d +dump End IonicPositions Lattice ElecDensity KEdensity BandEigs BandProjections EigStats RhoAtom DOS Symmetries Kpoints Gvectors +dump Ionic State EigStats Ecomponents +dump-name jdft.$VAR +elec-cutoff 30 +elec-eigen-algo Davidson +elec-ex-corr gga-PBE +elec-n-bands 174 +elec-smearing MP1 0.00367493 +electronic-minimize \ + dirUpdateScheme FletcherReeves \ + linminMethod DirUpdateRecommended \ + nIterations 200 \ + history 15 \ + knormThreshold 0 \ + energyDiffThreshold 1e-07 \ + nEnergyDiff 2 \ + alphaTstart 1 \ + alphaTmin 1e-10 \ + updateTestStepSize yes \ + alphaTreduceFactor 0.1 \ + alphaTincreaseFactor 3 \ + nAlphaAdjustMax 3 \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 \ + fdTest no +exchange-regularization WignerSeitzTruncated +fluid None +fluid-ex-corr lda-TF lda-PZ +fluid-gummel-loop 10 1.000000e-05 +fluid-minimize \ + dirUpdateScheme PolakRibiere \ + linminMethod DirUpdateRecommended \ + nIterations 100 \ + history 15 \ + knormThreshold 0 \ + energyDiffThreshold 0 \ + nEnergyDiff 2 \ + alphaTstart 1 \ + alphaTmin 1e-10 \ + updateTestStepSize yes \ + alphaTreduceFactor 0.1 \ + alphaTincreaseFactor 3 \ + nAlphaAdjustMax 3 \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 \ + fdTest no +fluid-solvent H2O 55.338 ScalarEOS \ + epsBulk 78.4 \ + pMol 0.92466 \ + epsInf 1.77 \ + Pvap 1.06736e-10 \ + sigmaBulk 4.62e-05 \ + Rvdw 2.61727 \ + Res 1.42 \ + tauNuc 343133 \ + poleEl 15 7 1 +forces-output-coords Positions +ion Pt 0.166666666666667 0.166666666666667 0.353103309628929 1 +ion Pt 0.166666666666667 0.666666666666667 0.353103309628929 1 +ion Pt 0.666666666666667 0.166666666666667 0.353103309628929 1 +ion Pt 0.666666666666667 0.666666666666667 0.353103309628929 1 +ion Pt 0.333333333333334 0.333333333333333 0.451694615621590 1 +ion Pt 0.333333333333334 0.833333333333333 0.451694615621590 1 +ion Pt 0.833333333333334 0.333333333333333 0.451694615621590 1 +ion Pt 0.833333333333334 0.833333333333333 0.451694615621590 1 +ion Pt 0.000000000000000 0.000000000000000 0.548305384378410 1 +ion Pt 0.000000000000000 0.500000000000000 0.548305384378410 1 +ion Pt 0.500000000000000 0.000000000000000 0.548305384378410 1 +ion Pt 0.500000000000000 0.500000000000000 0.548305384378410 1 +ion Pt 0.166666666666667 0.166666666666667 0.646896690371071 1 +ion Pt 0.166666666666667 0.666666666666667 0.646896690371071 1 +ion Pt 0.666666666666667 0.166666666666667 0.646896690371071 1 +ion Pt 0.666666666666667 0.666666666666667 0.646896690371071 1 +ion-species SG15/$ID_ONCV_PBE-1.1.upf +ion-species SG15/$ID_ONCV_PBE-1.0.upf +ion-width 0 +ionic-minimize \ + dirUpdateScheme L-BFGS \ + linminMethod DirUpdateRecommended \ + nIterations 0 \ + history 15 \ + knormThreshold 0.0001 \ + energyDiffThreshold 1e-06 \ + nEnergyDiff 2 \ + alphaTstart 1 \ + alphaTmin 1e-10 \ + updateTestStepSize yes \ + alphaTreduceFactor 0.1 \ + alphaTincreaseFactor 3 \ + nAlphaAdjustMax 3 \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 \ + fdTest no +kpoint 0.000000000000 0.000000000000 0.000000000000 1.00000000000000 +kpoint-folding 6 6 1 +kpoint-reduce-inversion no +latt-move-scale 0 0 0 +latt-scale 1 1 1 +lattice \ + 10.457499819964989 5.228749909982495 0.000000000000000 \ + 0.000000000000000 9.056460504160873 0.000000000000000 \ + 0.000000000000001 0.000000000000001 44.023042120134328 +lattice-minimize \ + dirUpdateScheme L-BFGS \ + linminMethod DirUpdateRecommended \ + nIterations 0 \ + history 15 \ + knormThreshold 0 \ + energyDiffThreshold 1e-06 \ + nEnergyDiff 2 \ + alphaTstart 1 \ + alphaTmin 1e-10 \ + updateTestStepSize yes \ + alphaTreduceFactor 0.1 \ + alphaTincreaseFactor 3 \ + nAlphaAdjustMax 3 \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 \ + fdTest no +lcao-params -1 1e-06 0.00367493 +pcm-variant GLSSA13 +spintype no-spin +subspace-rotation-factor 1 yes +symmetries automatic +symmetry-threshold 0.0001 +wavefunction lcao diff --git a/tests/files/io/jdftx/test_jdftx_in_files/example_sp_copy.in b/tests/files/io/jdftx/test_jdftx_in_files/example_sp_copy.in new file mode 100644 index 00000000000..1773f37fb99 --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/example_sp_copy.in @@ -0,0 +1,140 @@ +latt-scale 1 1 1 +latt-move-scale 0.0 0.0 0.0 +coords-type Lattice +lattice \ + 10.457499819965 5.228749909982 0.000000000000 \ + 0.000000000000 9.056460504161 0.000000000000 \ + 0.000000000000 0.000000000000 44.023042120134 +ion Pt 0.166666666667 0.166666666667 0.353103309629 1 +ion Pt 0.166666666667 0.666666666667 0.353103309629 1 +ion Pt 0.666666666667 0.166666666667 0.353103309629 1 +ion Pt 0.666666666667 0.666666666667 0.353103309629 1 +ion Pt 0.333333333333 0.333333333333 0.451694615622 1 +ion Pt 0.333333333333 0.833333333333 0.451694615622 1 +ion Pt 0.833333333333 0.333333333333 0.451694615622 1 +ion Pt 0.833333333333 0.833333333333 0.451694615622 1 +ion Pt 0.000000000000 0.000000000000 0.548305384378 1 +ion Pt 0.000000000000 0.500000000000 0.548305384378 1 +ion Pt 0.500000000000 0.000000000000 0.548305384378 1 +ion Pt 0.500000000000 0.500000000000 0.548305384378 1 +ion Pt 0.166666666667 0.166666666667 0.646896690371 1 +ion Pt 0.166666666667 0.666666666667 0.646896690371 1 +ion Pt 0.666666666667 0.166666666667 0.646896690371 1 +ion Pt 0.666666666667 0.666666666667 0.646896690371 1 +core-overlap-check vector +ion-species SG15/$ID_ONCV_PBE-1.1.upf +ion-species SG15/$ID_ONCV_PBE-1.0.upf +ion-width 0.0 + +symmetries automatic +symmetry-threshold 0.0001 + +kpoint 0.000000000000 0.000000000000 0.000000000000 1.000000000000 +kpoint-folding 6 6 1 +kpoint-reduce-inversion no + +elec-ex-corr gga-PBE +exchange-regularization WignerSeitzTruncated +elec-cutoff 30.0 +elec-smearing MP1 0.00367493 +elec-n-bands 174 +spintype no-spin +converge-empty-states yes +basis kpoint-dependent + +coulomb-interaction Slab 001 +coulomb-truncation-embed 0.5 0.5 0.5 +coulomb-truncation-ion-margin 5.0 + +wavefunction lcao + +lcao-params -1 1e-06 0.00367493 +elec-eigen-algo Davidson +ionic-minimize \ + alphaTincreaseFactor 3.0 \ + alphaTmin 1e-10 \ + alphaTreduceFactor 0.1 \ + alphaTstart 1.0 \ + dirUpdateScheme L-BFGS \ + energyDiffThreshold 1e-06 \ + fdTest no \ + history 15 \ + knormThreshold 0.0001 \ + linminMethod DirUpdateRecommended \ + nAlphaAdjustMax 3.0 \ + nEnergyDiff 2 \ + nIterations 0 \ + updateTestStepSize yes \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 +lattice-minimize \ + alphaTincreaseFactor 3.0 \ + alphaTmin 1e-10 \ + alphaTreduceFactor 0.1 \ + alphaTstart 1.0 \ + dirUpdateScheme L-BFGS \ + energyDiffThreshold 1e-06 \ + fdTest no \ + history 15 \ + knormThreshold 0.0 \ + linminMethod DirUpdateRecommended \ + nAlphaAdjustMax 3.0 \ + nEnergyDiff 2 \ + nIterations 0 \ + updateTestStepSize yes \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 +electronic-minimize \ + alphaTincreaseFactor 3.0 \ + alphaTmin 1e-10 \ + alphaTreduceFactor 0.1 \ + alphaTstart 1.0 \ + dirUpdateScheme FletcherReeves \ + energyDiffThreshold 1e-07 \ + fdTest no \ + history 15 \ + knormThreshold 0.0 \ + linminMethod DirUpdateRecommended \ + nAlphaAdjustMax 3.0 \ + nEnergyDiff 2 \ + nIterations 200 \ + updateTestStepSize yes \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 +fluid-minimize \ + alphaTincreaseFactor 3.0 \ + alphaTmin 1e-10 \ + alphaTreduceFactor 0.1 \ + alphaTstart 1.0 \ + dirUpdateScheme PolakRibiere \ + energyDiffThreshold 0.0 \ + fdTest no \ + history 15 \ + knormThreshold 0.0 \ + linminMethod DirUpdateRecommended \ + nAlphaAdjustMax 3.0 \ + nEnergyDiff 2 \ + nIterations 100 \ + updateTestStepSize yes \ + wolfeEnergy 0.0001 \ + wolfeGradient 0.9 +davidson-band-ratio 1.1 +subspace-rotation-factor 1.0 yes + +fluid None +fluid-solvent H2O 55.338 ScalarEOS epsBulk 78.4 epsInf 1.77 pMol 0.92466 poleEl 15.0 7.0 1.0 Pvap 1.06736e-10 Res 1.42 Rvdw 2.61727 sigmaBulk 4.62e-05 tauNuc 343133.0 +fluid-ex-corr lda-TF lda-PZ +fluid-gummel-loop 10 1e-05 +pcm-variant GLSSA13 + +dump-name jdft.$VAR +density-of-states \ + Total \ + OrthoOrbital Pt 1 s OrthoOrbital Pt 1 p OrthoOrbital Pt 1 d OrthoOrbital Pt 2 s OrthoOrbital Pt 2 p OrthoOrbital Pt 2 d OrthoOrbital Pt 3 s OrthoOrbital Pt 3 p OrthoOrbital Pt 3 d OrthoOrbital Pt 4 s OrthoOrbital Pt 4 p OrthoOrbital Pt 4 d OrthoOrbital Pt 5 s OrthoOrbital Pt 5 p OrthoOrbital Pt 5 d OrthoOrbital Pt 6 s OrthoOrbital Pt 6 p OrthoOrbital Pt 6 d OrthoOrbital Pt 7 s OrthoOrbital Pt 7 p OrthoOrbital Pt 7 d OrthoOrbital Pt 8 s OrthoOrbital Pt 8 p OrthoOrbital Pt 8 d OrthoOrbital Pt 9 s OrthoOrbital Pt 9 p OrthoOrbital Pt 9 d OrthoOrbital Pt 10 s OrthoOrbital Pt 10 p OrthoOrbital Pt 10 d OrthoOrbital Pt 11 s OrthoOrbital Pt 11 p OrthoOrbital Pt 11 d OrthoOrbital Pt 12 s OrthoOrbital Pt 12 p OrthoOrbital Pt 12 d OrthoOrbital Pt 13 s OrthoOrbital Pt 13 p OrthoOrbital Pt 13 d OrthoOrbital Pt 14 s OrthoOrbital Pt 14 p OrthoOrbital Pt 14 d OrthoOrbital Pt 15 s OrthoOrbital Pt 15 p OrthoOrbital Pt 15 d OrthoOrbital Pt 16 s OrthoOrbital Pt 16 p OrthoOrbital Pt 16 d \ + Etol 1e-06 \ + Esigma 0.001 \ + Complete +forces-output-coords Positions +dump End BandEigs BandProjections DOS EigStats ElecDensity Gvectors IonicPositions KEdensity Kpoints Lattice RhoAtom Symmetries +dump Ionic Ecomponents EigStats State + diff --git a/tests/files/io/jdftx/test_jdftx_in_files/input-simple1.in b/tests/files/io/jdftx/test_jdftx_in_files/input-simple1.in new file mode 100644 index 00000000000..e167680805a --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/input-simple1.in @@ -0,0 +1,30 @@ + +elec-cutoff 30 100 +#fix-electron-density jdft.$VAR +include input-simple2.in +#van-der-waals D3 + +#lcao-params 10 +spintype no-spin + +elec-n-bands 34 +converge-empty-states yes + +initial-magnetic-moments C 1 2 3 O 1 F 1 + +#elec-ex-corr gga-PBE + +dump-only +dump End State +dump End BandEigs + +lattice \ + 10 0.5 0 \ + 0 11 0 \ + 0 1 12 + +ion C 0.5 0.5 0.6 v 0.1 0.2 0.3 0 +ion C 0.5 0.5 0.9 v 0.1 0.2 0.3 0 +ion O 0.2 0.3 0.4 v 0.7 0.8 0.9 1 +ion F 0.0 0.01 0.02 v 0.99 0.99 0.99 0 +ion C 0.1 0.5 0.6 v 0.1 0.2 0.3 0 diff --git a/tests/files/io/jdftx/test_jdftx_in_files/input-simple2.in b/tests/files/io/jdftx/test_jdftx_in_files/input-simple2.in new file mode 100644 index 00000000000..04bf8fc4574 --- /dev/null +++ b/tests/files/io/jdftx/test_jdftx_in_files/input-simple2.in @@ -0,0 +1,13 @@ + +electronic-scf nIterations 100 verbose yes + +symmetry-matrix \ +1 0 0 \ +0 1 0 \ +0 0 1 \ +0 0 0 +symmetry-matrix \ +1 0 0 \ +0 -1 0 \ +0 1 -1 \ +0.5 0.5 0.5 diff --git a/tests/files/io/jdftx/tmp/empty.txt b/tests/files/io/jdftx/tmp/empty.txt new file mode 100644 index 00000000000..6acb82af3e8 --- /dev/null +++ b/tests/files/io/jdftx/tmp/empty.txt @@ -0,0 +1 @@ +I am here to let github add this directory. \ No newline at end of file diff --git a/tests/io/jdftx/__init__.py b/tests/io/jdftx/__init__.py new file mode 100644 index 00000000000..6854b2c1e31 --- /dev/null +++ b/tests/io/jdftx/__init__.py @@ -0,0 +1 @@ +# these testchecks will have to be migrated over to pymatgen once the io functions are migrated there diff --git a/tests/io/jdftx/inputs_test_utils.py b/tests/io/jdftx/inputs_test_utils.py new file mode 100644 index 00000000000..417c1deca63 --- /dev/null +++ b/tests/io/jdftx/inputs_test_utils.py @@ -0,0 +1,65 @@ +"""Shared test utilities for JDFTx input files. + +This module contains shared testing functions and example data + known values for JDFTx input files. +This module will be combined with shared_test_utils.py upon final implementation. +""" + +from __future__ import annotations + +from pathlib import Path + +from pymatgen.io.jdftx.inputs import JDFTXInfile, JDFTXStructure +from pymatgen.util.testing import TEST_FILES_DIR + +from .shared_test_utils import assert_same_value + + +def assert_idential_jif(jif1: JDFTXInfile | dict, jif2: JDFTXInfile | dict): + djif1 = jif1.as_dict() if isinstance(jif1, JDFTXInfile) else jif1 + djif2 = jif2.as_dict() if isinstance(jif2, JDFTXInfile) else jif2 + assert_same_value(djif1, djif2) + + +def assert_equiv_jdftxstructure(struc1: JDFTXStructure, struc2: JDFTXStructure) -> None: + """Check if two JDFTXStructure objects are equivalent. + + Check if two JDFTXStructure objects are equivalent. + + Parameters: + ---------- + struc1: JDFTXStructure + The first JDFTXStructure object. + struc2: JDFTXStructure + The second JDFTXStructure object. + """ + d1 = struc1.as_dict() + d2 = struc2.as_dict() + assert_idential_jif(d1, d2) + + +ex_in_files_dir = Path(TEST_FILES_DIR) / "io" / "jdftx" / "test_jdftx_in_files" + +ex_infile1_fname = ex_in_files_dir / "CO.in" +ex_infile1_knowns = { + "dump-name": "$VAR", + "initial-state": "$VAR", + "elec-ex-corr": "gga", + "van-der-waals": "D3", + "elec-cutoff": {"Ecut": 20.0, "EcutRho": 100.0}, + "elec-n-bands": 15, + "kpoint-folding": {"n0": 1, "n1": 1, "n2": 1}, + "spintype": "z-spin", + "core-overlap-check": "none", + "converge-empty-states": True, + "latt-move-scale": {"s0": 0.0, "s1": 0.0, "s2": 0.0}, + "symmetries": "none", + "fluid": {"type": "LinearPCM"}, + "pcm-variant": "CANDLE", + "fluid-solvent": [{"name": "H2O"}], + "fluid-cation": {"name": "Na+", "concentration": 0.5}, + "fluid-anion": {"name": "F-", "concentration": 0.5}, + "initial-magnetic-moments": "C 1 O 1", +} + +ex_infile2_fname = ex_in_files_dir / "example_sp.in" +ex_infile3_fname = ex_in_files_dir / "ct_slab_001.in" diff --git a/tests/io/jdftx/shared_test_utils.py b/tests/io/jdftx/shared_test_utils.py new file mode 100644 index 00000000000..6ac94317e84 --- /dev/null +++ b/tests/io/jdftx/shared_test_utils.py @@ -0,0 +1,49 @@ +"""Test utilities for JDFTx tests shared between inputs and outputs parts. + +This module will inherit everything from inputs_test_utils.py and outputs_test_utils.py upon final implementation. +""" + +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +import pytest + +from pymatgen.util.testing import TEST_FILES_DIR + +dump_files_dir = Path(TEST_FILES_DIR) / "io" / "jdftx" / "tmp" + + +def assert_same_value(testval, knownval): + if type(testval) not in [tuple, list]: + assert isinstance(testval, type(knownval)) + if isinstance(testval, float): + assert testval == pytest.approx(knownval) + elif isinstance(testval, dict): + for k in knownval: + assert k in testval + assert_same_value(testval[k], knownval[k]) + elif testval is None: + assert knownval is None + else: + assert testval == knownval + else: + assert len(testval) == len(knownval) + for i in range(len(testval)): + assert_same_value(testval[i], knownval[i]) + + +@pytest.fixture(scope="module") +def tmp_path(): + os.mkdir(dump_files_dir) + yield dump_files_dir + shutil.rmtree(dump_files_dir) + + +def write_mt_file(tmp_path: Path, fname: str): + filepath = tmp_path / fname + with open(filepath, "w") as f: + f.write("if you're reading this yell at ben") + f.close() diff --git a/tests/io/jdftx/test_generic_tags.py b/tests/io/jdftx/test_generic_tags.py new file mode 100644 index 00000000000..4fd3e8a9580 --- /dev/null +++ b/tests/io/jdftx/test_generic_tags.py @@ -0,0 +1,447 @@ +from __future__ import annotations + +import re + +import pytest + +from pymatgen.io.jdftx.generic_tags import ( + AbstractTag, + BoolTag, + BoolTagContainer, + FloatTag, + InitMagMomTag, + IntTag, + StrTag, + TagContainer, +) +from pymatgen.io.jdftx.jdftxinfile_master_format import get_dump_tag_container, get_tag_object + +from .shared_test_utils import assert_same_value + + +class Unstringable: + """Dummy class that cannot be converted to a string""" + + def __str__(self): + raise ValueError("Cannot convert to string") + + +class NonIterable: + """Dummy class that cannot be iterated through""" + + def __iter__(self): + raise ValueError("Cannot iterate through this object") + + +def test_abstract_tag(): + with pytest.raises(TypeError): + # AbstractTag cannot be instantiated directly + AbstractTag() + + +def test_stringify(): + str_tag = StrTag(options=["ken"]) + out_str = str(str_tag) + assert isinstance(out_str, str) + assert len(out_str) + + +def test_bool_tag(): + """Expected behavior of BoolTag is tested here""" + bool_tag = BoolTag(write_value=False) + # Values with spaces are impossible to interpret as bools + tag = "barbie" + value = "this string has spaces" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + bool_tag.read(tag, value) + # Value errors should be raised if value cannot be conveniently converted to a bool + value = "non-bool-like" + with pytest.raises(ValueError, match=f"Could not set '{value}' as True/False for tag '{tag}'!"): + bool_tag.read(tag, value) + # bool_tag = BoolTag(write_value=True) + # Only values appearing in the "_TF_options" map can be converted to bool (allows for yes/no to be interpreted + # as bools) + value = "not-appearing-in-read-TF-options" + with pytest.raises(ValueError, match=f"Could not set '{value}' as True/False for tag '{tag}'!"): + bool_tag.read(tag, value) + + +def test_abstract_tag_inheritor(): + """Expected behavior of methods inherited from AbstractTag are tested here + (AbstractTag cannot be directly initiated)""" + bool_tag = BoolTag(write_value=False) + tag = "barbie" + value = "ken" + # Abstract-tag inheritors should raise the following error for calling get_list_representation unless + # they have implemented the method (bool_tag does not and should not) + with pytest.raises(ValueError, match=f"Tag object with tag '{tag}' has no get_list_representation method"): + bool_tag.get_list_representation(tag, value) + # Abstract-tag inheritors should raise the following error for calling get_dict_representation unless + # they have implemented the method (bool_tag does not and should not) + with pytest.raises(ValueError, match=f"Tag object with tag '{tag}' has no get_dict_representation method"): + bool_tag.get_dict_representation(tag, value) + # "_validate_repeat" is only called if "can_repeat" is True, but must raise an error if value is not a list + with pytest.raises(TypeError): + bool_tag._validate_repeat(tag, value) + + +def test_str_tag(): + str_tag = StrTag(options=["ken"]) + # Values with spaces are rejected as spaces are occasionally used as delimiters in input files + tag = "barbie" + value = "ken, allan" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + str_tag.read(tag, value) + # Values that cannot be converted to strings have a safety net error message + value = Unstringable() + print_value = "(unstringable)" + with pytest.raises(TypeError, match=re.escape(f"Value '{print_value}' for '{tag}' should be a string!")): + str_tag.read(tag, value) + # "str_tag" here was initiated with only "ken" as an option, so "allan" should raise an error + # (barbie is the tagname, not the value) + value = "allan" + with pytest.raises( + ValueError, match=re.escape(f"The string value '{value}' must be one of {str_tag.options} for tag '{tag}'") + ): + str_tag.read(tag, value) + # If both the tagname and value are written, two tokens are to be written + str_tag = StrTag(write_tagname=True, write_value=True) + assert str_tag.get_token_len() == 2 + # If only the tagname is written, one token is to be written + str_tag = StrTag(write_tagname=True, write_value=False) + assert str_tag.get_token_len() == 1 + # If only the value is written, one token is to be written + str_tag = StrTag(write_tagname=False, write_value=True) + assert str_tag.get_token_len() == 1 + # If neither the tagname nor the value are written, no tokens are to be written + # (this is useless, but it is a valid option) + str_tag = StrTag(write_tagname=False, write_value=False) + assert str_tag.get_token_len() == 0 + + +def test_int_tag(): + int_tag = IntTag() + # Values with spaces are rejected as spaces are occasionally used as delimiters in input files + value = "ken, allan" + tag = "barbie" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + int_tag.read(tag, value) + # Values passed to "read" must be strings + value = {} + with pytest.raises(TypeError, match=f"Value '{value}' for '{tag}' should be a string!"): + int_tag.read(tag, value) + # Values must be able to be type-cast to an integer + value = "ken" # (ken is not an integer) + with pytest.raises(ValueError, match=f"Could not set value '{value}' to an int for tag '{tag}'!"): + int_tag.read(tag, value) + + +def test_float_tag(): + float_tag = FloatTag() + tag = "barbie" + value = "ken, allan" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + float_tag.read(tag, value) + with pytest.raises(TypeError): + float_tag.read(tag, {}) + value = "ken" # (ken is not an number) + with pytest.raises(ValueError, match=f"Could not set value '{value}' to a float for tag '{tag}'!"): + float_tag.read(tag, value) + value = "1.2.3" # (1.2.3 cannot be a float) + with pytest.raises(ValueError, match=f"Could not set value '{value}' to a float for tag '{tag}'!"): + float_tag.read("barbie", "1.2.3") + + +def test_initmagmomtag(): + initmagmomtag = InitMagMomTag(write_tagname=True) + tag = "tag" + value = Unstringable() + print_value = "(unstringable)" + with pytest.raises(TypeError, match=re.escape(f"Value '{print_value}' for '{tag}' should be a string!")): + initmagmomtag.read(tag, value) + assert initmagmomtag.read("tag", "42") == "42" + assert initmagmomtag.write("magtag", 42) == "magtag 42 " + initmagmomtag = InitMagMomTag(write_tagname=False) + assert initmagmomtag.write("magtag", 42) == "42 " + initmagmomtag = InitMagMomTag(write_tagname=True, write_value=False) + assert initmagmomtag.write("magtag", 42) == "magtag " + initmagmomtag = InitMagMomTag(write_tagname=False, write_value=False) + assert initmagmomtag.write("magtag", 42) == "" + assert initmagmomtag.get_token_len() == 0 + initmagmomtag = InitMagMomTag(write_tagname=True, write_value=True) + assert initmagmomtag.get_token_len() == 2 + with pytest.warns(Warning): + initmagmomtag.validate_value_type("tag", Unstringable(), try_auto_type_fix=True) + + +def test_tagcontainer_validation(): + tag = "barbie" + repeatable_str_subtag = "ken" + non_repeatable_int_subtag = "allan" + non_intable_value = "nonintable" + tagcontainer = TagContainer( + can_repeat=False, + subtags={ + f"{repeatable_str_subtag}": StrTag(can_repeat=True), + f"{non_repeatable_int_subtag}": IntTag(can_repeat=False), + }, + ) + # issues with converting values to the correct type should only raise a warning within validate_value_type + with pytest.warns(Warning): + tagcontainer.validate_value_type( + f"{tag}", {f"{repeatable_str_subtag}": ["1"], f"{non_repeatable_int_subtag}": f"{non_intable_value}"} + ) + # _validate_single_entry for TagContainer should raise an error if the value is not a dict + with pytest.raises(TypeError): + tagcontainer._validate_single_entry("not a dict") # Not a dict + # inhomogeneous values for repeated TagContainers should raise an error + # until filling with default values is implemented + tagcontainer.can_repeat = True + value = [{"ken": 1}, {"ken": 2, "allan": 3}] + with pytest.raises( + ValueError, + match=re.escape(f"The values '{value}' for tag '{tag}' provided in a list of lists have different lengths"), + ): + tagcontainer.validate_value_type(tag, value) + + +def test_tagcontainer_mixed_nesting(): + tag = "barbie" + str_subtag = "ken" + int_subtag = "allan" + tagcontainer = TagContainer( + can_repeat=False, + subtags={ + f"{str_subtag}": StrTag(), + f"{int_subtag}": IntTag(), + }, + ) + tagcontainer_mixed_nesting_tester(tagcontainer, tag, str_subtag, int_subtag) + + +def tagcontainer_mixed_nesting_tester(tagcontainer, tag, str_subtag, int_subtag): + list_of_dicts_and_lists = [{str_subtag: "b"}, [[int_subtag, 1]]] + list_of_dicts_and_lists_err = f"tag '{tag}' with value '{list_of_dicts_and_lists}' cannot have" + " nested lists/dicts mixed with bool/str/int/floats!" + list_of_dicts_of_strs_and_ints = [{str_subtag: "b"}, {int_subtag: 1}] + list_of_dicts_of_strs_and_ints_err = f"tag '{tag}' with value '{list_of_dicts_of_strs_and_ints}' " + "cannot have nested dicts mixed with bool/str/int/floats!" + list_of_lists_of_strs_and_ints = [[str_subtag, "b"], [int_subtag, 1]] + list_of_lists_of_strs_and_ints_err = f"tag '{tag}' with value '{list_of_lists_of_strs_and_ints}' " + "cannot have nested lists mixed with bool/str/int/floats!" + for value, err_str in zip( + [list_of_dicts_and_lists, list_of_dicts_of_strs_and_ints, list_of_lists_of_strs_and_ints], + [list_of_dicts_and_lists_err, list_of_dicts_of_strs_and_ints_err, list_of_lists_of_strs_and_ints_err], + strict=False, + ): + with pytest.raises( + ValueError, + match=re.escape(err_str), + ): + tagcontainer._check_for_mixed_nesting(tag, value) + + +def test_tagcontainer_read(): + tag = "barbie" + repeatable_str_subtag = "ken" + non_repeatable_int_subtag = "allan" + tagcontainer = TagContainer( + can_repeat=False, + subtags={ + f"{repeatable_str_subtag}": StrTag(can_repeat=True), + f"{non_repeatable_int_subtag}": IntTag(can_repeat=False), + }, + ) + # non-repeatable subtags that repeat in the value for "read" should raise an error + value = f"{non_repeatable_int_subtag} 1 {non_repeatable_int_subtag} 2" + with pytest.raises( + ValueError, + match=f"Subtag '{non_repeatable_int_subtag}' for tag '{tag}' is not allowed to repeat " + f"but repeats value {value}", + ): + tagcontainer.read(tag, value) + # output of "read" should be a dict of subtags, with list values for repeatable subtags and single values for + # non-repeatable subtags + assert_same_value( + tagcontainer.read(f"{tag}", "ken a ken b allan 3"), + {"ken": ["a", "b"], "allan": 3}, + ) + required_subtag = "ken" + optional_subtag = "allan" + tagcontainer = TagContainer( + can_repeat=True, + write_tagname=True, + subtags={ + required_subtag: StrTag(optional=False, write_tagname=True, write_value=True), + optional_subtag: IntTag(optional=True, write_tagname=True, write_value=True), + }, + ) + with pytest.raises( + ValueError, + match=re.escape( + f"The subtag '{required_subtag}' for tag '{tag}' is not optional but was not populated during the read!" + ), + ): + tagcontainer.read("barbie", "allan 1") + unread_values = "fgfgfgf" + value = f"ken a {unread_values}" + with pytest.raises( + ValueError, + match=re.escape( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {[unread_values]}" + ), + ): + tagcontainer.read(tag, value) + ### + tagcontainer = get_tag_object("ion") + with pytest.warns(Warning): + tagcontainer.read("ion", "Fe 1 1 1 1 HyperPlane") + + +def test_tagcontainer_write(): + tag = "barbie" + repeatable_str_subtag = "ken" + non_repeatable_int_subtag = "allan" + tagcontainer = TagContainer( + can_repeat=False, + subtags={ + f"{repeatable_str_subtag}": StrTag(can_repeat=True), + f"{non_repeatable_int_subtag}": IntTag(can_repeat=False), + }, + ) + assert_same_value( + tagcontainer.write(tag, {repeatable_str_subtag: ["a", "b"]}), + f"{tag} {repeatable_str_subtag} a {repeatable_str_subtag} b ", + ) + tagcontainer.subtags[repeatable_str_subtag].write_tagname = False + assert_same_value(tagcontainer.write(tag, {repeatable_str_subtag: ["a", "b"]}), f"{tag} a b ") + # Lists are reserved for repeatable tagcontainers + value = [{"ken": 1}] + with pytest.raises( + TypeError, + match=re.escape( + f"The value '{value}' (of type {type(value)}) for tag '{tag}' must be a dict for this TagContainer!" + ), + ): + tagcontainer.write(tag, value) + + +def test_tagcontainer_list_dict_conversion(): + top_subtag = "universe" + bottom_subtag1 = "sun" + bottom_subtag2 = "moon" + tagcontainer = TagContainer( + can_repeat=True, + allow_list_representation=False, + subtags={ + top_subtag: TagContainer( + allow_list_representation=True, + write_tagname=True, + subtags={ + bottom_subtag1: BoolTag( + write_tagname=False, + allow_list_representation=False, + ), + bottom_subtag2: BoolTag( + write_tagname=True, + allow_list_representation=False, + ), + }, + ) + }, + ) + notadict = f"True {bottom_subtag2} False" + value = {top_subtag: notadict} + with pytest.raises( + ValueError, match=re.escape(f"The subtag {top_subtag} is not a dict: '{notadict}', so could not be converted") + ): + tagcontainer._make_list(value) + # Despite bottom_subtag2 have "write_tagname" set to True, it is not written in the list for _make_list + # (obviously this is dangerous and this method should be avoided) + assert_same_value( + tagcontainer._make_list({top_subtag: {bottom_subtag1: "True", bottom_subtag2: "False"}}), + [top_subtag, "True", "False"], + ) + value = {top_subtag: {bottom_subtag1: {"True": True}, bottom_subtag2: "False"}} + with pytest.warns(Warning): + tagcontainer._make_list(value) + value = [[top_subtag, "True", "False"]] + assert_same_value(tagcontainer.get_list_representation("barbie", value), value) + tag = "barbie" + value = [["universe", "True", "False"], {"universe": {"sun": True, "moon": False}}] + err_str = f"The tag '{tag}' set to value '{value}' must be a list of dicts when passed to " + "'get_list_representation' since the tag is repeatable." + with pytest.raises(ValueError, match=re.escape(err_str)): + tagcontainer.get_list_representation(tag, value) + value = {"universe": {"sun": {"True": True}, "moon": "False"}} + err_str = f"Value '{value}' must be a list when passed to 'get_dict_representation' since " + f"tag '{tag}' is repeatable." + with pytest.raises(ValueError, match=re.escape(err_str)): + tagcontainer.get_dict_representation("barbie", value) + err_str = f"Value '{value}' must be a list when passed to 'get_list_representation' since " + f"tag '{tag}' is repeatable." + with pytest.raises(ValueError, match=re.escape(err_str)): + tagcontainer.get_list_representation("barbie", value) + + +def test_dumptagcontainer(): + dtc = get_dump_tag_container() + tag = "dump" + unread_value = "barbie" + value = f"{unread_value} End DOS" + with pytest.raises( + ValueError, + match=re.escape( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {[unread_value]}" + ), + ): + dtc.read(tag, value) + + +def test_booltagcontainer(): + tag = "barbie" + required_subtag = "ken" + optional_subtag = "allan" + not_a_subtag = "alan" + btc = BoolTagContainer( + subtags={ + required_subtag: BoolTag(optional=False, write_tagname=True, write_value=False), + optional_subtag: BoolTag(optional=True, write_tagname=True, write_value=False), + }, + ) + with pytest.raises( + ValueError, + match=re.escape( + f"The subtag '{required_subtag}' for tag '{tag}' is not optional but was not populated during the read!" + ), + ): + btc.read(tag, optional_subtag) + value = f"{required_subtag} {not_a_subtag}" + with pytest.raises( + ValueError, + match=re.escape( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {[not_a_subtag]}" + ), + ): + btc.read(tag, value) + + +def test_multiformattagcontainer(): + tag = "dump-fermi-density" + value = "notabool" + mftg = get_tag_object(tag) + with pytest.raises(RuntimeError): + mftg.read(tag, value) + with pytest.raises(RuntimeError): + mftg.write(tag, value) + errormsg = f"No valid read format for tag '{tag}' with value '{value}'\n" + "Add option to format_options or double-check the value string and retry!\n\n" + with pytest.raises(ValueError, match=re.escape(errormsg)): + mftg.get_format_index_for_str_value(tag, value) + err_str = f"The format for tag '{tag}' with value '{value}' could not be determined from the available options! " + "Check your inputs and/or MASTER_TAG_LIST!" + with pytest.raises(ValueError, match=re.escape(err_str)): + mftg._determine_format_option(tag, value) diff --git a/tests/io/jdftx/test_input_utils.py b/tests/io/jdftx/test_input_utils.py new file mode 100644 index 00000000000..728a7227b86 --- /dev/null +++ b/tests/io/jdftx/test_input_utils.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import pytest + +from pymatgen.io.jdftx.generic_tags import _flatten_list +from pymatgen.io.jdftx.inputs import _multi_getattr as multi_getattr +from pymatgen.io.jdftx.inputs import _multi_hasattr as multi_hasattr + + +def test_flatten_list(): + assert _flatten_list("", [1, 2, 3]) == [1, 2, 3] + assert _flatten_list("", [1, [2, 3]]) == [1, 2, 3] + assert _flatten_list("", [1, [2, [3]]]) == [1, 2, 3] + assert _flatten_list("", [1, [2, [3, [4]]]]) == [1, 2, 3, 4] + with pytest.raises(TypeError): + _flatten_list("", 1) + + +def test_multihasattr(): + class A: + def __init__(self): + self.v1: int = 1 + + class B: + def __init__(self): + self.a = A() + self.v2: int = 2 + + a = A() + b = B() + assert multi_hasattr(a, "v1") + assert multi_hasattr(b, "a") + assert multi_hasattr(b, "a.v1") + assert not multi_hasattr(b, "a.v2") + assert not multi_hasattr(b, "v1") + + +def test_multigetattr(): + class A: + def __init__(self): + self.v1: int = 1 + + class B: + def __init__(self): + self.a = A() + self.v2: int = 2 + + a = A() + b = B() + assert multi_getattr(a, "v1") == 1 + assert multi_getattr(b, "v2") == 2 + assert multi_getattr(b, "a.v1") == 1 + with pytest.raises(AttributeError): + multi_getattr(b, "v1") diff --git a/tests/io/jdftx/test_jdftxinfile.py b/tests/io/jdftx/test_jdftxinfile.py new file mode 100644 index 00000000000..41f990f7124 --- /dev/null +++ b/tests/io/jdftx/test_jdftxinfile.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import re +from copy import deepcopy +from typing import TYPE_CHECKING, Any + +import numpy as np +import pytest + +from pymatgen.core.structure import Structure +from pymatgen.io.jdftx.inputs import JDFTXInfile, JDFTXStructure +from pymatgen.io.jdftx.jdftxinfile_master_format import get_tag_object + +from .inputs_test_utils import ( + assert_equiv_jdftxstructure, + assert_idential_jif, + ex_in_files_dir, + ex_infile1_fname, + ex_infile1_knowns, + ex_infile2_fname, + ex_infile3_fname, +) +from .shared_test_utils import assert_same_value + +if TYPE_CHECKING: + from collections.abc import Callable + + from pymatgen.util.typing import PathLike + + +def test_jdftxinfile_structuregen(): + jif = JDFTXInfile.from_file(ex_infile1_fname) + jdftxstruct = jif.to_jdftxstructure(jif) + assert isinstance(jdftxstruct, JDFTXStructure) + + +@pytest.mark.parametrize( + ("infile_fname", "bool_func"), + [ + (ex_infile1_fname, lambda jif: all(jif["kpoint-folding"][x] == 1 for x in jif["kpoint-folding"])), + (ex_infile1_fname, lambda jif: jif["elec-n-bands"] == 15), + ], +) +def test_JDFTXInfile_known_lambda(infile_fname: str, bool_func: Callable[[JDFTXInfile], bool]): + jif = JDFTXInfile.from_file(infile_fname) + assert bool_func(jif) + + +def JDFTXInfile_self_consistency_tester(jif: JDFTXInfile, tmp_path: PathLike): + """Create an assortment of JDFTXinfile created from the same data but through different methods, and test that + they are all equivalent through "assert_idential_jif" """ + dict_jif = jif.as_dict() + # # Commenting out tests with jif2 due to the list representation asserted + jif2 = JDFTXInfile.get_dict_representation(JDFTXInfile._from_dict(dict_jif)) + str_list_jif = jif.get_text_list() + str_jif = "\n".join(str_list_jif) + jif3 = JDFTXInfile.from_str(str_jif) + tmp_fname = tmp_path / "tmp.in" + jif.write_file(tmp_fname) + jif4 = JDFTXInfile.from_file(tmp_fname) + jifs = [jif, jif2, jif3, jif4] + for i in range(len(jifs)): + for j in range(i + 1, len(jifs)): + print(f"{i}, {j}") + assert_idential_jif(jifs[i], jifs[j]) + + +def test_JDFTXInfile_from_dict(tmp_path) -> None: + jif = JDFTXInfile.from_file(ex_infile1_fname) + jif_dict = jif.as_dict() + # Test that dictionary can be modified and that _from_dict will fix set values + jif_dict["elec-cutoff"] = 20 + jif2 = JDFTXInfile.from_dict(jif_dict) + JDFTXInfile_self_consistency_tester(jif2, tmp_path) + + +@pytest.mark.parametrize("infile_fname", [ex_infile3_fname, ex_infile1_fname, ex_infile2_fname]) +def test_JDFTXInfile_self_consistency_fromfile(infile_fname: PathLike, tmp_path) -> None: + """Test that JDFTXInfile objects with different assortments of tags survive inter-conversion done within + "JDFTXInfile_self_consistency_tester""" + jif = JDFTXInfile.from_file(infile_fname) + JDFTXInfile_self_consistency_tester(jif, tmp_path) + + +@pytest.mark.parametrize( + ("val_key", "val"), + [ + ("lattice", np.eye(3)), + ("fluid-solvent", "H2O 0.5"), + ("fluid-solvent", "H2O"), + ("latt-scale", "1 1 1"), + ("latt-scale", ["1", "1", "1"]), + ("latt-scale", [1, 1, 1]), + ("latt-scale", {"s0": 1, "s1": 1, "s2": 1}), + ("elec-cutoff", {"Ecut": 20.0, "EcutRho": 100.0}), + ("elec-cutoff", "20 100"), + ("elec-cutoff", [20, 100]), + ("elec-cutoff", 20), + ], +) +def test_JDFTXInfile_set_values(val_key: str, val: Any, tmp_path) -> None: + """Test value setting for various tags""" + jif = JDFTXInfile.from_file(ex_infile1_fname) + jif[val_key] = val + # Test that the JDFTXInfile object is still consistent + JDFTXInfile_self_consistency_tester(jif, tmp_path) + + +@pytest.mark.parametrize( + ("val_key", "val"), + [ + ("fluid-solvent", "H2O"), + ("dump", "End DOS"), + ("dump", "End DOS BandEigs"), + ("dump-interval", "Electronic 1"), + ("ion", "Fe 1 1 1 0"), + ], +) +def test_JDFTXInfile_append_values(val_key: str, val: Any, tmp_path) -> None: + """Test the append_tag method""" + jif = JDFTXInfile.from_file(ex_infile1_fname) + val_old = None if val_key not in jif else deepcopy(jif[val_key]) + jif.append_tag(val_key, val) + val_new = jif[val_key] + assert val_old != val_new + # Test that the append_tag does not break the JDFTXInfile object + JDFTXInfile_self_consistency_tester(jif, tmp_path) + + +def test_JDFTXInfile_expected_exceptions(): + jif = JDFTXInfile.from_file(ex_infile1_fname) + with pytest.raises(KeyError): + jif["barbie"] = "ken" + # non-repeating tags raise value-errors when appended + tag = "initial-state" + with pytest.raises(ValueError, match=re.escape(f"The tag '{tag}' cannot be repeated and thus cannot be appended")): + jif.append_tag(tag, "$VAR") + # Phonon and Wannier tags raise value-errors at _preprocess_line + with pytest.raises(ValueError, match="Phonon functionality has not been added!"): + jif._preprocess_line("phonon idk") + with pytest.raises(ValueError, match="Wannier functionality has not been added!"): + jif._preprocess_line("wannier idk") + # Tags not in MASTER_TAG_LIST raise value-errors at _preprocess_line + err_str = f"The barbie tag in {['barbie', 'ken allan']} is not in MASTER_TAG_LIST and is not a comment, " + "something is wrong with this input data!" + with pytest.raises(ValueError, match=re.escape(err_str)): + jif._preprocess_line("barbie ken allan") + # include tags raise value-errors if the file cannot be found + _filename = "barbie" + err_str = f"The include file {_filename} ({_filename}) does not exist!" + with pytest.raises(ValueError, match=re.escape(err_str)): + JDFTXInfile.from_str(f"include {_filename}\n") + # If it does exist, no error should be raised + filename = ex_in_files_dir / "barbie" + err_str = f"The include file {_filename} ({filename}) does not exist!" + str(err_str) + # If the wrong parent_path is given for a file that does exist, error + with pytest.raises(ValueError, match=re.escape(err_str)): + JDFTXInfile.from_str(f"include {_filename}\n", path_parent=ex_in_files_dir) + # JDFTXInfile cannot be constructed without lattice and ion tags + with pytest.raises(ValueError, match="This input file is missing required structure tags"): + JDFTXInfile.from_str("dump End DOS\n") + # "barbie" here is supposed to be "list-to-dict" or "dict-to-list" + with pytest.raises(ValueError, match="Conversion type barbie is not 'list-to-dict' or 'dict-to-list'"): + jif._needs_conversion("barbie", ["ken"]) + # Setting tags with unfixable values immediately raises an error + tag = "exchange-params" + value = {"blockSize": 1, "nOuterVxx": "barbie"} + err_str = str(f"The {tag} tag with value:\n{value}\ncould not be fixed!") + with pytest.raises(ValueError, match=re.escape(err_str)): + # Implicitly tests validate_tags + jif[tag] = value + # Setting tags with unfixable values through "update" side-steps the error, but will raise it once + # "validate_tags" is inevitably called + jif2 = jif.copy() + jif2.update({tag: value}) + with pytest.raises(ValueError, match=re.escape(err_str)): + jif2.validate_tags(try_auto_type_fix=True) + # The inevitable error can be reduced to a warning if you tell it not to try to fix the values + with pytest.warns(UserWarning): + jif2.validate_tags(try_auto_type_fix=False) + # Setting a non-string tag raises an error within the JDFTXInfile object + err_str = str(f"{1.2} is not a string!") + with pytest.raises(TypeError, match=err_str): + jif[1.2] = 3.4 + + +def test_JDFTXInfile_niche_cases(): + jif = JDFTXInfile.from_file(ex_infile1_fname) + tag_object, tag, value = jif._preprocess_line("dump-only") + assert value == ("") + tag = "elec-ex-corr" + tag_object = get_tag_object(tag) + value = "gga" + params = jif.as_dict() + err_str = f"The '{tag}' tag appears multiple times in this input when it should not!" + with pytest.raises(ValueError, match=err_str): + jif._store_value(params, tag_object, tag, value) + struct = jif.to_pmg_structure(jif) + assert isinstance(struct, Structure) + noneout = jif.validate_tags(return_list_rep=True) + assert noneout is None + jif["fluid-solvent"] = {"name": "H2O", "concentration": 0.5} + assert len(jif["fluid-solvent"]) == 1 + jif.append_tag("fluid-solvent", {"name": "H2O", "concentration": 0.5}) + assert len(jif["fluid-solvent"]) == 2 + + +def test_JDFTXInfile_add_method(): + """Test the __add__ method""" + # No new values are being assigned in jif2, so jif + jif2 should be the same as jif + # Since the convenience of this method would be lost if the user has to pay special attention to duplicating + # repeatable values, repeatable tags are not append to each other + jif = JDFTXInfile.from_file(ex_infile1_fname) + jif2 = jif.copy() + jif3 = jif + jif2 + assert_idential_jif(jif, jif3) + # If a tag is repeated, the values must be the same since choice of value is ambiguous + key = "elec-ex-corr" + val_old = deepcopy(jif[key]) + val_new = "lda" + assert val_old != val_new + jif2[key] = val_new + err_str = f"JDFTXInfiles have conflicting values for {key}: {val_old} != {val_new}" + with pytest.raises(ValueError, match=re.escape(err_str)): + jif3 = jif + jif2 + # Normal expected behavior + key_add = "target-mu" + val_add = 0.5 + assert key_add not in jif + jif2 = jif.copy() + jif2[key_add] = val_add + jif3 = jif + jif2 + assert jif3[key_add]["mu"] == pytest.approx(val_add) + + +@pytest.mark.parametrize(("infile_fname", "knowns"), [(ex_infile1_fname, ex_infile1_knowns)]) +def test_JDFTXInfile_knowns_simple(infile_fname: PathLike, knowns: dict): + """Test that known values that can be tested with assert_same_value are correct""" + jif = JDFTXInfile.from_file(infile_fname) + for key, val in knowns.items(): + assert_same_value(jif[key], val) + + +def test_jdftxstructure(): + """Test the JDFTXStructure object associated with the JDFTXInfile object""" + jif = JDFTXInfile.from_file(ex_infile2_fname) + struct = jif.to_jdftxstructure(jif) + assert isinstance(struct, JDFTXStructure) + struc_str = str(struct) + assert isinstance(struc_str, str) + assert struct.natoms == 16 + with open(ex_infile2_fname) as f: + lines = list.copy(list(f)) + # Test different ways of creating a JDFTXStructure object create the same object if data is the same + data = "\n".join(lines) + struct2 = JDFTXStructure.from_str(data) + assert_equiv_jdftxstructure(struct, struct2) + struct3 = JDFTXStructure.from_dict(struct.as_dict()) + assert_equiv_jdftxstructure(struct, struct3) + + +def test_pmg_struc(): + jif = JDFTXInfile.from_file(ex_infile2_fname) + struc1 = jif.to_pmg_structure(jif) + struc2 = jif.structure + for s in [struc1, struc2]: + assert isinstance(s, Structure) + assert_idential_jif(struc1.as_dict(), struc2.as_dict()) + + +def test_jdftxtructure_naming(): + """Test the naming of the JDFTXStructure object. + + Test to make sure reading from a Structure with labels not exactly matching the element names + (ie Si0, Si1, or Si+2) will still be read correctly. + """ + struct = Structure.from_file(ex_in_files_dir / "Si.cif") + jstruct = JDFTXStructure(structure=struct) + JDFTXInfile.from_jdftxstructure(jstruct) + JDFTXInfile.from_structure(struct)