Source code for aiida_fleur.calculation.fleurinputgen

# -*- coding: utf-8 -*-
###############################################################################
# Copyright (c), Forschungszentrum Jülich GmbH, IAS-1/PGI-1, Germany.         #
#                All rights reserved.                                         #
# This file is part of the AiiDA-FLEUR package.                               #
#                                                                             #
# The code is hosted on GitHub at https://github.com/JuDFTteam/aiida-fleur    #
# For further information on the license, see the LICENSE.txt file            #
# For further information please visit http://www.flapw.de or                 #
# http://aiida-fleur.readthedocs.io/en/develop/                               #
###############################################################################
"""
Input plug-in for the FLEUR input generator 'inpgen'.
The input generator for the Fleur code is a preprocessor
and should be run locally (with the direct scheduler) or inline,
because it does not take many resources.
"""
from __future__ import absolute_import
import six
from six.moves import zip as zip_six

from aiida.engine import CalcJob
from aiida.common.exceptions import InputValidationError
from aiida.common.datastructures import CalcInfo, CodeInfo
from aiida.common.constants import elements as PeriodicTableElements
from aiida.orm import StructureData, Dict

from aiida_fleur.data.fleurinp import FleurinpData
from aiida_fleur.tools.StructureData_util import abs_to_rel_f, abs_to_rel
from aiida_fleur.tools.xml_util import convert_to_fortran_bool, convert_to_fortran_string
from aiida_fleur.common.constants import bohr_a


[docs]class FleurinputgenCalculation(CalcJob): """ JobCalculationClass for the inpgen, which is a preprocessor for a FLEUR calculation. For more information about produced files and the FLEUR-code family, go to http://www.flapw.de/. """ __version__ = '1.2.0' # Default input and output files _INPUT_FILE = 'aiida.in' # will be shown with inputcat _OUTPUT_FILE = 'out' # 'shell.out' #will be shown with outputcat # created file names, some needed for Fleur calc _INPXML_FILE_NAME = 'inp.xml' _INPUT_FILE_NAME = 'aiida.in' _SHELLOUT_FILE_NAME = 'shell.out' _OUTPUT_FILE_NAME = 'out' _ERROR_FILE_NAME = 'out.error' _STRUCT_FILE_NAME = 'struct.xsf' _settings_keys = ['additional_retrieve_list', 'remove_from_retrieve_list', 'cmdline'] # TODO switch all these to init_internal_params? _OUTPUT_SUBFOLDER = './fleur_inp_out/' _PREFIX = 'aiida' # Additional files that should always be retrieved for the specific plugin _internal_retrieve_list = [] _automatic_namelists = {} # Specify here what namelist and parameters the inpgen takes _possible_namelists = [ 'title', 'input', 'lattice', 'gen', 'shift', 'factor', 'qss', 'soc', 'atom', 'comp', 'exco', 'film', 'kpt', 'end' ] # this order is important! _possible_params = { 'input': ['film', 'cartesian', 'cal_symm', 'checkinp', 'symor', 'oldfleur'], 'lattice': ['latsys', 'a0', 'a', 'b', 'c', 'alpha', 'beta', 'gamma'], 'atom': ['id', 'z', 'rmt', 'dx', 'jri', 'lmax', 'lnonsph', 'ncst', 'econfig', 'bmu', 'lo', 'element', 'name'], 'comp': ['jspins', 'frcor', 'ctail', 'kcrel', 'gmax', 'gmaxxc', 'kmax'], 'exco': ['xctyp', 'relxc'], 'film': ['dvac', 'dtild'], 'soc': ['theta', 'phi'], 'qss': ['x', 'y', 'z'], 'kpt': ['nkpt', 'kpts', 'div1', 'div2', 'div3', 'tkb', 'tria'], 'title': {} } # Keywords that cannot be set # TODO: To specify what combinations are not allowed together, # or not at all (lattice ?, shift, scale) _blocked_keywords = [] # TODO different kpt mods? (use kpointNode)? FleurinpdData can do it. _use_kpoints = False # If two lattices are given, via the input &lattice # and the aiida structure prefare the aiida structure? # currently is not allow the use of &lattice _use_aiida_structure = True # Default title _inp_title = 'A Fleur input generator calculation with aiida'
[docs] @classmethod def define(cls, spec): super(FleurinputgenCalculation, cls).define(spec) spec.input('metadata.options.input_filename', valid_type=six.string_types, default=cls._INPUT_FILE) spec.input('metadata.options.output_filename', valid_type=six.string_types, default=cls._INPXML_FILE_NAME) spec.input('structure', valid_type=StructureData, help='Choose the input structure to use') spec.input('parameters', valid_type=Dict, required=False, help='Use a node that specifies the input parameters ' 'for the namelists') spec.input('settings', valid_type=Dict, required=False, help='This parameter data node is used to specify for some ' 'advanced features how the plugin behaves. You can add files' 'the retrieve list, or add command line switches, ' 'for all available features here check the documentation.') # parser spec.input('metadata.options.parser_name', valid_type=six.string_types, default='fleur.fleurinpgenparser') # declaration of outputs of the calclation spec.output('fleurinpData', valid_type=FleurinpData, required=True) # exit codes # spec.exit_code(251, 'ERROR_WRONG_INPUT_PARAMS', # message='Input parameters for inpgen contain unknown keys.') # spec.exit_code(253, 'ERROR_ATOM_POSITION_NEEDED', # message='Fleur lattice needs atom positions as input.') # spec.exit_code(254, 'ERROR_INPUT_PARAMS_LEFTOVER', # message='Excessive input parameters were specified.') spec.exit_code(300, 'ERROR_NO_RETRIEVED_FOLDER', message='No retrieved folder found.') spec.exit_code(301, 'ERROR_OPENING_OUTPUTS', message='One of the output files can not be opened.') spec.exit_code(306, 'ERROR_NO_INPXML', message='XML input file was not found.') spec.exit_code(307, 'ERROR_MISSING_RETRIEVED_FILES', message='Some required files were not retrieved.') spec.exit_code(308, 'ERROR_FLEURINPDATA_INPUT_NOT_VALID', message=('During parsing: FleurinpData could not be initialized, see log. ' 'Maybe no Schemafile was found or the Fleurinput is not valid.')) spec.exit_code(309, 'ERROR_FLEURINPDATE_NOT_VALID', message='During parsing: FleurinpData failed validation.')
[docs] def prepare_for_submission(self, folder): """ This is the routine to be called when you want to create the input files for the inpgen with the plug-in. :param folder: a aiida.common.folders.Folder subclass where the plugin should put all its files. """ # Get the connection between coordination number and element symbol _atomic_numbers = {data['symbol']: num for num, data in six.iteritems(PeriodicTableElements)} possible_namelists = self._possible_namelists possible_params = self._possible_params local_copy_list = [] remote_copy_list = [] remote_symlink_list = [] bulk = True film = False # convert these 'booleans' to the inpgen format. replacer_values_bool = [True, False, 'True', 'False', 't', 'T', 'F', 'f'] # some keywords require a string " around them in the input file. string_replace = ['econfig', 'lo', 'element', 'name'] # of some keys only the values are written to the file, specify them here. val_only_namelist = ['soc', 'qss'] # Scaling comes from the Structure # but we have to convert from Angstrom to a.u (bohr radii) scaling_factors = [1.0, 1.0, 1.0] scaling_lat = 1. # /bohr_to_ang = 0.52917720859 scaling_pos = 1. / bohr_a # Angstrom to atomic own_lattice = False # not self._use_aiida_structure ########################################## ############# INPUT CHECK ################ ########################################## # first check existence of structure and if 1D, 2D, 3D structure = self.inputs.structure pbc = structure.pbc if False in pbc: bulk = False film = True # check existence of parameters (optional) if 'parameters' in self.inputs: parameters = self.inputs.parameters else: parameters = None if parameters is None: # use default parameters_dict = {} else: parameters_dict = _lowercase_dict(parameters.get_dict(), dict_name='parameters') # we write always out rel coordinates, because thats the way FLEUR uses # them best. we have to convert them from abs, because thats how they # are stored in a Structure node. cartesian=F is default if 'input' in parameters_dict: parameters_dict['input']['cartesian'] = False if film: parameters_dict['input']['film'] = True else: if bulk: parameters_dict['input'] = {'cartesian': False} elif film: parameters_dict['input'] = {'cartesian': False, 'film': True} namelists_toprint = possible_namelists input_params = parameters_dict if 'title' in list(input_params.keys()): self._inp_title = input_params.pop('title') # TODO validate type of values of the input parameter keys ? # check input_parameters for namelist, paramdic in six.iteritems(input_params): if 'atom' in namelist: # this namelist can be specified more often # special atom namelist needs to be set for writing, # but insert it in the right spot! index = namelists_toprint.index('atom') + 1 namelists_toprint.insert(index, namelist) namelist = 'atom' if namelist not in possible_namelists: raise InputValidationError("The namelist '{0}' is not supported by the fleur" " inputgenerator. Check on the fleur website or add '{0}'" 'to _possible_namelists.'.format(namelist)) for para in paramdic.keys(): if para not in possible_params[namelist]: raise InputValidationError("The property '{}' is not supported by the " "namelist '{}'. " 'Check the fleur website, or if it really is,' ' update _possible_params. '.format(para, namelist)) if para in string_replace: # TODO check if its in the parameter dict paramdic[para] = convert_to_fortran_string(paramdic[para]) # things that are in string replace can never be a bool # Otherwise input where someone given the title 'F' would fail... elif paramdic[para] in replacer_values_bool: # because 1/1.0 == True, and 0/0.0 == False # maybe change in convert_to_fortran that no error occurs if isinstance(paramdic[para], (int, float)): if isinstance(paramdic[para], bool): paramdic[para] = convert_to_fortran_bool(paramdic[para]) else: paramdic[para] = convert_to_fortran_bool(paramdic[para]) # in fleur it is possible to give a lattice namelist if 'lattice' in list(input_params.keys()): own_lattice = True if structure in self.inputs: # two structures given? # which one should be prepared? TODO: log warning or even error if self._use_aiida_structure: input_params.pop('lattice', {}) own_lattice = False # TODO allow only usual kpt meshes and use therefore Aiida kpointData # if self._use_kpoints: # try: # kpoints = inputdict.pop(self.get_linkname('kpoints')) # except KeyError: # raise InputValidationError("No kpoints specified for this" # " calculation") # if not isinstance(kpoints, KpointsData): # raise InputValidationError("kpoints is not of type KpointsData") code = self.inputs.code # check existence of settings (optional) if 'settings' in self.inputs: settings = self.inputs.settings else: settings = None if settings is None: settings_dict = {} else: settings_dict = settings.get_dict() # check for for allowed keys, ignore unknown keys but warn. for key in settings_dict.keys(): if key not in self._settings_keys: # TODO warning self.logger.info('settings dict key %s for Fleur calculation' 'not recognized, only %s are allowed.', key, str(self._settings_keys)) ############################## # END OF INITIAL INPUT CHECK # ############################## ####################################################### ######### PREPARE PARAMETERS FOR INPUT FILE ########### ####################################################### #### STRUCTURE_PARAMETERS #### scaling_factor_card = '' cell_parameters_card = '' if not own_lattice: cell = structure.cell for vector in cell: scaled = [a * scaling_pos for a in vector] # scaling_pos=1./bohr_to_ang cell_parameters_card += ('{0:18.10f} {1:18.10f} {2:18.10f}' '\n'.format(scaled[0], scaled[1], scaled[2])) scaling_factor_card += ('{0:18.10f} {1:18.10f} {2:18.10f}' '\n'.format(scaling_factors[0], scaling_factors[1], scaling_factors[2])) #### ATOMIC_POSITIONS #### # TODO: be careful with units atomic_positions_card_list = [''] atomic_positions_card_listtmp = [''] if not own_lattice: natoms = len(structure.sites) # for FLEUR true, general not, because you could put several # atoms on a site # TODO: test that only one atom at site? # TODO this feature might change in Fleur, do different. that in inpgen kind gets a name, which will also be the name in fleur inp.xml. # now user has to make kind_name = atom id. for site in structure.sites: kind_name = site.kind_name kind = structure.get_kind(kind_name) if kind.has_vacancies: # then we do not at atoms with weights smaller one if kind.weights[0] < 1.0: natoms = natoms - 1 # Log message? continue # TODO: list I assume atoms therefore I just get the first one... site_symbol = kind.symbols[0] atomic_number = _atomic_numbers[site_symbol] atomic_number_name = atomic_number # per default we use relative coordinates in Fleur # we have to scale back to atomic units from angstrom pos = site.position if bulk: vector_rel = abs_to_rel(pos, cell) elif film: vector_rel = abs_to_rel_f(pos, cell, structure.pbc) vector_rel[2] = vector_rel[2] * scaling_pos if site_symbol != kind_name: # This is an important fact, if user renames it becomes a new specie! suc = True try: head = kind_name.rstrip('0123456789') kind_namet = int(kind_name[len(head):]) except ValueError: suc = False if suc: atomic_number_name = '{}.{}'.format(atomic_number, kind_namet) # append a label to the detached atom atomic_positions_card_listtmp.append(' {0:7} {1:18.10f} {2:18.10f} {3:18.10f} {4}' '\n'.format(atomic_number_name, vector_rel[0], vector_rel[1], vector_rel[2], kind_namet)) else: atomic_positions_card_listtmp.append(' {0:7} {1:18.10f} {2:18.10f} {3:18.10f}' '\n'.format(atomic_number_name, vector_rel[0], vector_rel[1], vector_rel[2])) # TODO check format # we write it later, since we do not know what natoms is before the loop... atomic_positions_card_list.append(' {0:3}\n'.format(natoms)) for card in atomic_positions_card_listtmp: atomic_positions_card_list.append(card) else: # TODO with own lattice atomic positions have to come from somewhere # else.... User input? raise InputValidationError('fleur lattice needs also the atom ' ' position as input,' ' not implemented yet, sorry!') atomic_positions_card = ''.join(atomic_positions_card_list) del atomic_positions_card_list # Free memory #### Kpts #### # TODO: kpts # kpoints_card = ""#.join(kpoints_card_list) #del kpoints_card_list ####################################### #### WRITE ALL CARDS IN INPUT FILE #### input_filename = folder.get_abs_path(self._INPUT_FILE_NAME) with open(input_filename, 'w') as infile: # first write title infile.write('{0}\n'.format(self._inp_title)) # then write &input namelist infile.write('&{0}'.format('input')) # namelist content; set to {} if not present, so that we leave an # empty namelist namelist = input_params.pop('input', {}) for k, val in sorted(six.iteritems(namelist)): infile.write(get_input_data_text(k, val, False, mapping=None)) infile.write('/\n') # Write lattice information now infile.write(cell_parameters_card) infile.write('{0:18.10f}\n'.format(scaling_lat)) infile.write(scaling_factor_card) infile.write('\n') # Write Atomic positons infile.write(atomic_positions_card) # Write namelists after atomic positions for namels_name in namelists_toprint: namelist = input_params.pop(namels_name, {}) if namelist: if 'atom' in namels_name: namels_name = 'atom' infile.write('&{0}\n'.format(namels_name)) if namels_name in val_only_namelist: make_reversed = False if namels_name == 'soc': make_reversed = True for k, val in sorted(six.iteritems(namelist), reverse=make_reversed): infile.write(get_input_data_text(k, val, True, mapping=None)) else: for k, val in sorted(six.iteritems(namelist)): infile.write(get_input_data_text(k, val, False, mapping=None)) infile.write('/\n') # infile.write(kpoints_card) if input_params: raise InputValidationError('input_params leftover: The following namelists are specified' ' in input_params, but are ' 'not valid namelists for the current type of calculation: ' '{}'.format(','.join(list(input_params.keys())))) calcinfo = CalcInfo() calcinfo.uuid = self.uuid calcinfo.local_copy_list = local_copy_list calcinfo.remote_copy_list = remote_copy_list calcinfo.remote_symlink_list = remote_symlink_list # Retrieve per default only out file and inp.xml file? retrieve_list = [] retrieve_list.append(self._INPXML_FILE_NAME) retrieve_list.append(self._OUTPUT_FILE_NAME) retrieve_list.append(self._SHELLOUT_FILE_NAME) retrieve_list.append(self._ERROR_FILE_NAME) retrieve_list.append(self._STRUCT_FILE_NAME) retrieve_list.append(self._INPUT_FILE_NAME) # user specific retrieve add_retrieve = settings_dict.get('additional_retrieve_list', []) for file1 in add_retrieve: retrieve_list.append(file1) remove_retrieve = settings_dict.get('remove_from_retrieve_list', []) for file1 in remove_retrieve: if file1 in retrieve_list: retrieve_list.remove(file1) calcinfo.retrieve_list = [] for file1 in retrieve_list: calcinfo.retrieve_list.append(file1) codeinfo = CodeInfo() # , "-electronConfig"] # TODO? let the user decide -electronconfig? #cmdline_params = ['-explicit', '-inc', '+all', '-f', '{}'.format(self._INPUT_FILE_NAME)] cmdline_params = ['-explicit'] # user specific commandline_options for command in settings_dict.get('cmdline', []): cmdline_params.append(command) codeinfo.cmdline_params = (list(cmdline_params)) codeinfo.code_uuid = code.uuid codeinfo.stdin_name = self._INPUT_FILE_NAME codeinfo.stdout_name = self._SHELLOUT_FILE_NAME # shell output will be piped in file codeinfo.stderr_name = self._ERROR_FILE_NAME # std error too calcinfo.codes_info = [codeinfo] return calcinfo
[docs]def conv_to_fortran(val, quote_strings=True): """ :param val: the value to be read and converted to a Fortran-friendly string. """ # Note that bool should come before integer, because a boolean matches also # isinstance(...,int) import numpy import numbers if isinstance(val, (bool, numpy.bool_)): if val: val_str = '.true.' else: val_str = '.false.' elif isinstance(val, numbers.Integral): val_str = '{:d}'.format(val) elif isinstance(val, numbers.Real): val_str = ('{:18.10e}'.format(val)).replace('e', 'd') elif isinstance(val, six.string_types): if quote_strings: val_str = "'{!s}'".format(val) else: val_str = '{!s}'.format(val) else: raise ValueError("Invalid value '{}' of type '{}' passed, accepts only booleans, ints, " 'floats and strings'.format(val, type(val))) return val_str
# TODO rewrite for fleur/ delete unnecessary parts
[docs]def get_input_data_text(key, val, value_only, mapping=None): """ Given a key and a value, return a string (possibly multiline for arrays) with the text to be added to the input file. :param key: the flag name :param val: the flag value. If it is an array, a line for each element is produced, with variable indexing starting from 1. Each value is formatted using the conv_to_fortran function. :param mapping: Optional parameter, must be provided if val is a dictionary. It maps each key of the 'val' dictionary to the corresponding list index. For instance, if ``key='magn'``, ``val = {'Fe': 0.1, 'O': 0.2}`` and ``mapping = {'Fe': 2, 'O': 1}``, this function will return the two lines ``magn(1) = 0.2`` and ``magn(2) = 0.1``. This parameter is ignored if 'val' is not a dictionary. """ #from aiida.common.utils import conv_to_fortran # I don't try to do iterator=iter(val) and catch TypeError because # it would also match strings # I check first the dictionary, because it would also matc # hasattr(__iter__) if isinstance(val, dict): if mapping is None: raise ValueError("If 'val' is a dictionary, you must provide also " "the 'mapping' parameter") # At difference with the case of a list, at the beginning # list_of_strings # is a list of 2-tuples where the first element is the idx, and the # second is the actual line. This is used at the end to # resort everything. list_of_strings = [] for elemk, itemval in six.iteritems(val): try: idx = mapping[elemk] except KeyError: raise ValueError("Unable to find the key '{}' in the mapping " 'dictionary'.format(elemk)) list_of_strings.append((idx, ' {0}({2})={1} '.format(key, conv_to_fortran(itemval), idx))) # changed {0}({2}) = {1}\n".format # I first have to resort, then to remove the index from the first # column, finally to join the strings list_of_strings = list(zip_six(*sorted(list_of_strings)))[1] return ''.join(list_of_strings) elif not isinstance(val, six.string_types) and hasattr(val, '__iter__'): if value_only: list_of_strings = [ ' ({1}){0} '.format(conv_to_fortran(itemval), idx + 1) for idx, itemval in enumerate(val) ] else: # a list/array/tuple of values list_of_strings = [ ' {0}({2})={1} '.format(key, conv_to_fortran(itemval), idx + 1) for idx, itemval in enumerate(val) ] return ''.join(list_of_strings) else: # single value # return " {0}={1} ".format(key, conv_to_fortran(val)) if value_only: return ' {0} '.format(val) else: return ' {0}={1} '.format(key, val)
def _lowercase_dict(dic, dict_name): """ Converts every entry in a dictionary to lowercase :param dic: parameters dictionary :param dict_name: dictionary name """ from collections import Counter if isinstance(dic, dict): new_dict = dict((str(k).lower(), val) for k, val in six.iteritems(dic)) if len(new_dict) != len(dic): num_items = Counter(str(k).lower() for k in dic.keys()) double_keys = ','.join([k for k, val in num_items if val > 1]) raise InputValidationError("Inside the dictionary '{}' there are the following keys that " 'are repeated more than once when compared case-insensitively:' '{}.This is not allowed.'.format(dict_name, double_keys)) return new_dict else: raise TypeError('_lowercase_dict accepts only dictionaries as argument')