Source code for masci_tools.util.xml.converters

###############################################################################
# Copyright (c), Forschungszentrum Jülich GmbH, IAS-1/PGI-1, Germany.         #
#                All rights reserved.                                         #
# This file is part of the Masci-tools package.                               #
# (Material science tools)                                                    #
#                                                                             #
# The code is hosted on GitHub at https://github.com/judftteam/masci-tools.   #
# For further information on the license, see the LICENSE.txt file.           #
# For further information please visit http://judft.de/.                      #
#                                                                             #
###############################################################################
"""
Common functions for converting types to and from XML files
"""
from __future__ import annotations

from typing import Iterable, Any, Union
import sys
if sys.version_info >= (3, 10):
    from typing import TypeAlias
else:
    from typing_extensions import TypeAlias
if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal
from lxml import etree
import logging
from masci_tools.io.parsers import fleur_schema
import re

BaseType: TypeAlias = Literal['int', 'switch', 'string', 'float', 'float_expression', 'complex']
ConvertedType: TypeAlias = Union[int, float, bool, str, complex]


[docs]def convert_to_xml(value: Any | list[Any], schema_dict: fleur_schema.SchemaDict, name: str, text: bool = False, logger: logging.Logger | None = None, list_return: bool = False) -> tuple[str | list[str], bool]: """ Tries to converts a given string to the types specified in the schema_dict. First succeeded conversion will be returned If no logger is given and a attribute cannot be converted an error is raised :param stringattribute: str, Attribute to convert. :param schema_dict: Schema dictionary containing all the information :param name: name of the attribute or element :param text: bool, decides whether to take the definitions for text or attributes :param constants: dict, of constants defined in fleur input :param logger: logger object for logging warnings if given the errors are logged and the list is returned with the unconverted values otherwise a error is raised, when the first conversion fails :param list_return: if True, the returned quantity is always a list even if only one element is in it :return: The converted value of the first successful conversion """ if text: if name not in schema_dict['text_types']: raise KeyError(f'Unknown text tag: {name}') definitions = schema_dict['text_types'][name] float_format = '16.13' else: if name not in schema_dict['attrib_types']: raise KeyError(f'Unknown attribute name: {name}') definitions = schema_dict['attrib_types'][name] float_format = '.10' return convert_to_xml_explicit(value, definitions, logger=logger, list_return=list_return, float_format=float_format)
[docs]def convert_from_xml( xmlstring: str | list[str], schema_dict: fleur_schema.SchemaDict, name: str, text: bool = False, constants: dict[str, float] | None = None, logger: logging.Logger | None = None, list_return: bool = False ) -> tuple[ConvertedType | list[ConvertedType] | list[ConvertedType | list[ConvertedType]], bool]: """ Tries to converts a given string to the types specified in the schema_dict. First succeeded conversion will be returned If no logger is given and a attribute cannot be converted an error is raised :param stringattribute: str, Attribute to convert. :param schema_dict: Schema dictionary containing all the information :param name: name of the attribute or element :param text: bool, decides whether to take the definitions for text or attributes :param constants: dict, of constants defined in fleur input :param logger: logger object for logging warnings if given the errors are logged and the list is returned with the unconverted values otherwise a error is raised, when the first conversion fails :param list_return: if True, the returned quantity is always a list even if only one element is in it :return: The converted value of the first successful conversion """ if not isinstance(xmlstring, list): xmlstring = [xmlstring] if text: if name not in schema_dict['text_types']: raise KeyError(f'Unknown text tag: {name}') definitions = schema_dict['text_types'][name] xmlstring = [string.strip() for string in xmlstring] else: if name not in schema_dict['attrib_types']: raise KeyError(f'Unknown attribute name: {name}') definitions = schema_dict['attrib_types'][name] return convert_from_xml_explicit(xmlstring, definitions, constants=constants, logger=logger, list_return=list_return)
[docs]def convert_from_xml_explicit( xmlstring: str | list[str], definitions: list[fleur_schema.AttributeType], constants: dict[str, float] | None = None, logger: logging.Logger | None = None, list_return: bool = False ) -> tuple[ConvertedType | list[ConvertedType] | list[ConvertedType | list[ConvertedType]], bool]: """ Tries to converts a given string to the types given in definitions. First succeeded conversion will be returned If no logger is given and a attribute cannot be converted an error is raised :param stringattribute: str, Attribute to convert. :param definitions: list of :py:class:`~masci_tools.io.parsers.fleur_schema.AttributeType` definitions :param constants: dict, of constants defined in fleur input :param logger: logger object for logging warnings if given the errors are logged and the list is returned with the unconverted values otherwise a error is raised, when the first conversion fails :param list_return: if True, the returned quantity is always a list even if only one element is in it :return: The converted value of the first successful conversion """ if not isinstance(xmlstring, list): xmlstring = [xmlstring] if logger is not None: logger.debug('Value to convert from XML: %s', xmlstring) converted_list: list[ConvertedType | list[ConvertedType]] = [] all_success = True for text in xmlstring: split_text = text.split(' ') while '' in split_text: split_text.remove('') text_definitions = [] for definition in definitions: if definition.length == len(split_text): text_definitions.append(definition) if not text_definitions: for definition in definitions: if definition.length in ('unbounded', 1): text_definitions.append(definition) if not text_definitions: if logger is None: raise ValueError(f"Could not convert '{text}', no matching definition found") logger.warning("Could not convert '%s', no matching definition found", text) converted_list.append(text) all_success = False continue types = tuple(definition.base_type for definition in text_definitions) lengths = {definition.length for definition in text_definitions} if len(text_definitions) == 1: if text_definitions[0].length == 1: split_text = [text] if logger is not None: logger.debug('Convert from XML: %s using definitions %s', split_text, types) converted_text, suc = convert_from_xml_single_values(split_text, types, constants=constants, logger=logger) all_success = all_success and suc if len(converted_text) == 1 and 'unbounded' not in lengths: converted_text = converted_text[0] #type:ignore[assignment] elif len(converted_text) == 0 and 'unbounded' not in lengths: converted_text = '' #type:ignore converted_list.append(converted_text) ret_value = converted_list if len(converted_list) == 1 and not list_return: ret_value = converted_list[0] #type:ignore[assignment] if logger is not None: logger.debug('Converted Value from XML: %s', ret_value) return ret_value, all_success
[docs]def convert_to_xml_explicit(value: Any | Iterable[Any], definitions: list[fleur_schema.AttributeType], logger: logging.Logger | None = None, float_format: str = '.10', list_return: bool = False) -> tuple[str | list[str], bool]: """ Tries to convert a given list of values to str for a xml file based on the definitions (length and type). First succeeded conversion will be returned :param textvalue: value to convert :param definitions: list of :py:class:`~masci_tools.io.parsers.fleur_schema.AttributeType` definitions :param logger: logger object for logging warnings if given the errors are logged and the list is returned with the unconverted values otherwise a error is raised, when the first conversion fails :param list_return: if True, the returned quantity is always a list even if only one element is in it :return: The converted value of the first successful conversion """ import numpy as np lengths = {definition.length for definition in definitions} if not isinstance(value, (list, np.ndarray)): value = [value] elif not isinstance(value[0], (list, np.ndarray)) and lengths != {1}: value = [value] if logger is not None: logger.debug('Value to convert to XML: %s', value) converted_list = [] all_success = True for val in value: if not isinstance(val, (list, np.ndarray)): val = [val] text_definitions = [] for definition in definitions: if definition.length == len(val): text_definitions.append(definition) if not text_definitions: for definition in definitions: if definition.length == 'unbounded': text_definitions.append(definition) if not text_definitions: if len(val) == 1 and isinstance(val[0], str): converted_list.append(val[0]) continue if logger is None: raise ValueError(f"Could not convert '{val}', no matching definition found") logger.warning("Could not convert '%s', no matching definition found", val) converted_list.append('') all_success = False continue types = tuple(definition.base_type for definition in text_definitions) if logger is not None: logger.debug('Convert to XML: %s with definitions %s', val, types) converted_text, suc = convert_to_xml_single_values(val, types, logger=logger, float_format=float_format) all_success = all_success and suc converted_list.append(' '.join(converted_text)) ret_value = converted_list if len(converted_list) == 1 and not list_return: ret_value = converted_list[0] #type:ignore if logger is not None: logger.debug('Converted Value to XML: %s', ret_value) return ret_value, all_success
[docs]def convert_from_xml_single_values(xmlstring: str | list[str], possible_types: tuple[BaseType, ...], constants: dict[str, float] | None = None, logger: logging.Logger | None = None) -> tuple[list[ConvertedType], bool]: """ Tries to converts a given string attribute to the types given in possible_types. First succeeded conversion will be returned If no logger is given and a attribute cannot be converted an error is raised :param stringattribute: str, Attribute to convert. :param possible_types: list of str What types it will try to convert to :param constants: dict, of constants defined in fleur input :param logger: logger object for logging warnings if given the errors are logged and the list is returned with the unconverted values otherwise a error is raised, when the first conversion fails :param list_return: if True, the returned quantity is always a list even if only one element is in it :return: The converted value of the first successful conversion """ from masci_tools.util.fleur_calculate_expression import calculate_expression, MissingConstant if not isinstance(xmlstring, list): xmlstring = [xmlstring] converted_value: ConvertedType converted_list = [] all_success = True for text in xmlstring: exceptions: list[Exception] = [] for value_type in possible_types: if value_type == 'float': try: converted_value = float(text) except (ValueError, TypeError) as exc: exceptions.append(exc) continue elif value_type == 'float_expression': try: converted_value = calculate_expression(text, constants=constants) except ValueError as exc: exceptions.append(exc) continue except MissingConstant as exc: new_exc = MissingConstant(f'No value available for expression {exc}\n' 'Please provide the value for this constant' ' by using the get_constants function for example') exceptions.append(new_exc) continue elif value_type == 'complex': try: converted_value = convert_from_fortran_complex(text) except (ValueError, TypeError) as exc: exceptions.append(exc) continue elif value_type == 'int': try: converted_value = int(text) except (ValueError, TypeError) as exc: exceptions.append(exc) continue elif value_type == 'switch': try: converted_value = convert_from_fortran_bool(text) except (ValueError, TypeError) as exc: exceptions.append(exc) continue elif value_type == 'string': converted_value = str(text) converted_list.append(converted_value) break else: if logger is None: raise ValueError(f"Could not convert '{text}'. Tried: {possible_types}.\n" 'The following errors occurred:\n ' + '\n '.join([str(error) for error in exceptions])) logger.warning("Could not convert '%s'. The following errors occurred:", text) for error in exceptions: logger.warning(' %s', str(error)) logger.debug(error, exc_info=error) converted_list.append(text) all_success = False return converted_list, all_success
[docs]def convert_to_xml_single_values(value: Any | Iterable[Any], possible_types: tuple[BaseType, ...], logger: logging.Logger | None = None, float_format: str = '.10') -> tuple[list[str], bool]: """ Tries to converts a given attributevalue to a string for a xml file according to the types given in possible_types. First succeeded conversion will be returned :param value: value to convert. :param possible_types: list of str What types it will try to convert from :param logger: logger object for logging warnings if given the errors are logged and the list is returned with the unconverted values otherwise a error is raised, when the first conversion fails :param list_return: if True, the returned quantity is always a list even if only one element is in it :return: The converted str of the value of the first successful conversion """ import numpy as np if not isinstance(value, (list, np.ndarray)): value = [value] if any(val is None for val in value): if logger is not None: logger.error("Could not convert '%s' to text. All values have to be not None", value) raise ValueError(f"Could not convert '{value}' to text. All values have to be not None") converted_value: str converted_list = [] exceptions: list[Exception] = [] all_success = True for val in value: for value_type in possible_types: if value_type in ('float', 'float_expression'): if isinstance(val, complex): exceptions.append(ValueError(f'Could not convert {val} to fortran float. Value is complex')) continue try: converted_value = f'{val:{float_format}f}' except ValueError as exc: exceptions.append(exc) continue elif value_type == 'complex': try: converted_value = f'({val.real:{float_format}f},{val.imag:{float_format}f})' except (ValueError, AttributeError) as exc: exceptions.append(exc) continue elif value_type == 'switch': try: converted_value = convert_to_fortran_bool(val) except (ValueError, TypeError) as exc: exceptions.append(exc) continue elif value_type == 'int': try: converted_value = f'{val:d}' except ValueError as exc: exceptions.append(exc) continue elif value_type == 'string': converted_value = str(val) converted_list.append(converted_value) break else: if isinstance(val, str): converted_list.append(val) else: if logger is None: raise ValueError(f"Could not convert '{val}' to text. Tried: {possible_types}.\n" 'The following errors occurred:\n ' + '\n '.join([str(error) for error in exceptions])) logger.warning("Could not convert '%s' to text. The following errors occurred:", val) for error in exceptions: logger.warning(' %s', str(error)) logger.debug(error, exc_info=error) converted_list.append(val) all_success = False return converted_list, all_success
[docs]def convert_from_fortran_bool(stringbool: str | bool) -> bool: """ Converts a string in this case ('T', 'F', or 't', 'f') to True or False :param stringbool: a string ('t', 'f', 'F', 'T') :return: boolean (either True or False) """ true_items = ['True', 't', 'T'] false_items = ['False', 'f', 'F'] if isinstance(stringbool, str): if stringbool in false_items: return False if stringbool in true_items: return True raise ValueError(f"Could not convert: '{stringbool}' to boolean, " "which is not 'True', 'False', 't', 'T', 'F' or 'f'") if isinstance(stringbool, bool): return stringbool # no conversion needed... raise TypeError(f"Could not convert: '{stringbool}' to boolean, only accepts str or boolean")
[docs]def convert_to_fortran_bool(boolean: bool | str) -> Literal['T', 'F']: """ Converts a Boolean as string to the format defined in the input :param boolean: either a boolean or a string ('True', 'False', 'F', 'T') :return: a string (either 't' or 'f') """ if isinstance(boolean, bool): if boolean: return 'T' return 'F' if isinstance(boolean, str): # basestring): if boolean in ('True', 't', 'T'): return 'T' if boolean in ('False', 'f', 'F'): return 'F' raise ValueError(f"A string: {boolean} for a boolean was given, which is not 'True'," "'False', 't', 'T', 'F' or 'f'") raise TypeError(f'convert_to_fortran_bool accepts only a string or bool as argument, given {boolean} ')
[docs]def convert_from_fortran_complex(number_str: str) -> complex: """ Converts a string of the form (float,float) to a complex number :param number_str: string to convert :returns: complex number """ number_str = number_str.strip() RE_COMPLEX_NUMBER = r'\([-+]?(?:\d*\.\d+|\d+)\,[-+]?(?:\d*\.\d+|\d+)\)' RE_SINGLE_FLOAT = r'[-+]?(?:\d*\.\d+|\d+)' if re.fullmatch(RE_COMPLEX_NUMBER, number_str) is None: raise ValueError(f"String '{number_str}' is not of the format (float,float)") real_str, imag_str = re.findall(RE_SINGLE_FLOAT, number_str) return float(real_str) + 1j * float(imag_str)
[docs]def convert_fleur_lo(loelements: list[etree._Element], allow_special_los: bool = True) -> str: """ Converts lo xml elements from the inp.xml file into a lo string for the inpgen """ # Developer hint: Be careful with using '' and "", basestring and str are not the same... # therefore other conversion methods might fail, or the wrong format could be written. from masci_tools.util.xml.common_functions import get_xml_attribute shell_map = {0: 's', 1: 'p', 2: 'd', 3: 'f'} lo_string = '' for element in loelements: lo_type = get_xml_attribute(element, 'type') if lo_type != 'SCLO' and not allow_special_los: # non standard los not supported for now continue eDeriv = get_xml_attribute(element, 'eDeriv') if allow_special_los and eDeriv not in ('0', '1'): # LOs with higher derivatives are also dropped continue if not allow_special_los and eDeriv != '0': continue l_num = get_xml_attribute(element, 'l') n_num = get_xml_attribute(element, 'n') if l_num is None or n_num is None: raise ValueError('Failed to evaluate l and n attribute of LO element') lostr = f'{n_num}{shell_map[int(l_num)]}' lo_string = lo_string + ' ' + lostr return lo_string.strip()
[docs]def convert_fleur_electronconfig(econfig_element: etree._Element) -> str: """ Convert electronConfig tag to eConfig string """ from masci_tools.util.xml.common_functions import eval_xpath_one from masci_tools.util.econfig import convert_fleur_config_to_econfig core_config = eval_xpath_one(econfig_element, 'coreConfig/text()', str) valence_config = eval_xpath_one(econfig_element, 'valenceConfig/text()', str) core_config_str = convert_fleur_config_to_econfig(core_config) valence_config_str = convert_fleur_config_to_econfig(valence_config) return f'{core_config_str} | {valence_config_str}'
[docs]def convert_str_version_number(version_str: str) -> tuple[int, int]: """ Convert the version number as a integer for easy comparisons :param version_str: str of the version number, e.g. '0.33' :returns: tuple of ints representing the version str """ version_numbers = version_str.split('.') if len(version_numbers) != 2: raise ValueError(f"Version number is malformed: '{version_str}'") return tuple(int(part) for part in version_numbers) #type:ignore