From 05e1a2d845937e6901125fa888a4fbb2d0730f0e Mon Sep 17 00:00:00 2001 From: Daniel Wortmann Date: Tue, 28 May 2024 10:24:19 +0200 Subject: [PATCH] Some changes for CLI --- aiida_fleur/cmdline/__init__.py | 3 + aiida_fleur/cmdline/launch/launch.py | 19 ++++- aiida_fleur/cmdline/util/__init__.py | 28 ++++++++ aiida_fleur/cmdline/util/defaults.py | 71 +++++++++++++++++-- aiida_fleur/cmdline/util/options.py | 8 +-- aiida_fleur/cmdline/util/types.py | 101 ++++++++++++++++++++++++++- 6 files changed, 218 insertions(+), 12 deletions(-) diff --git a/aiida_fleur/cmdline/__init__.py b/aiida_fleur/cmdline/__init__.py index 2f8c32a47..7cfaa34e9 100755 --- a/aiida_fleur/cmdline/__init__.py +++ b/aiida_fleur/cmdline/__init__.py @@ -30,6 +30,7 @@ from .workflows import cmd_workflow from .visualization import cmd_plot from .util import options as options_af +from .util import cmd_defaults # Activate the completion of parameter types provided by the click_completion package # for bash: eval "$(_AIIDA_FLEUR_COMPLETE=source aiida-fleur)" @@ -59,3 +60,5 @@ def cmd_root(profile): # pylint: disable=unused-argument cmd_root.add_command(cmd_data) cmd_root.add_command(cmd_workflow) cmd_root.add_command(cmd_plot) + +cmd_root.add_command(cmd_defaults) diff --git a/aiida_fleur/cmdline/launch/launch.py b/aiida_fleur/cmdline/launch/launch.py index eee8cdce4..d5991169e 100755 --- a/aiida_fleur/cmdline/launch/launch.py +++ b/aiida_fleur/cmdline/launch/launch.py @@ -201,6 +201,8 @@ def launch_scf(structure, inpgen, calc_parameters, fleurinp, fleur, wf_parameter from aiida.orm import load_node wf=load_node(pk) scf_output=wf.outputs.output_scf_wc_para.get_dict() + scf_output["SCF-uuid"]=wf.uuid + #json with dict import json with open("scf.json","w") as file: @@ -319,7 +321,7 @@ def launch_eos(structure, inpgen, calc_parameters, fleur, wf_parameters, scf_par @click.command('banddos') -@options.FLEURINP() +@options.FLEURINP(default='inp.xml') @options.FLEUR() @options.WF_PARAMETERS() @options.REMOTE() @@ -330,6 +332,7 @@ def launch_banddos(fleurinp, fleur, wf_parameters, parent_folder, daemon, settin """ Launch a banddos workchain """ + workchain_class = WorkflowFactory('fleur.banddos') inputs = { 'wf_parameters': wf_parameters, @@ -341,7 +344,19 @@ def launch_banddos(fleurinp, fleur, wf_parameters, parent_folder, daemon, settin inputs = clean_nones(inputs) builder = workchain_class.get_builder() builder.update(inputs) - utils.launch_process(builder, daemon) + pk=utils.launch_process(builder, daemon) + + #Now create output files + from aiida.orm import load_node + wf=load_node(pk) + banddos_output=wf.outputs.output_banddos_wc_para.get_dict() + #json with dict + import json + with open("banddos.json","w") as file: + json.dump(banddos_output,file,indent=2) + #plot + from aiida_fleur.tools.plot.fleur import plot_fleur + plot_fleur(wf,save=True,show=False) @click.command('init_cls') diff --git a/aiida_fleur/cmdline/util/__init__.py b/aiida_fleur/cmdline/util/__init__.py index 71ba092a1..dce99968e 100755 --- a/aiida_fleur/cmdline/util/__init__.py +++ b/aiida_fleur/cmdline/util/__init__.py @@ -11,3 +11,31 @@ ''' AiiDA-FLEUR ''' +import click +import json + +from .defaults import get_code_interactive,get_default_dict +from aiida.cmdline.utils import decorators + + +# general further commands for fleur workchains +@click.command('config') +@decorators.with_dbenv() +def cmd_defaults(): + """Interactively create/modify the default settings for aiida-fleur CLI.""" + + dict=get_default_dict() + + #default codes + dict["fleur"]=get_code_interactive("fleur.fleur",dict["fleur"]) + dict["inpgen"]=get_code_interactive("fleur.inpgen",dict["inpgen"]) + + try: + os.mkdir(f"{HOME}/.aiida-fleur") + except: + pass #dir might exist already + with open(f"{HOME}/.aiida-fleur/cli.json","w") as f: + json.dump(dict,f) + + + diff --git a/aiida_fleur/cmdline/util/defaults.py b/aiida_fleur/cmdline/util/defaults.py index de4fe4c38..fac0a2303 100644 --- a/aiida_fleur/cmdline/util/defaults.py +++ b/aiida_fleur/cmdline/util/defaults.py @@ -2,9 +2,24 @@ Here we specify some defaults for cli commands """ -# Structures +def get_default_dict(): + import os + HOME=os.getenv("HOME") + #first see if we have already a setting file + try: + with open(f"{HOME}/.aiida-fleur/cli.json","r") as f: + dict=json.load(f) + except: + dict={"fleur":None, + "inpgen":None, + "copyback":False, + "resources":None + } + return dict + +# Structures def get_si_bulk_structure(): """Return a `StructureData` representing bulk silicon. @@ -84,15 +99,20 @@ def get_fept_film_structure(): # Codes def get_inpgen(): """Return a `Code` node of the latest added inpgen executable in the database.""" - - return get_last_code('fleur.inpgen') + try: + return get_default_dict()["inpgen"] + except: + return get_last_code('fleur.inpgen') def get_fleur(): """Return a `Code` node of the latest added inpgen executable in the database.""" + try: + return get_default_dict()["fleur"] + except: + return get_last_code('fleur.fleur') - return get_last_code('fleur.fleur') - + def get_last_code(entry_point_name): """Return a `Code` node of the latest code executable of the given entry_point_name in the database. @@ -117,3 +137,44 @@ def get_last_code(entry_point_name): if not results: raise NotExistent(f'ERROR: Could not find any Code in the database with entry point: {entry_point_name}!') return results[0].uuid + + +def get_code_interactive(entry_point_name,default_uuid=None): + """Return a `Code` node of the given entry_point_name in the database. + + The database will be queried for the existence the possible codes, they will be listed and + one can be choosen. + If this is not exists and NotExistent error is raised. + + + :param entry_point_name: string + :return: the uuid of a inpgen `Code` node + :raise: aiida.common.exceptions.NotExistent + """ + import click + from aiida.orm import QueryBuilder, Code + from aiida.common.exceptions import NotExistent + + filters = {'attributes.input_plugin': {'==': entry_point_name}} + + builder = QueryBuilder().append(Code, filters=filters) + + if not builder.all(): + raise NotExistent(f'ERROR: Could not find any Code in the database with entry point: {entry_point_name}!') + + print(f"Selection for {entry_point_name}:") + i=0 + default_i=0 + for code in builder.all(): + if code[0].uuid==default_uuid: + default_i=i + print(f"{i}:{code[0].full_label}") + i=i+1 + i=click.prompt("Please enter your choice",type=int,default=default_i) + try: + result=builder.all()[i] + except: + return default_uuid + + + return result[0].uuid \ No newline at end of file diff --git a/aiida_fleur/cmdline/util/options.py b/aiida_fleur/cmdline/util/options.py index 0f808f040..9aeab4fed 100755 --- a/aiida_fleur/cmdline/util/options.py +++ b/aiida_fleur/cmdline/util/options.py @@ -17,7 +17,7 @@ from aiida.cmdline.params import types from aiida.cmdline.params.options import OverridableOption from .defaults import get_inpgen, get_fleur, get_si_bulk_structure -from .types import StructureNodeOrFileParamType +from .types import StructureNodeOrFileParamType,WFParameterType,RemoteType,FleurinpType STRUCTURE_OR_FILE = OverridableOption( '-s', @@ -55,7 +55,7 @@ FLEURINP = OverridableOption('-inp', '--fleurinp', - type=types.DataParamType(sub_classes=('aiida.data:fleur.fleurinp',)), + type=FleurinpType(), help='FleurinpData node for the fleur calculation.') CALC_PARAMETERS = OverridableOption( @@ -71,7 +71,7 @@ WF_PARAMETERS = OverridableOption('-wf', '--wf-parameters', - type=types.DataParamType(sub_classes=('aiida.data:core.dict',)), + type=WFParameterType(), help='Dict containing parameters given to the workchain.') SCF_PARAMETERS = OverridableOption('-scf', @@ -132,7 +132,7 @@ REMOTE = OverridableOption('-P', '--parent-folder', 'parent_folder', - type=types.DataParamType(sub_classes=('aiida.data:core.remote',)), + type=RemoteType(), show_default=True, required=False, help='The PK of a parent remote folder (for restarts).') diff --git a/aiida_fleur/cmdline/util/types.py b/aiida_fleur/cmdline/util/types.py index c88e9cb04..8b5e83cd0 100644 --- a/aiida_fleur/cmdline/util/types.py +++ b/aiida_fleur/cmdline/util/types.py @@ -18,7 +18,106 @@ from aiida.plugins import DataFactory from aiida.cmdline.utils.decorators import with_dbenv +wf_template_files={"eos":'{"points": 9,\n' + '"step": 0.002,\n' + '"guess": 1.00}', + "scf":'{"fleur_runmax": 4,\n' + '"density_converged": 0.00002,\n' + '"energy_converged": 0.002,\n' + '"mode": "density",\n' + '"itmax_per_run": 30}', + "banddos":'{"mode": "band",\n' + '"kpath": "auto",\n' + '"klistname": "path-3",\n' + '"kpoints_number": None,\n' + '"kpoints_distance": None,\n' + '"kpoints_explicit": None,\n' + '"sigma": 0.005,\n' + '"emin": -0.50,\n' + '"emax": 0.90\n}' + } + +class FleurinpType(click.ParamType): + """ + Type to either load a fleurinp node or read an inp.xml file + """ + name = "FleurInp data/inp.xml file" + def convert(self,value,param,ctx): + try: + return types.DataParamType(sub_classes=('aiida.data:fleur.fleurinp',)).convert(value, param, ctx) + except: + pass #Ok this failed, so we try to read the file + + if value in ["inp.xml",".",'./']: + from aiida_fleur.data.fleurinp import FleurinpData + inp_files=["inp.xml"] + #check if there are included files in this dir + for file in ["kpts.xml","sym.xml","relax.xml"]: + import os.path + if os.path.isfile(file): + inp_files.append(file) + finp = FleurinpData(files=inp_files) + return finp.store() + return None + +class RemoteType(click.ParamType): + """ + Type for remote data. Might be specified by a uuid or a file in which this uuid is found + """ + name = "Remote folder" + def convert(self,value,param,ctx): + #Try to interpret value as uuid + try: + return types.DataParamType(sub_classes=('aiida.data:core.remote',)).convert(value, param, ctx) + except: + pass #Ok this failed, so we try to read the file + + try: + from aiida.orm import load_node + with open(value,"r") as f: + import json + dict_from_file=json.load(f) + + scf_wf=load_node(dict_from_file["SCF-uuid"]) + print(scf_wf) + return scf_wf.outputs.last_calc.remote_folder + except: + return None + +class WFParameterType(click.ParamType): + """ + ParamType for giving workflow parameters + """ + name = "Workflow parameters" + def convert(self,value,param,ctx): + import os + + if value=="template.json": + if ctx.command.name in wf_template_files: + with open(f"wf_{ctx.command.name}.json","w") as f: + f.write(wf_template_files[ctx.command.name]) + quit() + + if (os.path.isfile(value)): + # a file was given. Create a dict from the file and use it + try: + with open(value,"r") as f: + import json + wf_param=json.load(f) + except: + print(f"{value} could not be converted into a dict") + os.abort() + aiida_dict=DataFactory("dict") + wf_dict=aiida_dict(wf_param) + + return wf_dict.store() + + #Now load from aiida + wf_dict = types.DataParamType(sub_classes=('aiida.data:core.dict',)).convert(value, param, ctx) + + return wf_dict + class StructureNodeOrFileParamType(click.ParamType): """ The ParamType for identifying a structure by node or to extract it from a given file @@ -51,7 +150,7 @@ def convert(self, value, param, ctx): try: structure = types.DataParamType(sub_classes=('aiida.data:core.structure',)).convert(value, param, ctx) except (NotExistent, click.exceptions.BadParameter) as er: - echo.echo(f'Tried to load node, could not fine one for {value}. ' + echo.echo(f'Tried to load node, could not find one for {value}. ' 'I will further check if it is a filepath.') is_path = True