# -*- 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/ #
###############################################################################
"""
In here we put all things (methods) that are common to workflows AND
depend on AiiDA classes, therefore can only be used if the dbenv is loaded.
Util that does not depend on AiiDA classes should go somewhere else.
"""
from __future__ import absolute_import
from __future__ import print_function
import six
from aiida.orm import Node, load_node
from aiida.plugins import DataFactory, CalculationFactory
[docs]def is_code(code):
"""
Test if the given input is a Code node, by object, id, uuid, or pk
if yes returns a Code node in all cases
if no returns None
"""
from aiida.orm import Code
from aiida.common.exceptions import NotExistent, MultipleObjectsError, InputValidationError
if isinstance(code, Code):
return code
try:
pk = int(code)
except ValueError:
codestring = str(code)
try:
code = Code.get_from_string(codestring)
except NotExistent:
try:
code = load_node(codestring)
except NotExistent:
code = None
except (InputValidationError, MultipleObjectsError):
code = None
else:
try:
code = load_node(pk)
except NotExistent:
code = None
if isinstance(code, Code):
return code
else:
return None
# test
###############################
# codename = 'inpgen@local_mac'#'inpgen_v0.28@iff003'#'inpgen_iff@local_iff'
# codename2 = 'fleur_v0.28@iff003'#'fleur_mpi_v0.28@iff003'# 'fleur_iff_0.28@local_iff''
# codename2 = 'fleur_max_1.3_dev@iff003'
# codename2 = 'fleur_mpi_max_1.3_dev@iff003'
# codename4 = 'fleur_mpi_v0.28@claix'
###############################
# code = Code.get_from_string(codename)
# code2 = Code.get_from_string(codename2)
# code4 = Code.get_from_string(codename4)
# print(get_scheduler_extras(code, {'num_machines' : 1}))
# print(get_scheduler_extras(code2, {'num_machines' : 2}))
# print(get_scheduler_extras(code4, {'num_machines' : 1}))
[docs]def test_and_get_codenode(codenode, expected_code_type, use_exceptions=False):
"""
Pass a code node and an expected code (plugin) type. Check that the
code exists, is unique, and return the Code object.
:param codenode: the name of the code to load (in the form label@machine)
:param expected_code_type: a string with the plugin that is expected to
be loaded. In case no plugins exist with the given name, show all existing
plugins of that type
:param use_exceptions: if True, raise a ValueError exception instead of
calling sys.exit(1)
:return: a Code object
"""
import sys
from aiida.common.exceptions import NotExistent
from aiida.orm import Code
try:
if codenode is None or not isinstance(codenode, Code):
raise ValueError
code = codenode
if code.get_input_plugin_name() != expected_code_type:
raise ValueError
except ValueError:
from aiida.orm.querybuilder import QueryBuilder
qb = QueryBuilder()
qb.append(Code, filters={'attributes.input_plugin': {'==': expected_code_type}}, project='*')
valid_code_labels = ['{}@{}'.format(c.label, c.computer.name) for [c] in qb.all()]
if valid_code_labels:
msg = ('Given Code node is not of expected code type.\n'
'Valid labels for a {} executable are:\n'.format(expected_code_type))
msg += '\n'.join('* {}'.format(l) for l in valid_code_labels)
if use_exceptions:
raise ValueError(msg)
else:
print(msg) # , file=sys.stderr)
sys.exit(1)
else:
msg = ('Code not valid, and no valid codes for {}.\n'
'Configure at least one first using\n'
' verdi code setup'.format(expected_code_type))
if use_exceptions:
raise ValueError(msg)
else:
print(msg) # , file=sys.stderr)
sys.exit(1)
return code
[docs]def get_kpoints_mesh_from_kdensity(structure, kpoint_density):
"""
params: structuredata, Aiida structuredata
params: kpoint_density
returns: tuple (mesh, offset)
returns: kpointsdata node
"""
KpointsData = DataFactory('array.kpoints')
kp = KpointsData()
kp.set_cell_from_structure(structure)
density = kpoint_density # 1/A
kp.set_kpoints_mesh_from_density(density)
mesh = kp.get_kpoints_mesh()
return mesh, kp
# test
# print(get_kpoints_mesh_from_kdensity(load_node(structure(120)), 0.1))
# (([33, 33, 18], [0.0, 0.0, 0.0]), <KpointsData: uuid: cee9d05f-b31a-44d7-aa72-30a406712fba (unstored)>)
# mesh, kp = get_kpoints_mesh_from_kdensity(structuredata, 0.1)
# print mesh[0]
# TODO maybe allow lists of uuids in workchain dict, or write a second funtion for this,...
# The question is how do get the 'enthalpy for a reaction out of my database?
# where I have redundant calculations or calculations with different parameters...
# are total energies comparable?
# -> as long as the same scheme ist used (all GGA or all GGA+U)
# total energies are compareable and the gibs enthalpy is approximately the
# total energy difference
# there are tricks to also compare mixed energies, with experimental fits
# for binary reactions, where both is needed
[docs]def determine_favorable_reaction(reaction_list, workchain_dict):
"""
Finds out with reaction is more favorable by simple energy standpoints
# TODO check physics
reaction list: list of reaction strings
workchain_dict = {'Be12W' : uuid_wc or output, 'Be2W' : uuid, ...}
return dictionary that ranks the reactions after their enthalpy
"""
from aiida.engine import WorkChain
from aiida_fleur.tools.common_fleur_wf_util import get_enhalpy_of_equation
# for each reaction get the total energy sum
# make sure to use the right multipliers...
# then sort the given list from (lowest if negativ energies to highest)
energy_sorted_reactions = []
formenergy_dict = {}
for compound, uuid in six.iteritems(workchain_dict):
# TODO ggf get formation energy from output node, or extras
if isinstance(uuid, float): # allow to give values
formenergy_dict[compound] = uuid
continue
n = load_node(uuid)
extras = n.get_extras() # sadly there is no get(,) method...
try:
formenergy = extras.get('formation_energy', None)
except KeyError:
formenergy = None
if not formenergy: # test if 0 case ok
if isinstance(n, WorkChain): # TODO: untested for aiida > 1.0
plabel = n.get_attr('_process_label')
if plabel == 'fleur_initial_cls_wc':
try:
ouputnode = n.out.output_initial_cls_wc_para.get_dict()
except AttributeError:
try:
ouputnode = n.out.output_inital_cls_wc_para.get_dict()
except (AttributeError, KeyError, ValueError): # TODO: Check this
ouputnode = None
formenergy = None
print(('WARNING: ouput node of {} not found. I skip'.format(n)))
continue
formenergy = ouputnode.get('formation_energy')
# TODO is this value per atom?
else: # check if corehole wc?
pass
formenergy_dict[compound] = formenergy
for reaction_string in reaction_list:
ent_peratom = get_enhalpy_of_equation(reaction_string, formenergy_dict)
print(ent_peratom)
energy_sorted_reactions.append([reaction_string, ent_peratom])
energy_sorted_reactions = sorted(energy_sorted_reactions, key=lambda ent: ent[1])
return energy_sorted_reactions
# test
# reaction_list = ['1*Be12W->1*Be12W', '2*Be12W->1*Be2W+1*Be22W', '11*Be12W->5*W+6*Be22W', '1*Be12W->12*Be+1*W', '1*Be12W->1*Be2W+10*Be']
# workchain_dict = {'Be12W' : '4f685bc5-b5fb-46d3-aad6-e0f512c3313d',
# 'Be2W' : '045d3071-f442-46b4-8d6b-3c85d72b24d4',
# 'Be22W' : '1e32880a-bdc9-4081-a5da-be04860aa1bc',
# 'W' : 'f8b12b23-0b71-45a1-9040-b51ccf379439',
# 'Be' : 0.0}
# reac_list = determine_favorable_reaction(reaction_list, workchain_dict)
# print reac_list
# {'products': {'Be12W': 1}, 'educts': {'Be12W': 1}}
# 0.0
# {'products': {'Be2W': 1, 'Be22W': 1}, 'educts': {'Be12W': 2}}
# 0.114321037514
# {'products': {'Be22W': 6, 'W': 5}, 'educts': {'Be12W': 11}}
# -0.868053153884
# {'products': {'Be': 12, 'W': 1}, 'educts': {'Be12W': 1}}
# -0.0946046496213
# {'products': {'Be': 10, 'Be2W': 1}, 'educts': {'Be12W': 1}}
# 0.180159355144
# [['11*Be12W->5*W+6*Be22W', -0.8680531538839534], ['1*Be12W->12*Be+1*W', -0.0946046496213127], ['1*Be12W->1*Be12W', 0.0], ['2*Be12W->1*Be2W+1*Be22W', 0.11432103751404535], ['1*Be12W->1*Be2W+10*Be', 0.1801593551436103]]
def get_mpi_proc(resources):
nmachines = resources.get('num_machines', 0)
total_proc = resources.get('tot_num_mpiprocs', 0)
if not total_proc:
if nmachines:
total_proc = nmachines * resources.get('default_mpiprocs_per_machine', 12)
else:
total_proc = resources.get('tot_num_mpiprocs', 24)
return total_proc
def calc_time_cost_function(natom, nkpt, kmax, nspins=1):
costs = natom**3 * kmax**3 * nkpt * nspins
return costs
def calc_time_cost_function_total(natom, nkpt, kmax, niter, nspins=1):
costs = natom**3 * kmax**3 * nkpt * nspins * niter
return costs
def cost_ratio(total_costs, walltime_sec, ncores):
ratio = total_costs / (walltime_sec * ncores)
return ratio
[docs]def optimize_calc_options(nodes,
mpi_per_node,
omp_per_mpi,
use_omp,
mpi_omp_ratio,
fleurinpData=None,
kpts=None,
sacrifice_level=0.9):
"""
Makes a suggestion on parallelisation setup for a particular fleurinpData.
Only the total number of k-points is analysed: the function suggests ideal k-point
parallelisation + OMP parallelisation (if required). Note: the total number of used CPUs
per node will not exceed mpi_per_node * omp_per_mpi.
Sometimes perfect parallelisation is terms of idle CPUs is not what
used wanted because it can harm MPI/OMP ratio. Thus the function first chooses first top
parallelisations in terms of total CPUs used
(bigger than sacrifice_level * maximal_number_CPUs_possible). Then a parallelisation which is
the closest to the MPI/OMP ratio is chosen among them and returned.
:param nodes: maximal number of nodes that can be used
:param mpi_per_node: an input suggestion of MPI tasks per node
:param omp_per_mpi: an input suggestion for OMP tasks per MPI process
:param use_omp: False if OMP parallelisation is not needed
:param mpi_omp_ratio: requested MPI/OMP ratio
:param fleurinpData: FleurinpData to extract total number of kpts from
:param kpts: the total number of kpts
:param sacrifice_level: sets a level of performance sacrifice that a user can afford for better
MPI/OMP ratio.
:returns nodes, MPI_tasks, OMP_per_MPI, message: first three are parallelisation info and
the last one is an exit message.
"""
from sympy.ntheory.factor_ import divisors
import numpy as np
cpus_per_node = mpi_per_node * omp_per_mpi
if fleurinpData:
modes = fleurinpData.get_fleur_modes()
kpts = fleurinpData.attributes['inp_dict']['calculationSetup']['bzIntegration']
if modes['band'] or modes['gw']:
kpts = kpts['altKPointSet']['count']
else:
if 'kPointList' in kpts:
kpts = kpts['kPointList']['count']
else:
kpts = kpts['kPointCount']['count']
kpts = int(kpts)
elif not kpts:
raise ValueError('You must specify either kpts of fleurinpData')
divisors_kpts = divisors(kpts)
possible_nodes = [x for x in divisors_kpts if x <= nodes]
suggestions = []
for n_n in possible_nodes:
advise_cpus = [x for x in divisors(kpts // n_n) if x <= cpus_per_node]
for advised_cpu_per_node in advise_cpus:
suggestions.append((n_n, advised_cpu_per_node))
def add_omp(suggestions):
"""
Also adds possibility of omp parallelisation
"""
final_suggestion = []
for suggestion in suggestions:
if use_omp:
omp = cpus_per_node // suggestion[1]
else:
omp = 1
final_suggestion.append([suggestion[0], suggestion[1], omp])
return final_suggestion
# all possible suggestions taking into account omp
suggestions = np.array(add_omp(suggestions))
best_resources = max(np.prod(suggestions, axis=1))
top_suggestions = suggestions[np.prod(suggestions, axis=1) > sacrifice_level * best_resources]
def best_criterion(suggestion):
'''
also implements hard preference of even numper of MPIs over odd
'''
if use_omp:
return (abs(suggestion[1] % 2 - 1), -abs(suggestion[1] / suggestion[2] - mpi_omp_ratio))
return (suggestion[0] * suggestion[1], abs(suggestion[1] % 2 - 1), -suggestion[0])
best_suggestion = max(top_suggestions, key=best_criterion)
message = ''
if float(best_suggestion[1] * best_suggestion[2]) / cpus_per_node < 0.6:
message = ('WARNING: Changed the number of MPIs per node from {} to {} and OMP per MPI '
'from {} to {}.'
'Changed the number of nodes from {} to {}. '
'Computational setup, needed for a given number k-points ({})'
' provides less then 60% of node load.'
''.format(mpi_per_node, best_suggestion[1], omp_per_mpi, best_suggestion[2], nodes,
best_suggestion[0], kpts))
raise ValueError(message)
elif best_suggestion[1] * best_suggestion[2] == cpus_per_node:
if best_suggestion[0] != nodes:
message = ('WARNING: Changed the number of nodes from {} to {}' ''.format(nodes, best_suggestion[0]))
else:
message = ('Computational setup is perfect! Nodes: {}, MPIs per node {}, OMP per MPI '
'{}. Number of k-points is {}'.format(best_suggestion[0], best_suggestion[1], best_suggestion[2],
kpts))
else:
message = ('WARNING: Changed the number of MPIs per node from {} to {} and OMP from {} to {}'
'. Changed the number of nodes from {} to {}. Number of k-points is {}.'
''.format(mpi_per_node, best_suggestion[1], omp_per_mpi, best_suggestion[2], nodes,
best_suggestion[0], kpts))
return int(best_suggestion[0]), int(best_suggestion[1]), int(best_suggestion[2]), message
[docs]def find_last_submitted_calcjob(restart_wc):
"""
Finds the last CalcJob submitted in a higher-level workchain
and returns it's uuid
"""
from aiida.common.exceptions import NotExistent
from aiida.orm import CalcJobNode
links = restart_wc.get_outgoing().all()
calls = list([x for x in links if isinstance(x.node, CalcJobNode)])
if calls:
calls = sorted(calls, key=lambda x: x.node.pk)
return calls[-1].node.uuid
else:
raise NotExistent
[docs]def find_last_submitted_workchain(restart_wc):
"""
Finds the last CalcJob submitted in a higher-level workchain
and returns it's uuid
"""
from aiida.common.exceptions import NotExistent
from aiida.orm import WorkChainNode
links = restart_wc.get_outgoing().all()
calls = list([x for x in links if isinstance(x.node, WorkChainNode)])
if calls:
calls = sorted(calls, key=lambda x: x.node.pk)
return calls[-1].node.uuid
else:
raise NotExistent