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