From a3e1f82448f9c241e59e8dae7ced91dca9ccd927 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Fri, 7 Feb 2025 23:02:00 +0100 Subject: [PATCH 1/7] [XPS] support multiple core levels for a element --- docs/source/howto/xps_workchain.ipynb | 277 +++++++---------- .../functions/xspectra/get_xps_spectra.py | 151 +++++---- src/aiida_qe_xspec/gui/xps/model.py | 16 +- src/aiida_qe_xspec/gui/xps/setting.py | 36 ++- src/aiida_qe_xspec/gui/xps/workchain.py | 30 +- src/aiida_qe_xspec/utils.py | 19 ++ .../functions/get_marked_structures.py | 6 +- src/aiida_qe_xspec/workflows/xps.py | 294 +++++++----------- 8 files changed, 363 insertions(+), 466 deletions(-) create mode 100644 src/aiida_qe_xspec/utils.py diff --git a/docs/source/howto/xps_workchain.ipynb b/docs/source/howto/xps_workchain.ipynb index 9638636..33b3691 100644 --- a/docs/source/howto/xps_workchain.ipynb +++ b/docs/source/howto/xps_workchain.ipynb @@ -27,38 +27,45 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/xing/apps/miniforge3/envs/aiida/lib/python3.11/site-packages/spglib/spglib.py:115: DeprecationWarning: dict interface (SpglibDataset['number']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead\n", - " warnings.warn(\n", - "/home/xing/apps/miniforge3/envs/aiida/lib/python3.11/site-packages/spglib/spglib.py:115: DeprecationWarning: dict interface (SpglibDataset['equivalent_atoms']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead\n", - " warnings.warn(\n", - "/home/xing/apps/miniforge3/envs/aiida/lib/python3.11/site-packages/spglib/spglib.py:115: DeprecationWarning: dict interface (SpglibDataset['std_types']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead\n", - " warnings.warn(\n", - "/home/xing/apps/miniforge3/envs/aiida/lib/python3.11/site-packages/spglib/spglib.py:115: DeprecationWarning: dict interface (SpglibDataset['international']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead\n", - " warnings.warn(\n", - "02/01/2025 02:07:49 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5658|XpsWorkChain|prepare_structures]: structures_to_process: {'site_0': }\n", - "02/01/2025 02:07:49 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5658|XpsWorkChain|run_gs_scf]: launched PwBaseWorkChain for supercell<5665>\n", - "02/01/2025 02:07:49 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5658|XpsWorkChain|run_all_scf]: launched PwBaseWorkChain for site_0<5667>\n", - "02/01/2025 02:07:50 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|run_process]: launching PwCalculation<5674> iteration #1\n", - "02/01/2025 02:07:50 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5665|PwBaseWorkChain|run_process]: launching PwCalculation<5677> iteration #1\n", - "02/01/2025 02:08:08 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5665|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", - "02/01/2025 02:08:08 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5665|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", - "02/01/2025 02:08:27 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|sanity_check_insufficient_bands]: PwCalculation<5674> run with smearing and highest band is occupied\n", - "02/01/2025 02:08:27 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|sanity_check_insufficient_bands]: BandsData<5685> has invalid occupations: Occupation of 1.000932459633147 at last band lkn<0,0,21>\n", - "02/01/2025 02:08:27 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|sanity_check_insufficient_bands]: PwCalculation<5674> had insufficient bands\n", - "02/01/2025 02:08:27 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|sanity_check_insufficient_bands]: Action taken: increased number of bands to 25 and restarting from the previous charge density.\n", - "02/01/2025 02:08:27 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|inspect_process]: PwCalculation<5674> finished successfully but a handler was triggered, restarting\n", - "02/01/2025 02:08:27 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|run_process]: launching PwCalculation<5690> iteration #2\n", - "02/01/2025 02:08:47 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|results]: work chain completed after 2 iterations\n", - "02/01/2025 02:08:47 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5667|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", - "02/01/2025 02:08:48 AM <2611687> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [5658|XpsWorkChain|on_terminated]: remote folders will not be cleaned\n" + "02/07/2025 10:49:08 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23660|XpsWorkChain|prepare_structures]: structures_to_process: {'site_0': }\n", + "02/07/2025 10:49:08 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23660|XpsWorkChain|run_gs_scf]: launched PwBaseWorkChain for supercell<23667>\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ + "site site_0\n", + "abs_element Si\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "02/07/2025 10:49:09 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23660|XpsWorkChain|run_all_scf]: launched PwBaseWorkChain for Si_site_0_2p<23669>\n", + "02/07/2025 10:49:10 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|run_process]: launching PwCalculation<23676> iteration #1\n", + "02/07/2025 10:49:11 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23667|PwBaseWorkChain|run_process]: launching PwCalculation<23684> iteration #1\n", + "02/07/2025 10:49:12 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|sanity_check_insufficient_bands]: PwCalculation<23676> run with smearing and highest band is occupied\n", + "02/07/2025 10:49:12 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|sanity_check_insufficient_bands]: BandsData<23679> has invalid occupations: Occupation of 1.000932470972213 at last band lkn<0,0,21>\n", + "02/07/2025 10:49:12 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|sanity_check_insufficient_bands]: PwCalculation<23676> had insufficient bands\n", + "02/07/2025 10:49:12 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|sanity_check_insufficient_bands]: Action taken: increased number of bands to 25 and restarting from the previous charge density.\n", + "02/07/2025 10:49:12 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|inspect_process]: PwCalculation<23676> finished successfully but a handler was triggered, restarting\n", + "02/07/2025 10:49:13 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|run_process]: launching PwCalculation<23692> iteration #2\n", + "02/07/2025 10:49:13 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23667|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", + "02/07/2025 10:49:13 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23667|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", + "02/07/2025 10:49:14 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|results]: work chain completed after 2 iterations\n", + "02/07/2025 10:49:14 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23669|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", + "02/07/2025 10:49:15 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23660|XpsWorkChain|on_terminated]: remote folders will not be cleaned\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "result {'chemical_shifts': , 'chemical_shift_spectra': {'Si_2p': }, 'binding_energy_spectra': {'Si_2p': }, 'binding_energies': }\n", "Binding energy of Si 2p core level is\n", - "{'site_0': 99.8438337976}\n" + "{'Si': {'2p': {'site_0': {'energy': 99.8438337976, 'multiplicity': 8}}}}\n" ] } ], @@ -67,44 +74,16 @@ "from ase.build import bulk\n", "from aiida.engine import run_get_node\n", "from aiida_qe_xspec.workflows.xps import XpsWorkChain\n", + "from aiida_qe_xspec.utils import load_core_hole_pseudos\n", "import numpy as np\n", "from aiida import orm\n", "\n", "\n", - "def load_core_hole_pseudos(core_level_list, pseudo_group=\"pseudo_demo_pbe\"):\n", - " \"\"\"Load the core hole pseudos.\"\"\"\n", - " pseudo_group = orm.QueryBuilder().append(orm.Group, filters={\"label\": pseudo_group}).one()[0]\n", - " all_correction_energies = pseudo_group.base.extras.get(\"correction\", {})\n", - " pseudos = {}\n", - " elements_list = []\n", - " correction_energies = {}\n", - " for label in core_level_list:\n", - " element = label.split('_')[0]\n", - " pseudos[element] = {\n", - " 'core_hole': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == label),\n", - " 'gipaw': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == f'{element}_gs'),\n", - " }\n", - " correction_energies[element] = all_correction_energies[label]['core'] - all_correction_energies[label]['exp']\n", - " elements_list.append(element)\n", - " return pseudos, correction_energies\n", - "\n", - "\n", "load_profile()\n", "\n", "atoms = bulk(\"Si\")\n", "structure = orm.StructureData(ase=atoms)\n", "code = orm.load_code(\"qe-7.2-pw@localhost\")\n", - "parameters = {\n", - " \"CONTROL\": {\n", - " \"calculation\": \"scf\",\n", - " },\n", - " \"SYSTEM\": {\n", - " \"ecutwfc\": 30,\n", - " \"ecutrho\": 200,\n", - " \"occupations\": \"smearing\",\n", - " \"smearing\": \"gaussian\",\n", - " },\n", - "}\n", "# Load the pseudopotential family.\n", "kpoints = orm.KpointsData()\n", "kpoints.set_kpoints_mesh([5, 5, 5])\n", @@ -120,18 +99,17 @@ " 'is_molecule_input': orm.Bool(False),\n", "}\n", "# Load the pseudopotential family.\n", - "core_level_list = [\"Si_2p\"]\n", - "core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_level_list, \"pseudo_demo_pbe\")\n", + "core_levels = {\"Si\": [\"2p\"]}\n", + "core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_levels, \"pseudo_demo_pbe\")\n", "core_hole_treatments={\"Si\": \"xch_smear\"}\n", "builder = XpsWorkChain.get_builder_from_protocol(\n", " structure=structure,\n", " code=code,\n", " protocol=\"fast\",\n", " overrides = {\"ch_scf\": {\"pseudo_family\": \"SSSP/1.2/PBE/efficiency\"}},\n", - " pseudos=core_hole_pseudos,\n", - " elements_list=[\"Si\"],\n", + " core_hole_pseudos=core_hole_pseudos,\n", + " core_levels=core_levels,\n", " calc_binding_energy=orm.Bool(True),\n", - " parameters=orm.Dict(dict=parameters),\n", " correction_energies=orm.Dict(correction_energies),\n", " core_hole_treatments=core_hole_treatments,\n", " structure_preparation_settings=structure_preparation_settings,\n", @@ -142,7 +120,7 @@ "_, node = run_get_node(builder)\n", "\n", "print(\"Binding energy of Si 2p core level is\")\n", - "print(node.outputs.binding_energies.Si_be.get_dict())\n", + "print(node.outputs.binding_energies.get_dict())\n", "\n" ] }, @@ -156,31 +134,46 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "02/03/2025 06:18:56 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7339|XpsWorkChain|prepare_structures]: structures_to_process: {'site_0': }\n", - "02/03/2025 06:18:56 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7339|XpsWorkChain|run_gs_scf]: launched PwBaseWorkChain for supercell<7345>\n", - "02/03/2025 06:18:57 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7339|XpsWorkChain|run_all_scf]: launched PwBaseWorkChain for site_0<7347>\n", - "02/03/2025 06:18:58 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7345|PwBaseWorkChain|run_process]: launching PwCalculation<7350> iteration #1\n", - "02/03/2025 06:18:59 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7347|PwBaseWorkChain|run_process]: launching PwCalculation<7353> iteration #1\n", - "02/03/2025 06:19:11 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7345|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", - "02/03/2025 06:19:11 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7345|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", - "02/03/2025 06:19:12 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7347|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", - "02/03/2025 06:19:12 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7347|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", - "02/03/2025 06:19:13 PM <445142> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [7339|XpsWorkChain|on_terminated]: remote folders will not be cleaned\n" + "02/07/2025 10:49:16 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23723|XpsWorkChain|prepare_structures]: structures_to_process: {'site_0': }\n", + "02/07/2025 10:49:17 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23723|XpsWorkChain|run_gs_scf]: launched PwBaseWorkChain for supercell<23729>\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ + "site site_0\n", + "abs_element O\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "02/07/2025 10:49:17 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23723|XpsWorkChain|run_all_scf]: launched PwBaseWorkChain for O_site_0_1s<23731>\n", + "02/07/2025 10:49:18 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23729|PwBaseWorkChain|run_process]: launching PwCalculation<23734> iteration #1\n", + "02/07/2025 10:49:19 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23731|PwBaseWorkChain|run_process]: launching PwCalculation<23742> iteration #1\n", + "02/07/2025 10:49:20 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23729|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", + "02/07/2025 10:49:20 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23729|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", + "02/07/2025 10:49:20 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23731|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", + "02/07/2025 10:49:20 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23731|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", + "02/07/2025 10:49:21 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23723|XpsWorkChain|on_terminated]: remote folders will not be cleaned\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "result {'chemical_shifts': , 'chemical_shift_spectra': {'O_1s': }, 'binding_energy_spectra': {'O_1s': }, 'binding_energies': }\n", "Binding energy of O 1s core level is\n", - "{'site_0': 538.99493145453}\n" + "{'O': {'1s': {'site_0': {'energy': 538.99493145453, 'multiplicity': 1}}}}\n" ] } ], @@ -189,28 +182,10 @@ "from ase.build import molecule\n", "from aiida.engine import run_get_node\n", "from aiida_qe_xspec.workflows.xps import XpsWorkChain\n", - "import numpy as np\n", + "from aiida_qe_xspec.utils import load_core_hole_pseudos\n", "from aiida import orm\n", "\n", "\n", - "def load_core_hole_pseudos(core_level_list, pseudo_group=\"pseudo_demo_pbe\"):\n", - " \"\"\"Load the core hole pseudos.\"\"\"\n", - " pseudo_group = orm.QueryBuilder().append(orm.Group, filters={\"label\": pseudo_group}).one()[0]\n", - " all_correction_energies = pseudo_group.base.extras.get(\"correction\", {})\n", - " pseudos = {}\n", - " elements_list = []\n", - " correction_energies = {}\n", - " for label in core_level_list:\n", - " element = label.split('_')[0]\n", - " pseudos[element] = {\n", - " 'core_hole': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == label),\n", - " 'gipaw': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == f'{element}_gs'),\n", - " }\n", - " correction_energies[element] = all_correction_energies[label]['core'] - all_correction_energies[label]['exp']\n", - " elements_list.append(element)\n", - " return pseudos, correction_energies\n", - "\n", - "\n", "load_profile()\n", "\n", "atoms = molecule('H2O')\n", @@ -218,17 +193,6 @@ "atoms.pbc = True\n", "structure = orm.StructureData(ase=atoms)\n", "code = orm.load_code(\"qe-7.2-pw@localhost\")\n", - "parameters = {\n", - " \"CONTROL\": {\n", - " \"calculation\": \"scf\",\n", - " },\n", - " \"SYSTEM\": {\n", - " \"ecutwfc\": 30,\n", - " \"ecutrho\": 200,\n", - " \"occupations\": \"smearing\",\n", - " \"smearing\": \"gaussian\",\n", - " },\n", - "}\n", "# Load the pseudopotential family.\n", "kpoints = orm.KpointsData()\n", "kpoints.set_kpoints_mesh([1, 1, 1])\n", @@ -244,18 +208,17 @@ " 'is_molecule_input': orm.Bool(True),\n", "}\n", "# Load the pseudopotential family.\n", - "core_level_list = [\"O_1s\"]\n", - "core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_level_list, \"pseudo_demo_pbe\")\n", + "core_levels = {\"O\": [\"1s\"]}\n", + "core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_levels, \"pseudo_demo_pbe\")\n", "core_hole_treatments={\"O\": \"full\"}\n", "builder = XpsWorkChain.get_builder_from_protocol(\n", " structure=structure,\n", " code=code,\n", " protocol=\"fast\",\n", " overrides = {\"ch_scf\": {\"pseudo_family\": \"SSSP/1.2/PBE/efficiency\"}},\n", - " pseudos=core_hole_pseudos,\n", - " elements_list=[\"O\"],\n", + " core_hole_pseudos=core_hole_pseudos,\n", + " core_levels=core_levels,\n", " calc_binding_energy=orm.Bool(True),\n", - " parameters=orm.Dict(dict=parameters),\n", " correction_energies=orm.Dict(correction_energies),\n", " core_hole_treatments=core_hole_treatments,\n", " structure_preparation_settings=structure_preparation_settings,\n", @@ -266,7 +229,7 @@ "_, node = run_get_node(builder)\n", "\n", "print(\"Binding energy of O 1s core level is\")\n", - "print(node.outputs.binding_energies.O_be.get_dict())\n", + "print(node.outputs.binding_energies.get_dict())\n", "\n" ] }, @@ -277,28 +240,55 @@ "## Run calculation for selected atoms\n", "The previous examples calculated the XPS spectra for selected elements by analyzing the symmetry and finding all non-equivalent sites. This is not suitable for large systems with low symmetry, e.g. supported nanoparticles, in which all atoms are non-equivalent and the user is usually only interested in the spectra of some special atoms.\n", "\n", - "You can use the `atoms_list` input to specify the atoms you are interested in." + "You can use the `atom_indices` input to specify the atoms you are interested in." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "02/07/2025 08:55:54 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22185|XpsWorkChain|prepare_structures]: structures_to_process: {'site_1': }\n", - "02/07/2025 08:55:54 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22185|XpsWorkChain|run_gs_scf]: launched PwBaseWorkChain for supercell<22190>\n", - "02/07/2025 08:55:55 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22185|XpsWorkChain|run_all_scf]: launched PwBaseWorkChain for site_1<22192>\n", - "02/07/2025 08:55:55 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22190|PwBaseWorkChain|run_process]: launching PwCalculation<22195> iteration #1\n", - "02/07/2025 08:55:56 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22192|PwBaseWorkChain|run_process]: launching PwCalculation<22203> iteration #1\n", - "02/07/2025 08:55:57 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22190|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", - "02/07/2025 08:55:57 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22190|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", - "02/07/2025 08:55:57 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22192|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", - "02/07/2025 08:55:57 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22192|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", - "02/07/2025 08:55:58 AM <4123212> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [22185|XpsWorkChain|on_terminated]: remote folders will not be cleaned\n" + "02/07/2025 10:55:41 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23946|XpsWorkChain|prepare_structures]: structures_to_process: {'site_1': }\n", + "02/07/2025 10:55:42 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23946|XpsWorkChain|run_gs_scf]: launched PwBaseWorkChain for supercell<23951>\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "site site_1\n", + "abs_element C\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "02/07/2025 10:55:42 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23946|XpsWorkChain|run_all_scf]: launched PwBaseWorkChain for C_site_1_1s<23953>\n", + "02/07/2025 10:55:43 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23951|PwBaseWorkChain|run_process]: launching PwCalculation<23956> iteration #1\n", + "02/07/2025 10:55:44 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23953|PwBaseWorkChain|run_process]: launching PwCalculation<23964> iteration #1\n", + "02/07/2025 10:55:45 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23951|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", + "02/07/2025 10:55:45 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23951|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n", + "02/07/2025 10:55:45 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23953|PwBaseWorkChain|results]: work chain completed after 1 iterations\n", + "02/07/2025 10:55:45 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23953|PwBaseWorkChain|on_terminated]: remote folders will not be cleaned\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "result {'chemical_shifts': , 'chemical_shift_spectra': {'C_1s': }, 'binding_energy_spectra': {'C_1s': }, 'binding_energies': }\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "02/07/2025 10:55:46 PM <606530> aiida.orm.nodes.process.workflow.workchain.WorkChainNode: [REPORT] [23946|XpsWorkChain|on_terminated]: remote folders will not be cleaned\n" ] }, { @@ -306,7 +296,7 @@ "output_type": "stream", "text": [ "Binding energy of C 1s core level is\n", - "{'site_1': 293.8658379553}\n" + "{'C': {'1s': {'site_1': {'energy': 293.8658379553, 'multiplicity': 1}}}}\n" ] } ], @@ -315,28 +305,8 @@ "from ase.build import molecule\n", "from aiida.engine import run_get_node\n", "from aiida_qe_xspec.workflows.xps import XpsWorkChain\n", - "import numpy as np\n", + "from aiida_qe_xspec.utils import load_core_hole_pseudos\n", "from aiida import orm\n", - "from ase.visualize import view\n", - "\n", - "\n", - "def load_core_hole_pseudos(core_level_list, pseudo_group=\"pseudo_demo_pbe\"):\n", - " \"\"\"Load the core hole pseudos.\"\"\"\n", - " pseudo_group = orm.QueryBuilder().append(orm.Group, filters={\"label\": pseudo_group}).one()[0]\n", - " all_correction_energies = pseudo_group.base.extras.get(\"correction\", {})\n", - " pseudos = {}\n", - " elements_list = []\n", - " correction_energies = {}\n", - " for label in core_level_list:\n", - " element = label.split('_')[0]\n", - " pseudos[element] = {\n", - " 'core_hole': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == label),\n", - " 'gipaw': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == f'{element}_gs'),\n", - " }\n", - " correction_energies[element] = all_correction_energies[label]['core'] - all_correction_energies[label]['exp']\n", - " elements_list.append(element)\n", - " return pseudos, correction_energies\n", - "\n", "\n", "load_profile()\n", "\n", @@ -345,17 +315,6 @@ "atoms.pbc = True\n", "structure = orm.StructureData(ase=atoms)\n", "code = orm.load_code(\"qe-7.2-pw@localhost\")\n", - "parameters = {\n", - " \"CONTROL\": {\n", - " \"calculation\": \"scf\",\n", - " },\n", - " \"SYSTEM\": {\n", - " \"ecutwfc\": 30,\n", - " \"ecutrho\": 200,\n", - " \"occupations\": \"smearing\",\n", - " \"smearing\": \"gaussian\",\n", - " },\n", - "}\n", "# Load the pseudopotential family.\n", "kpoints = orm.KpointsData()\n", "kpoints.set_kpoints_mesh([1, 1, 1])\n", @@ -371,18 +330,18 @@ " 'is_molecule_input': orm.Bool(True),\n", "}\n", "# Load the pseudopotential family.\n", - "core_level_list = [\"C_1s\"]\n", - "core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_level_list, \"pseudo_demo_pbe\")\n", + "core_levels = {\"C\": [\"1s\"]}\n", + "core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_levels, \"pseudo_demo_pbe\")\n", "core_hole_treatments={\"C\": \"full\"}\n", "builder = XpsWorkChain.get_builder_from_protocol(\n", " structure=structure,\n", " code=code,\n", " protocol=\"fast\",\n", " overrides = {\"ch_scf\": {\"pseudo_family\": \"SSSP/1.2/PBE/efficiency\"}},\n", - " pseudos=core_hole_pseudos,\n", - " atoms_list=[1],\n", + " core_hole_pseudos=core_hole_pseudos,\n", + " core_levels=core_levels,\n", + " atom_indices=[1],\n", " calc_binding_energy=orm.Bool(True),\n", - " parameters=orm.Dict(dict=parameters),\n", " correction_energies=orm.Dict(correction_energies),\n", " core_hole_treatments=core_hole_treatments,\n", " structure_preparation_settings=structure_preparation_settings,\n", @@ -393,7 +352,7 @@ "_, node = run_get_node(builder)\n", "\n", "print(\"Binding energy of C 1s core level is\")\n", - "print(node.outputs.binding_energies.C_be.get_dict())\n", + "print(node.outputs.binding_energies.get_dict())\n", "\n" ] } diff --git a/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py b/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py index c983dca..7219ea0 100644 --- a/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py +++ b/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py @@ -3,109 +3,100 @@ from aiida.engine import calcfunction import numpy as np - -@calcfunction -def get_spectra_by_element(elements_list, equivalent_sites_data, voight_gamma, voight_sigma, **kwargs): # pylint: disable=too-many-statements - """Generate the XPS spectra for each element. - - Calculate the core level shift and binding energy for each element. - Generate the final spectra using the Voigt profile. - - :param elements_list: a List object defining the list of elements to consider - when producing spectrum. - :param equivalent_sites_data: an Dict object containing symmetry data. - :param voight_gamma: a Float node for the gamma parameter of the voigt profile. - :param voight_sigma: a Float node for the sigma parameter of the voigt profile. - :param structure: the StructureData object to be analysed - :returns: Dict objects for all generated spectra and associated binding energy - and core level shift. - - """ +def spectra_broadening(points, sigma=0.1, gamma=0.1): + """Broadening base on the binding energy.""" from scipy.special import voigt_profile # pylint: disable=no-name-in-module - ground_state_node = kwargs.pop('ground_state', None) - correction_energies = kwargs.pop('correction_energies', orm.Dict()).get_dict() - incoming_param_nodes = {key: value for key, value in kwargs.items() if key != 'metadata'} - group_state_energy = None - if ground_state_node is not None: - group_state_energy = ground_state_node.get_dict()['energy'] - elements = elements_list.get_list() - sigma = voight_sigma.value - gamma = voight_gamma.value - equivalency_data = equivalent_sites_data.get_dict() - - data_dict = {element: {} for element in elements} - for key in incoming_param_nodes: - xspectra_out_params = incoming_param_nodes[key].get_dict() - multiplicity = equivalency_data[key]['multiplicity'] - element = equivalency_data[key]['symbol'] - total_energy = xspectra_out_params['energy'] - data_dict[element][key] = {'element': element, 'multiplicity': multiplicity, 'total_energy': total_energy} - - result = {} - core_level_shifts = {} - binding_energies = {} - for element in elements: - spectra_list = [] - for key in data_dict[element]: - site_multiplicity = data_dict[element][key]['multiplicity'] - spectra_list.append((site_multiplicity, float(data_dict[element][key]['total_energy']), key)) - spectra_list.sort(key=lambda entry: entry[1]) - lowest_total_energy = spectra_list[0][1] - core_level_shift = [(entry[0], entry[1] - lowest_total_energy, entry[2]) for entry in spectra_list] - core_level_shifts[element] = core_level_shift - result[f'{element}_cls'] = orm.Dict(dict={entry[2]: entry[1] for entry in core_level_shift}) - - if group_state_energy is not None: - binding_energy = [(entry[0], entry[1] - group_state_energy + correction_energies[element], entry[2]) - for entry in spectra_list] - binding_energies[element] = binding_energy - result[f'{element}_be'] = orm.Dict(dict={entry[2]: entry[1] for entry in binding_energy}) - fwhm_voight = gamma / 2 + np.sqrt(gamma**2 / 4 + sigma**2) - def spectra_broadening(points, label='cls_spectra'): - """Broadening base on the binding energy.""" - result_spectra = {} - for element in elements: + result_spectra = {} + for element, orbitals in points.items(): + for orbital, data in orbitals.items(): final_spectra_y_arrays = [] final_spectra_y_labels = [] final_spectra_y_units = [] - - total_multiplicity = sum(i[0] for i in points[element]) - + total_multiplicity = sum(d['multiplicity'] for d in data.values()) final_spectra = orm.XyData() - max_core_level_shift = points[element][-1][1] - min_core_level_shift = points[element][0][1] + max_core_level_shift = max([d['energy'] for d in data.values()]) + min_core_level_shift = min([d['energy'] for d in data.values()]) # Energy range for the Broadening function x_energy_range = np.linspace( min_core_level_shift - fwhm_voight - 1.5, max_core_level_shift + fwhm_voight + 1.5, 500 ) - - for atoms, index in zip(points[element], range(len(points[element]))): + for site, d in data.items(): # Weight for the spectra of every atom - intensity = atoms[0] - relative_peak_position = atoms[1] - final_spectra_y_labels.append(f'{element}{index}_xps') + intensity = d['multiplicity'] / total_multiplicity + relative_peak_position = d['energy'] + final_spectra_y_labels.append(f'{element}_{site}') final_spectra_y_units.append('sigma') final_spectra_y_arrays.append( - intensity * voigt_profile(x_energy_range - relative_peak_position, sigma, gamma) / - total_multiplicity + intensity* voigt_profile(x_energy_range - relative_peak_position, sigma, gamma) ) - - final_spectra_y_labels.append(f'{element}_total_xps') + final_spectra_y_labels.append(f'{element}_total') final_spectra_y_units.append('sigma') final_spectra_y_arrays.append(sum(final_spectra_y_arrays)) - final_spectra_x_label = 'energy' final_spectra_x_units = 'eV' final_spectra_x_array = x_energy_range final_spectra.set_x(final_spectra_x_array, final_spectra_x_label, final_spectra_x_units) final_spectra.set_y(final_spectra_y_arrays, final_spectra_y_labels, final_spectra_y_units) - result_spectra[f'{element}_{label}'] = final_spectra - return result_spectra + result_spectra[f'{element}_{orbital}'] = final_spectra + return result_spectra + +@calcfunction +def get_spectra_by_element(core_levels, equivalent_sites_data, voight_gamma, voight_sigma, **kwargs): # pylint: disable=too-many-statements + """Generate the XPS spectra for each element. + + Calculate the core level shift and binding energy for each element. + Generate the final spectra using the Voigt profile. + + :param core_levels: a Dict object defining the elements and their core-levels to consider + when producing spectrum. + :param equivalent_sites_data: an Dict object containing symmetry data. + :param voight_gamma: a Float node for the gamma parameter of the voigt profile. + :param voight_sigma: a Float node for the sigma parameter of the voigt profile. + :param structure: the StructureData object to be analysed + :returns: Dict objects for all generated spectra and associated binding energy + and core level shift. + + """ + from copy import deepcopy - result.update(spectra_broadening(core_level_shifts)) + ground_state_node = kwargs.pop('ground_state', None) + correction_energies = kwargs.pop('correction_energies', orm.Dict()).get_dict() + ch_nodes = kwargs.pop('ch_nodes', {}) + group_state_energy = ground_state_node.get_dict()['energy'] if ground_state_node is not None else None + core_levels = core_levels.get_dict() + equivalency_data = equivalent_sites_data.get_dict() + + data_dict = {element: {} for element in core_levels.keys()} + for key, xspectra_out_params in ch_nodes.items(): + element, _, site, orbital = key.split('_') + data_dict[element].setdefault(orbital, {}) + energy = xspectra_out_params.get_dict()['energy'] + data_dict[element][orbital][f'site_{site}'] = {'energy': energy, 'multiplicity': equivalency_data[f'site_{site}']['multiplicity']} + + result = {} + chemical_shifts = deepcopy(data_dict) + binding_energies = deepcopy(data_dict) + for element, orbitals in chemical_shifts.items(): + for orbital in orbitals: + lowest_energy = min([data['energy'] for data in data_dict[element][orbital].values()]) + for data in chemical_shifts[element][orbital].values(): + data['energy'] -= lowest_energy + if group_state_energy is not None: + for data in binding_energies[element][orbital].values(): + data['energy'] += -group_state_energy + correction_energies[element][orbital] + + result['chemical_shifts'] = orm.Dict(chemical_shifts) + spectra = spectra_broadening(chemical_shifts, + sigma=voight_sigma.value, + gamma=voight_gamma.value) + result['chemical_shift_spectra'] = spectra if ground_state_node is not None: - result.update(spectra_broadening(binding_energies, label='be_spectra')) + spectra = spectra_broadening(binding_energies, + sigma=voight_sigma.value, + gamma=voight_gamma.value) + result['binding_energy_spectra'] = spectra + result['binding_energies'] = orm.Dict(binding_energies) return result diff --git a/src/aiida_qe_xspec/gui/xps/model.py b/src/aiida_qe_xspec/gui/xps/model.py index 48dee30..cfac045 100644 --- a/src/aiida_qe_xspec/gui/xps/model.py +++ b/src/aiida_qe_xspec/gui/xps/model.py @@ -52,9 +52,10 @@ class XpsConfigurationSettingsModel(ConfigurationSettingsModel, HasInputStructur ) core_levels = tl.Dict( key_trait=tl.Unicode(), # core level - value_trait=tl.Bool(), # whether the core level is included + value_trait=tl.List(), default_value={}, ) + atom_indices = tl.List(trait=tl.Unicode(), default_value=[]) def update(self, specific=''): with self.hold_trait_notifications(): @@ -67,9 +68,9 @@ def get_supported_core_levels(self): for key in self.correction_energies: element = key.split('_')[0] if element not in supported_core_levels: - supported_core_levels[element] = [key] + supported_core_levels[element] = [key.split('_')[1]] else: - supported_core_levels[element].append(key) + supported_core_levels[element].append(key.split('_')[1]) return supported_core_levels def get_model_state(self): @@ -78,7 +79,8 @@ def get_model_state(self): 'structure_type': self.structure_type, 'pseudo_group': self.pseudo_group, 'correction_energies': self.correction_energies, - 'core_level_list': list(self.core_levels.keys()), + 'core_levels': self.core_levels, + 'atom_indices': self.atom_indices, } def set_model_state(self, parameters: dict): @@ -91,10 +93,8 @@ def set_model_state(self, parameters: dict): self.traits()['structure_type'].default_value, ) - core_level_list = parameters.get('core_level_list', []) - for orbital in self.core_levels: - if orbital in core_level_list: - self.core_levels[orbital] = True # type: ignore + self.core_levels = parameters.get('core_levels', []) + self.atom_indices = parameters.get('atom_indices', []) def reset(self): with self.hold_trait_notifications(): diff --git a/src/aiida_qe_xspec/gui/xps/setting.py b/src/aiida_qe_xspec/gui/xps/setting.py index 0b42835..73a79e0 100644 --- a/src/aiida_qe_xspec/gui/xps/setting.py +++ b/src/aiida_qe_xspec/gui/xps/setting.py @@ -161,35 +161,41 @@ def _build_core_levels_widget(self): return children = [] - - kind_names = self._model.input_structure.get_kind_names() if self._model.input_structure else [] - + elements = self._model.input_structure.get_symbols_set() supported_core_levels = self._model.get_supported_core_levels() - for kind_name in kind_names: - if kind_name in supported_core_levels: - for orbital in supported_core_levels[kind_name]: + for element in elements: + if element in supported_core_levels: + for orbital in supported_core_levels[element]: checkbox = ipw.Checkbox( - description=orbital, + description=f'{element}_{orbital}', indent=False, layout=ipw.Layout(max_width='100%'), ) + + def get_checked(levels, kind=element, orb=orbital): + return orb in levels.get(kind, []) + + def set_checked(value, kind=element, orb=orbital): + updated_levels = self._model.core_levels.copy() + if value: + updated_levels.setdefault(kind, []).append(orb) + else: + updated_levels[kind] = [o for o in updated_levels.get(kind, []) if o != orb] + if not updated_levels[kind]: + del updated_levels[kind] + return updated_levels + link = ipw.link( (self._model, 'core_levels'), (checkbox, 'value'), - [ - lambda levels, orbital=orbital: levels.get(orbital, False), - lambda value, orbital=orbital: { - **self._model.core_levels, - orbital: value, - }, - ], + [get_checked, set_checked], ) self.links.append(link) children.append(checkbox) else: checkbox = ipw.Checkbox( - description=f'{kind_name}, not supported by the selected pseudo group', + description=f'{element}, not supported by the selected pseudo group', indent=False, disabled=True, value=False, diff --git a/src/aiida_qe_xspec/gui/xps/workchain.py b/src/aiida_qe_xspec/gui/xps/workchain.py index 9c9982e..a356500 100644 --- a/src/aiida_qe_xspec/gui/xps/workchain.py +++ b/src/aiida_qe_xspec/gui/xps/workchain.py @@ -1,13 +1,13 @@ from aiida.orm import Bool, Dict, Float, Group, QueryBuilder from aiida.plugins import WorkflowFactory from aiida_quantumespresso.common.types import ElectronicType, SpinType +from aiida_qe_xspec.workflows.xps import XpsWorkChain +from aiida_qe_xspec.utils import load_core_hole_pseudos from aiidalab_qe.utils import ( enable_pencil_decomposition, set_component_resources, ) -XpsWorkChain = WorkflowFactory('xspec.xps') - # supercell min parameter for different protocols supercell_min_parameter_map = { 'fast': 4.0, @@ -27,23 +27,12 @@ def get_builder(codes, structure, parameters, **kwargs): protocol = parameters['workchain']['protocol'] xps_parameters = parameters.get('xps', {}) - all_correction_energies = xps_parameters.pop('correction_energies', {}) - core_level_list = xps_parameters.pop('core_level_list', None) + core_levels = xps_parameters.pop('core_levels') + atom_indices = xps_parameters.pop('atom_indices', None) # load pseudo for excited-state and group-state. pseudo_group = xps_parameters.pop('pseudo_group') - pseudo_group = QueryBuilder().append(Group, filters={'label': pseudo_group}).one()[0] - # set pseudo for element - pseudos = {} - elements_list = [] - correction_energies = {} - for label in core_level_list: - element = label.split('_')[0] - pseudos[element] = { - 'core_hole': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == label), - 'gipaw': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == f'{element}_gs'), - } - correction_energies[element] = all_correction_energies[label]['core'] - all_correction_energies[label]['exp'] - elements_list.append(element) + core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_levels, pseudo_group) + # is_molecule_input = True if xps_parameters.get('structure_type') == 'molecule' else False # set core hole treatment based on electronic type @@ -54,7 +43,7 @@ def get_builder(codes, structure, parameters, **kwargs): # if molecule input, set core hole treatment to full if is_molecule_input: core_hole_treatment = 'full' - core_hole_treatments = {element: core_hole_treatment for element in elements_list} + core_hole_treatments = {element: core_hole_treatment for element in core_levels} structure_preparation_settings = { 'supercell_min_parameter': Float(supercell_min_parameter_map[protocol]), 'is_molecule_input': Bool(is_molecule_input), @@ -78,8 +67,9 @@ def get_builder(codes, structure, parameters, **kwargs): code=pw_code, structure=structure, protocol=protocol, - pseudos=pseudos, - elements_list=elements_list, + core_hole_pseudos=core_hole_pseudos, + core_levels=core_levels, + atom_indices=atom_indices, calc_binding_energy=Bool(True), correction_energies=Dict(correction_energies), core_hole_treatments=core_hole_treatments, diff --git a/src/aiida_qe_xspec/utils.py b/src/aiida_qe_xspec/utils.py new file mode 100644 index 0000000..880eb1c --- /dev/null +++ b/src/aiida_qe_xspec/utils.py @@ -0,0 +1,19 @@ +from aiida import orm + + +def load_core_hole_pseudos(core_levels, pseudo_group='pseudo_demo_pbe'): + """Load the core hole pseudos for the given core levels and pseudo group.""" + pseudo_group = orm.QueryBuilder().append(orm.Group, filters={'label': pseudo_group}).one()[0] + all_correction_energies = pseudo_group.base.extras.get('correction', {}) + pseudos = {} + correction_energies = {} + for element in core_levels: + pseudos[element] = { + 'gipaw': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == f'{element}_gs'), + } + correction_energies[element] = {} + for orbital in core_levels[element]: + label = f'{element}_{orbital}' + pseudos[element][orbital] = next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == label) + correction_energies[element][orbital] = all_correction_energies[label]['core'] - all_correction_energies[label]['exp'] + return pseudos, correction_energies diff --git a/src/aiida_qe_xspec/workflows/functions/get_marked_structures.py b/src/aiida_qe_xspec/workflows/functions/get_marked_structures.py index 650f195..5ecdee4 100644 --- a/src/aiida_qe_xspec/workflows/functions/get_marked_structures.py +++ b/src/aiida_qe_xspec/workflows/functions/get_marked_structures.py @@ -6,10 +6,10 @@ @calcfunction -def get_marked_structures(structure, atoms_list, marker='X'): +def get_marked_structures(structure, atom_indices, marker='X'): """Read a StructureData object and return structures for XPS calculations. - :param atoms_list: the atoms_list of atoms to be marked. + :param atom_indices: the indices of atoms to be marked. :param marker: a Str node defining the name of the marked atom Kind. Default is 'X'. :returns: StructureData objects for the generated structure. """ @@ -23,7 +23,7 @@ def get_marked_structures(structure, atoms_list, marker='X'): output_params = {} result = {} - for index in atoms_list.get_list(): + for index in atom_indices.get_list(): marked_structure = StructureData() kinds = {kind.name: kind for kind in structure.kinds} marked_structure.set_cell(structure.cell) diff --git a/src/aiida_qe_xspec/workflows/xps.py b/src/aiida_qe_xspec/workflows/xps.py index d1984ed..a036efa 100644 --- a/src/aiida_qe_xspec/workflows/xps.py +++ b/src/aiida_qe_xspec/workflows/xps.py @@ -32,15 +32,15 @@ def validate_inputs(inputs, _): f'The marker given for the absorbing atom ("{abs_atom_marker}") matches an existing Kind in the ' f'input structure ({elements_present}).' ) - if 'elements_list' in inputs: - absorbing_elements_list = sorted(inputs['elements_list']) - if inputs['calc_binding_energy'].value: - ce_list = sorted(inputs['correction_energies'].get_dict().keys()) - if ce_list != absorbing_elements_list: - raise ValidationError( - f'The ``correction_energies`` provided ({ce_list}) does not match the list of' - f' absorbing elements ({absorbing_elements_list})' - ) + + if inputs['calc_binding_energy'].value: + elements1 = set(inputs['core_levels'].keys()) + elements2 = set(inputs['correction_energies'].get_dict().keys()) + if elements1 != elements2: + raise ValidationError( + f'The ``correction_energies`` provided ({elements1}) does not match the list of' + f' absorbing elements ({elements2})' + ) class XpsWorkChain(ProtocolMixin, WorkChain): @@ -93,15 +93,6 @@ def define(cls, spec): ' element. Must use the mapping "{element}" : {Upf}".' ) ) - spec.input_namespace( - 'gipaw_pseudos', - valid_type=(orm.UpfData, UpfData), - dynamic=True, - help=( - 'Dynamic namespace for pairs of ground-state pseudopotentials for each absorbing' - ' element. Must use the mapping "{element}" : {Upf}".' - ) - ) spec.input( 'core_hole_treatments', valid_type=orm.Dict, @@ -160,15 +151,15 @@ def define(cls, spec): ) ) spec.input( - 'elements_list', - valid_type=orm.List, + 'core_levels', + valid_type=orm.Dict, required=False, help=( - 'The list of elements to be considered for analysis, each must be valid elements of the periodic table.' + 'The elements and their core-levels to be considered for analysis. The element symbol must be valid elements of the periodic table.' ) ) spec.input( - 'atoms_list', + 'atom_indices', valid_type=orm.List, required=False, help=( @@ -260,26 +251,24 @@ def define(cls, spec): dynamic=True, help='The output parameters of each ``PwBaseWorkChain`` performed``.' ) - spec.output_namespace( + spec.output( 'chemical_shifts', valid_type=orm.Dict, - dynamic=True, help='All the chemical shift values for each element calculated by the WorkChain.' ) - spec.output_namespace( + spec.output( 'binding_energies', valid_type=orm.Dict, - dynamic=True, help='All the binding energy values for each element calculated by the WorkChain.' ) spec.output_namespace( - 'final_spectra_cls', + 'chemical_shift_spectra', valid_type=orm.XyData, dynamic=True, help='The fully-resolved spectra for each element based on chemical shift.' ) spec.output_namespace( - 'final_spectra_be', + 'binding_energy_spectra', valid_type=orm.XyData, dynamic=True, help='The fully-resolved spectra for each element based on binding energy.' @@ -374,12 +363,12 @@ def get_builder_from_protocol( # noqa cls, code, structure, - pseudos, + core_hole_pseudos, core_hole_treatments=None, protocol=None, overrides=None, - elements_list=None, - atoms_list=None, + core_levels=None, + atom_indices=None, options=None, structure_preparation_settings=None, correction_energies=None, @@ -430,39 +419,26 @@ def get_builder_from_protocol( # noqa else: builder.calc_binding_energy = orm.Bool(False) builder.clean_workdir = orm.Bool(inputs['clean_workdir']) - core_hole_pseudos = {} - gipaw_pseudos = {} - if elements_list: - elements_not_present = [] - elements_present = [kind.symbol for kind in structure.kinds] - for element in elements_list: - if element not in elements_present: - elements_not_present.append(element) - - if len(elements_not_present) > 0: - raise ValueError( - f'The following elements: {elements_not_present} are not present in the' - f' structure ({elements_present}) provided.' - ) - else: - builder.elements_list = orm.List(elements_list) - for element in elements_list: - core_hole_pseudos[element] = pseudos[element]['core_hole'] - gipaw_pseudos[element] = pseudos[element]['gipaw'] - elif atoms_list: - builder.atoms_list = orm.List(atoms_list) - for index in atoms_list: - element = structure.sites[index].kind_name - core_hole_pseudos[element] = pseudos[element]['core_hole'] - gipaw_pseudos[element] = pseudos[element]['gipaw'] - # if no elements list is given, we instead initalise the pseudos dict with all - # elements in the structure - else: - for element in pseudos: - core_hole_pseudos[element] = pseudos[element]['core_hole'] - gipaw_pseudos[element] = pseudos[element]['gipaw'] + elements_not_present = [] + elements_present = [kind.symbol for kind in structure.kinds] + if core_levels is None: + core_levels = {} + for element, pseudos in core_hole_pseudos.items(): + if element in structure.get_symbols_set(): + core_levels[element] = [key for key in pseudos.keys() if key != 'gipaw'] + for label in core_levels: + element = label.split('_')[0] + if element not in elements_present: + elements_not_present.append(element) + if len(elements_not_present) > 0: + raise ValueError( + f'The following elements: {elements_not_present} are not present in the' + f' structure ({elements_present}) provided.' + ) + builder.core_levels = orm.Dict(core_levels) + if atom_indices: + builder.atom_indices = orm.List(atom_indices) builder.core_hole_pseudos = core_hole_pseudos - builder.gipaw_pseudos = gipaw_pseudos if core_hole_treatments: builder.core_hole_treatments = orm.Dict(dict=core_hole_treatments) # for get_xspectra_structures @@ -481,17 +457,22 @@ def get_builder_from_protocol( # noqa def setup(self): """Init required context variables.""" - elements_list = self.inputs.get('elements_list', None) - atoms_list = self.inputs.get('atoms_list', None) - if elements_list: - self.ctx.elements_list = elements_list.get_list() - self.ctx.atoms_list = None - elif atoms_list: - self.ctx.atoms_list = atoms_list.get_list() - self.ctx.elements_list = None + self.ctx.current_structure = self.inputs.structure + self.ctx.atom_indices = self.inputs.atom_indices.get_list() if 'atom_indices' in self.inputs else None + # pseudos for all elements to be calculated should be replaced by the ground-state pseudos + self.ctx.pseudos = {key: value for key, value in self.inputs.ch_scf.pw.pseudos.items()} + + if 'core_levels' in self.inputs.core_levels.get_dict(): + self.ctx.core_levels = self.inputs.core_levels.get_dict() else: - structure = self.inputs.structure - self.ctx.elements_list = [Kind.symbol for Kind in structure.kinds] + core_levels = {} + for element, pseudos in self.inputs.core_hole_pseudos.items(): + if element in self.inputs.structure.get_symbols_set(): + core_levels[element] = [key for key in pseudos.keys() if key != 'gipaw'] + self.ctx.core_levels = core_levels + for kind in self.inputs.structure.kinds: + if kind.symbol in self.ctx.core_levels: + self.ctx.pseudos[kind.name] = self.inputs.core_hole_pseudos[kind.symbol]['gipaw'] def should_run_relax(self): """If the 'relax' input namespace was specified, we relax the input structure.""" @@ -517,10 +498,9 @@ def inspect_relax(self): self.report(f'PwRelaxWorkChain failed with exit status {workchain.exit_status}') return self.exit_codes.ERROR_SUB_PROCESS_FAILED_RELAX - relaxed_structure = workchain.outputs.output_structure relax_params = workchain.outputs.output_parameters - self.ctx.relaxed_structure = relaxed_structure - self.out('optimized_structure', relaxed_structure) + self.ctx.current_structure = workchain.outputs.output_structure + self.out('optimized_structure', workchain.outputs.output_structure) self.out('output_parameters_relax', relax_params) def prepare_structures(self): @@ -546,11 +526,19 @@ def prepare_structures(self): from aiida_qe_xspec.workflows.functions.get_marked_structures import get_marked_structures from aiida_qe_xspec.workflows.functions.get_xspectra_structures import get_xspectra_structures - input_structure = self.inputs.structure if 'relax' not in self.inputs else self.ctx.relaxed_structure - if self.ctx.elements_list: - elements_list = orm.List(self.ctx.elements_list) + input_structure = self.ctx.current_structure + if self.ctx.atom_indices: inputs = { - 'absorbing_elements_list': elements_list, + 'atom_indices': self.ctx.atom_indices, + 'marker': self.inputs.abs_atom_marker, + 'metadata': {'call_link_label': 'get_marked_structures'}, + } + result = get_marked_structures(input_structure, **inputs) + self.ctx.supercell = input_structure + self.ctx.equivalent_sites_data = result.pop('output_parameters').get_dict() + else: + inputs = { + 'absorbing_elements_list': orm.List(list(self.ctx.core_levels.keys())), 'absorbing_atom_marker': self.inputs.abs_atom_marker, 'metadata': {'call_link_label': 'get_xspectra_structures'}, } # populate this further once the schema for WorkChain options is figured out @@ -579,24 +567,10 @@ def prepare_structures(self): self.ctx.equivalent_sites_data = out_params['equivalent_sites_data'] self.out('supercell_structure', supercell) self.out('symmetry_analysis_data', out_params) - elif self.ctx.atoms_list: - atoms_list = orm.List(self.ctx.atoms_list) - inputs = { - 'atoms_list': atoms_list, - 'marker': self.inputs.abs_atom_marker, - 'metadata': {'call_link_label': 'get_marked_structures'}, - } - result = get_marked_structures(input_structure, **inputs) - self.ctx.supercell = input_structure - self.ctx.equivalent_sites_data = result.pop('output_parameters').get_dict() structures_to_process = {f'{Key.split("_")[0]}_{Key.split("_")[1]}': Value for Key, Value in result.items()} self.report(f'structures_to_process: {structures_to_process}') self.ctx.structures_to_process = structures_to_process - def should_run_gs_scf(self): - """If the 'calc_binding_energy' input namespace is True, we run a scf calculation for the supercell.""" - return self.inputs.calc_binding_energy - def run_gs_scf(self): """Call ``PwBaseWorkChain`` to compute total energy for the supercell.""" inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='ch_scf')) @@ -604,27 +578,13 @@ def run_gs_scf(self): inputs.metadata.call_link_label = 'supercell_xps' inputs = prepare_process_inputs(PwBaseWorkChain, inputs) - # pseudos for all elements to be calculated should be replaced - for site in self.ctx.equivalent_sites_data: - abs_element = self.ctx.equivalent_sites_data[site]['symbol'] - inputs.pw.pseudos[abs_element] = self.inputs.gipaw_pseudos[abs_element] + inputs.pw.pseudos = {key: value for key, value in self.ctx.pseudos.items()} running = self.submit(PwBaseWorkChain, **inputs) self.report(f'launched PwBaseWorkChain for supercell<{running.pk}>') return running - def inspect_scf(self): - """Verify that the PwBaseWorkChain finished successfully.""" - workchain = self.ctx.scf_workchain - - if not workchain.is_finished_ok: - self.report(f'PwBaseWorkChain failed with exit status {workchain.exit_status}') - return self.exit_codes.ERROR_SUB_PROCESS_FAILED_SCF - - scf_params = workchain.outputs.output_parameters - self.out('output_parameters_scf', scf_params) - def run_all_scf(self): """Call all PwBaseWorkChain's required to compute total energies for each absorbing atom site.""" # scf for supercell @@ -637,103 +597,75 @@ def run_all_scf(self): equivalent_sites_data = self.ctx.equivalent_sites_data abs_atom_marker = self.inputs.abs_atom_marker.value + ch_treatments = self.inputs.core_hole_treatments.get_dict() if 'core_hole_treatments' in self.inputs else {} + labels = {} for site in structures_to_process: - inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='ch_scf')) - structure = structures_to_process[site] - inputs.pw.structure = structure abs_element = equivalent_sites_data[site]['symbol'] - - if 'core_hole_treatments' in self.inputs: - ch_treatments = self.inputs.core_hole_treatments.get_dict() + labels[abs_element] = [] + for orbital in self.ctx.core_levels[abs_element]: + key = f'{abs_element}_{site}_{orbital}' + labels[abs_element].append(key) + inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='ch_scf')) + structure = structures_to_process[site] + inputs.pw.structure = structure ch_treatment = ch_treatments.get(abs_element, 'xch_smear') - else: - ch_treatment = 'xch_smear' - - inputs.metadata.call_link_label = f'{site}_xps' - - # Get the given settings for the SCF inputs and then overwrite them with the - # chosen core-hole approximation, then apply the correct pseudopotential pair - scf_params = inputs.pw.parameters.get_dict() - ch_treatment_inputs = self.get_treatment_inputs(treatment=ch_treatment) - - new_scf_params = recursive_merge(left=scf_params, right=ch_treatment_inputs) - if ch_treatment == 'xch_smear': - structure_kinds = [kind.name for kind in structure.kinds] - structure_kinds.sort() - abs_species = structure_kinds.index(abs_atom_marker) - new_scf_params['SYSTEM'][f'starting_magnetization({abs_species + 1})'] = 1 - - core_hole_pseudo = self.inputs.core_hole_pseudos[abs_element] - inputs.pw.pseudos[abs_atom_marker] = core_hole_pseudo - # pseudos for all elements to be calculated should be replaced - for key in self.ctx.equivalent_sites_data: - abs_element = self.ctx.equivalent_sites_data[key]['symbol'] - inputs.pw.pseudos[abs_element] = self.inputs.gipaw_pseudos[abs_element] - # remove pseudo if the only element is replaced by the marker - inputs.pw.pseudos = {kind.name: inputs.pw.pseudos[kind.name] for kind in structure.kinds} - - inputs.pw.parameters = orm.Dict(dict=new_scf_params) - - inputs = prepare_process_inputs(PwBaseWorkChain, inputs) - - future = self.submit(PwBaseWorkChain, **inputs) - futures[site] = future - self.report(f'launched PwBaseWorkChain for {site}<{future.pk}>') + inputs.metadata.call_link_label = f'{key}' + # Get the given settings for the SCF inputs and then overwrite them with the + # chosen core-hole approximation, then apply the correct pseudopotential pair + scf_params = inputs.pw.parameters.get_dict() + ch_treatment_inputs = self.get_treatment_inputs(treatment=ch_treatment) + new_scf_params = recursive_merge(left=scf_params, right=ch_treatment_inputs) + if ch_treatment == 'xch_smear': + structure_kinds = [kind.name for kind in structure.kinds] + structure_kinds.sort() + abs_species = structure_kinds.index(abs_atom_marker) + new_scf_params['SYSTEM'][f'starting_magnetization({abs_species + 1})'] = 1 + # remove pseudo if the only element is replaced by the marker + inputs.pw.pseudos = {key: value for key, value in self.ctx.pseudos.items() if key in structure.get_kind_names()} + inputs.pw.pseudos[abs_atom_marker] = self.inputs.core_hole_pseudos[abs_element][orbital] + inputs.pw.parameters = orm.Dict(dict=new_scf_params) + + inputs = prepare_process_inputs(PwBaseWorkChain, inputs) + + future = self.submit(PwBaseWorkChain, **inputs) + futures[key] = future + self.report(f'launched PwBaseWorkChain for {key}<{future.pk}>') + self.ctx.labels = labels return ToContext(**futures) def inspect_all_scf(self): """Check that all the PwBaseWorkChain sub-processes finished sucessfully.""" - labels = self.ctx.structures_to_process.keys() - work_chains = [self.ctx[label] for label in labels] failed_work_chains = [] - for work_chain, label in zip(work_chains, labels): - if not work_chain.is_finished_ok: - failed_work_chains.append(work_chain) - self.report(f'PwBaseWorkChain for ({label}) failed with exit status {work_chain.exit_status}') + for element, labels in self.ctx.labels.items(): + for label in labels: + work_chain = self.ctx[label] + if not work_chain.is_finished_ok: + failed_work_chains.append(work_chain) + self.report(f'PwBaseWorkChain for ({label}) failed with exit status {work_chain.exit_status}') if len(failed_work_chains) > 0: return self.exit_codes.ERROR_SUB_PROCESS_FAILED_CH_SCF def results(self): """Compile all output spectra, organise and post-process all computed spectra, and send to outputs.""" - site_labels = list(self.ctx.structures_to_process.keys()) - output_params_ch_scf = {label: self.ctx[label].outputs.output_parameters for label in site_labels} + output_params_ch_scf = {} + for labels in self.ctx.labels.values(): + for label in labels: + output_params_ch_scf[label] = self.ctx[label].outputs.output_parameters self.out('output_parameters_ch_scf', output_params_ch_scf) - kwargs = output_params_ch_scf.copy() + kwargs = {'ch_nodes': output_params_ch_scf} if self.inputs.calc_binding_energy: kwargs['ground_state'] = self.ctx['ground_state'].outputs.output_parameters kwargs['correction_energies'] = self.inputs.correction_energies kwargs['metadata'] = {'call_link_label': 'compile_final_spectra'} - if self.ctx.elements_list: - elements_list = orm.List(list=self.ctx.elements_list) - else: - symbols = {value['symbol'] for value in self.ctx.equivalent_sites_data.values()} - elements_list = orm.List(list(symbols)) voight_gamma = self.inputs.voight_gamma voight_sigma = self.inputs.voight_sigma equivalent_sites_data = orm.Dict(dict=self.ctx.equivalent_sites_data) - result = get_spectra_by_element(elements_list, equivalent_sites_data, voight_gamma, voight_sigma, **kwargs) - final_spectra_cls = {} - final_spectra_be = {} - chemical_shifts = {} - binding_energies = {} - for key, value in result.items(): - if key.endswith('cls_spectra'): - final_spectra_cls[key] = value - elif key.endswith('be_spectra'): - final_spectra_be[key] = value - elif key.endswith('cls'): - chemical_shifts[key] = value - elif key.endswith('be'): - binding_energies[key] = value - self.out('chemical_shifts', chemical_shifts) - self.out('final_spectra_cls', final_spectra_cls) - if self.inputs.calc_binding_energy: - self.out('binding_energies', binding_energies) - self.out('final_spectra_be', final_spectra_be) + result = get_spectra_by_element(orm.Dict(self.ctx.core_levels), equivalent_sites_data, voight_gamma, voight_sigma, **kwargs) + self.out_many(result) def on_terminated(self): """Clean the working directories of all child calculations if ``clean_workdir=True`` in the inputs.""" From 121573be2414550b39d2e5bd4c361b348a856859 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Sat, 8 Feb 2025 12:03:05 +0100 Subject: [PATCH 2/7] update GUI to support `atom_indices` and multiple core-levles --- src/aiida_qe_xspec/gui/xps/model.py | 2 +- src/aiida_qe_xspec/gui/xps/result/model.py | 2 +- src/aiida_qe_xspec/gui/xps/result/result.py | 3 + src/aiida_qe_xspec/gui/xps/result/utils.py | 60 ++++++++----------- src/aiida_qe_xspec/gui/xps/setting.py | 25 +++++++- .../gui/xps/structure_examples/Al.cif | 26 ++++++++ .../gui/xps/structure_examples/__init__.py | 1 + src/aiida_qe_xspec/workflows/xps.py | 2 +- 8 files changed, 83 insertions(+), 38 deletions(-) create mode 100644 src/aiida_qe_xspec/gui/xps/structure_examples/Al.cif diff --git a/src/aiida_qe_xspec/gui/xps/model.py b/src/aiida_qe_xspec/gui/xps/model.py index cfac045..720e0de 100644 --- a/src/aiida_qe_xspec/gui/xps/model.py +++ b/src/aiida_qe_xspec/gui/xps/model.py @@ -55,7 +55,7 @@ class XpsConfigurationSettingsModel(ConfigurationSettingsModel, HasInputStructur value_trait=tl.List(), default_value={}, ) - atom_indices = tl.List(trait=tl.Unicode(), default_value=[]) + atom_indices = tl.List(trait=tl.Int(), default_value=[]) def update(self, specific=''): with self.hold_trait_notifications(): diff --git a/src/aiida_qe_xspec/gui/xps/result/model.py b/src/aiida_qe_xspec/gui/xps/result/model.py index 7330b70..f64806b 100644 --- a/src/aiida_qe_xspec/gui/xps/result/model.py +++ b/src/aiida_qe_xspec/gui/xps/result/model.py @@ -41,7 +41,7 @@ def update_spectrum_options(self): self.binding_energies, self.equivalent_sites_data, ) = export_xps_data(outputs) - options = [key.split('_')[0] for key in self.chemical_shifts.keys()] + options = [f'{element}_{orbital}' for element, data in self.chemical_shifts.items() for orbital in data.keys()] self.spectrum_options = options self.spectrum = options[0] if options else None diff --git a/src/aiida_qe_xspec/gui/xps/result/result.py b/src/aiida_qe_xspec/gui/xps/result/result.py index 9ff4713..f3cfa17 100644 --- a/src/aiida_qe_xspec/gui/xps/result/result.py +++ b/src/aiida_qe_xspec/gui/xps/result/result.py @@ -172,6 +172,9 @@ def _render(self): self.plot, upload_container, ] + self.rendered = True + self._post_render() + self._update_plot(None) def _post_render(self): self._model.update_spectrum_options() diff --git a/src/aiida_qe_xspec/gui/xps/result/utils.py b/src/aiida_qe_xspec/gui/xps/result/utils.py index 8117efd..6a73b09 100644 --- a/src/aiida_qe_xspec/gui/xps/result/utils.py +++ b/src/aiida_qe_xspec/gui/xps/result/utils.py @@ -1,17 +1,9 @@ def export_xps_data(outputs): """Export the data from the XPS workchain""" - chemical_shifts = {} symmetry_analysis_data = outputs.symmetry_analysis_data.get_dict() equivalent_sites_data = symmetry_analysis_data['equivalent_sites_data'] - if 'chemical_shifts' in outputs: - for key, data in outputs.chemical_shifts.items(): - ele = key[:-4] - chemical_shifts[ele] = data.get_dict() - binding_energies = {} - if 'binding_energies' in outputs: - for key, data in outputs.binding_energies.items(): - ele = key[:-3] - binding_energies[ele] = data.get_dict() + chemical_shifts = outputs.chemical_shifts.get_dict() if 'chemical_shifts' in outputs else {} + binding_energies = outputs.binding_energies.get_dict() if 'binding_energies' in outputs else {} return ( chemical_shifts, @@ -27,29 +19,29 @@ def xps_spectra_broadening(points, equivalent_sites_data, gamma=0.3, sigma=0.3, result_spectra = {} fwhm_voight = gamma / 2 + np.sqrt(gamma**2 / 4 + sigma**2) - for element, point in points.items(): - result_spectra[element] = {} - final_spectra_y_arrays = [] - total_multiplicity = sum([equivalent_sites_data[site]['multiplicity'] for site in point]) - max_core_level_shift = max(point.values()) - min_core_level_shift = min(point.values()) - # Energy range for the Broadening function - x_energy_range = np.linspace( - min_core_level_shift - fwhm_voight - 1.5, - max_core_level_shift + fwhm_voight + 1.5, - 500, - ) - for site in point: - # Weight for the spectra of every atom - intensity = equivalent_sites_data[site]['multiplicity'] * intensity - relative_core_level_position = point[site] - y = ( - intensity - * voigt_profile(x_energy_range - relative_core_level_position, sigma, gamma) - / total_multiplicity + for element, orbitals in points.items(): + for orbital, data in orbitals.items(): + result_spectra[f'{element}_{orbital}'] = {} + final_spectra_y_arrays = [] + total_multiplicity = sum(d['multiplicity'] for d in data.values()) + max_core_level_shift = max([d['energy'] for d in data.values()]) + min_core_level_shift = min([d['energy'] for d in data.values()]) + # Energy range for the Broadening function + x_energy_range = np.linspace( + min_core_level_shift - fwhm_voight - 1.5, + max_core_level_shift + fwhm_voight + 1.5, + 500, ) - result_spectra[element][site] = [x_energy_range, y] - final_spectra_y_arrays.append(y) - total = sum(final_spectra_y_arrays) - result_spectra[element]['total'] = [x_energy_range, total] + for site, d in data.items(): + # Weight for the spectra of every atom + relative_core_level_position = d['energy'] + y = ( + intensity + * voigt_profile(x_energy_range - relative_core_level_position, sigma, gamma) + *d['multiplicity'] / total_multiplicity + ) + result_spectra[f'{element}_{orbital}'][site] = [x_energy_range, y] + final_spectra_y_arrays.append(y) + total = sum(final_spectra_y_arrays) + result_spectra[f'{element}_{orbital}']['total'] = [x_energy_range, total] return result_spectra diff --git a/src/aiida_qe_xspec/gui/xps/setting.py b/src/aiida_qe_xspec/gui/xps/setting.py index 73a79e0..64c0f24 100644 --- a/src/aiida_qe_xspec/gui/xps/setting.py +++ b/src/aiida_qe_xspec/gui/xps/setting.py @@ -49,6 +49,28 @@ def render(self): ) self.core_levels_widget = ipw.VBox() + self.atom_indices_input = ipw.Text( + description='Indices:', + placeholder='Enter indices separated by commas', + style={'description_width': 'initial'}, + ) + ipw.link( + (self._model, 'atom_indices'), + (self.atom_indices_input, 'value'), + [ + lambda value: ', '.join(value), + lambda value: [int(i.strip()) for i in value.split(',') if i.strip()], + ], + ) + self.atom_indices_container = ipw.VBox([ + ipw.HTML(""" +
+

Select atoms

+ Leave empty to calculate for all atoms of selected element. +
+ """), + self.atom_indices_input, + ]) self.structure_type = ipw.ToggleButtons() ipw.dlink( @@ -126,9 +148,10 @@ def render(self): """ ), - ipw.HBox( + ipw.VBox( children=[ self.core_levels_widget, + self.atom_indices_container, ] ), ] diff --git a/src/aiida_qe_xspec/gui/xps/structure_examples/Al.cif b/src/aiida_qe_xspec/gui/xps/structure_examples/Al.cif new file mode 100644 index 0000000..a271b15 --- /dev/null +++ b/src/aiida_qe_xspec/gui/xps/structure_examples/Al.cif @@ -0,0 +1,26 @@ +data_image0 +_chemical_formula_structural Al +_chemical_formula_sum "Al1" +_cell_length_a 2.8637824638055176 +_cell_length_b 2.8637824638055176 +_cell_length_c 2.8637824638055176 +_cell_angle_alpha 60.00000000000001 +_cell_angle_beta 60.00000000000001 +_cell_angle_gamma 60.00000000000001 + +_space_group_name_H-M_alt "P 1" +_space_group_IT_number 1 + +loop_ + _space_group_symop_operation_xyz + 'x, y, z' + +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Al Al1 1.0 0.0 0.0 0.0 1.0000 diff --git a/src/aiida_qe_xspec/gui/xps/structure_examples/__init__.py b/src/aiida_qe_xspec/gui/xps/structure_examples/__init__.py index 93f4c3e..5c3bcb5 100644 --- a/src/aiida_qe_xspec/gui/xps/structure_examples/__init__.py +++ b/src/aiida_qe_xspec/gui/xps/structure_examples/__init__.py @@ -6,5 +6,6 @@ 'structures': [ ('Phenylacetylene molecule', file_path / 'Phenylacetylene.xyz'), ('ETFA molecule', file_path / 'ETFA.xyz'), + ('Aluminum bulk', file_path / 'Al.cif'), ], } diff --git a/src/aiida_qe_xspec/workflows/xps.py b/src/aiida_qe_xspec/workflows/xps.py index a036efa..54508a3 100644 --- a/src/aiida_qe_xspec/workflows/xps.py +++ b/src/aiida_qe_xspec/workflows/xps.py @@ -601,7 +601,7 @@ def run_all_scf(self): labels = {} for site in structures_to_process: abs_element = equivalent_sites_data[site]['symbol'] - labels[abs_element] = [] + labels.setdefault(abs_element, []) for orbital in self.ctx.core_levels[abs_element]: key = f'{abs_element}_{site}_{orbital}' labels[abs_element].append(key) From c6c9ef78bf1c4fd62d099e5ad8d9a7519fa8f3a8 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Mon, 10 Feb 2025 16:44:51 +0100 Subject: [PATCH 3/7] Implement the review suggestions: - Add input validation - Update docstring --- .../functions/xspectra/get_xps_spectra.py | 9 +++- src/aiida_qe_xspec/workflows/xps.py | 53 +++++++++++++------ tests/conftest.py | 12 +++++ tests/test_xps_inputs_validation.py | 51 ++++++++++++++++++ 4 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 tests/test_xps_inputs_validation.py diff --git a/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py b/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py index 7219ea0..2db6235 100644 --- a/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py +++ b/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py @@ -4,7 +4,12 @@ import numpy as np def spectra_broadening(points, sigma=0.1, gamma=0.1): - """Broadening base on the binding energy.""" + """Broadening base on the binding energy. + + :param points: a Dict object containing the binding energy and multiplicity for each site. + :param sigma: a Float node for the sigma parameter of the voigt profile. + :param gamma: a Float node for the gamma parameter of the voigt profile. + """ from scipy.special import voigt_profile # pylint: disable=no-name-in-module fwhm_voight = gamma / 2 + np.sqrt(gamma**2 / 4 + sigma**2) @@ -51,7 +56,7 @@ def get_spectra_by_element(core_levels, equivalent_sites_data, voight_gamma, voi Generate the final spectra using the Voigt profile. :param core_levels: a Dict object defining the elements and their core-levels to consider - when producing spectrum. + when producing spectra, e.g., {"C": ["1s"], "Al": ["2s", "2p"]}. :param equivalent_sites_data: an Dict object containing symmetry data. :param voight_gamma: a Float node for the gamma parameter of the voigt profile. :param voight_sigma: a Float node for the sigma parameter of the voigt profile. diff --git a/src/aiida_qe_xspec/workflows/xps.py b/src/aiida_qe_xspec/workflows/xps.py index 54508a3..9cb5c88 100644 --- a/src/aiida_qe_xspec/workflows/xps.py +++ b/src/aiida_qe_xspec/workflows/xps.py @@ -25,13 +25,41 @@ def validate_inputs(inputs, _): """Validate the inputs before launching the WorkChain.""" structure = inputs['structure'] - elements_present = [kind.name for kind in structure.kinds] + elements_present = {kind.symbol for kind in structure.kinds} abs_atom_marker = inputs['abs_atom_marker'].value + core_hole_pseudos = inputs['core_hole_pseudos'] if abs_atom_marker in elements_present: raise ValidationError( f'The marker given for the absorbing atom ("{abs_atom_marker}") matches an existing Kind in the ' f'input structure ({elements_present}).' ) + if 'core_levels' in inputs: + # keys of core_levels should be a subset of the structure elements and core_hole_pseudos + core_levels = inputs['core_levels'].get_dict() + if not set(core_levels.keys()).issubset(elements_present): + elements_not_present = set(core_levels.keys()) - elements_present + raise ValidationError( + f'The following elements: {elements_not_present} are not present in the' + f' structure ({elements_present}) provided.' + ) + if not set(core_levels.keys()).issubset(set(core_hole_pseudos.keys())): + elements_not_present = set(core_levels.keys()) - set(core_hole_pseudos.keys()) + raise ValidationError( + f'The following elements: {elements_not_present} are required for analysis but ' + f'their pseudopotentials are not provided.' + ) + if 'atom_indices' in inputs: + # indices should be in the range of the number of atoms in the structure + if not all(0 <= index < len(structure.sites) for index in inputs['atom_indices'].get_list()): + raise ValidationError('All atom indices must be within the range of the number of atoms in the structure.') + # all the elements corresponding to the atom indices should be in the core_hole_pseudos + elements = {structure.get_kind(structure.sites[i].kind_name).symbol for i in inputs['atom_indices'].get_list()} + if not elements.issubset(set(core_hole_pseudos.keys())): + elements_not_present = elements - set(core_hole_pseudos.keys()) + raise ValidationError( + f'The following elements: {elements_not_present} are required for analysis but' + f' their pseudopotentials are not provided.' + ) if inputs['calc_binding_energy'].value: elements1 = set(inputs['core_levels'].keys()) @@ -42,7 +70,6 @@ def validate_inputs(inputs, _): f' absorbing elements ({elements2})' ) - class XpsWorkChain(ProtocolMixin, WorkChain): """Workchain to compute X-ray photoelectron spectra (XPS) for a given structure. @@ -378,12 +405,19 @@ def get_builder_from_protocol( # noqa :param code: the ``Code`` instance configured for the ``quantumespresso.pw`` plugin. :param structure: the ``StructureData`` instance to use. - :param pseudos: the core-hole pseudopotential pairs (ground-state and + :param core_hole_pseudos: the core-hole pseudopotential (ground-state and excited-state) for the elements to be calculated. These must - use the mapping of {"element" : {"core_hole" : , "gipaw" : }} + use the mapping of {"element" : {"1s" : , "gipaw" : }} :param protocol: the protocol to use. If not specified, the default will be used. + :core_hole_treatments: optional dictionary to set core-hole treatment for each element, + e.g., {"C": "full"}. :param overrides: optional dictionary of inputs to override the defaults of the XpsWorkChain itself. + :param core_levels: the elements and their core-levels to be considered for analysis. + e.g., {"C": ["1s"], "Al": ["2s", "2p"]}. + :param atom_indices: the indices of atoms to be considered for analysis. + :correction_energies: optional dictionary to set the correction energy to each core level, + e.g., {'C': {'1s': 339.79}}. :param kwargs: additional keyword arguments that will be passed to the ``get_builder_from_protocol`` of all the sub processes that are called by this workchain. @@ -419,22 +453,11 @@ def get_builder_from_protocol( # noqa else: builder.calc_binding_energy = orm.Bool(False) builder.clean_workdir = orm.Bool(inputs['clean_workdir']) - elements_not_present = [] - elements_present = [kind.symbol for kind in structure.kinds] if core_levels is None: core_levels = {} for element, pseudos in core_hole_pseudos.items(): if element in structure.get_symbols_set(): core_levels[element] = [key for key in pseudos.keys() if key != 'gipaw'] - for label in core_levels: - element = label.split('_')[0] - if element not in elements_present: - elements_not_present.append(element) - if len(elements_not_present) > 0: - raise ValueError( - f'The following elements: {elements_not_present} are not present in the' - f' structure ({elements_present}) provided.' - ) builder.core_levels = orm.Dict(core_levels) if atom_indices: builder.atom_indices = orm.List(atom_indices) diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..8220bf0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +from aiida_qe_xspec.gui.xps import structure_examples +from ase.io import read +from aiida import orm + +@pytest.fixture +def etfa_molecule(): + atoms = read(structure_examples['structures'][1][1]) + atoms.center(vacuum=3.0) + atoms.pbc = True + structure = orm.StructureData(ase=atoms) + return structure diff --git a/tests/test_xps_inputs_validation.py b/tests/test_xps_inputs_validation.py new file mode 100644 index 0000000..7ef362d --- /dev/null +++ b/tests/test_xps_inputs_validation.py @@ -0,0 +1,51 @@ +from aiida_qe_xspec.utils import load_core_hole_pseudos +from aiida_qe_xspec.workflows.xps import XpsWorkChain +from aiida import orm, load_profile +import pytest +from aiida.common import ValidationError +from aiida.engine import run + +load_profile() + +def test_validate_get_builder_from_protocol(etfa_molecule): + code = orm.load_code('qe-7.2-pw@localhost') + core_levels = {'C': ['1s']} + core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_levels, 'pseudo_demo_pbe') + with pytest.raises(ValidationError, match="The following elements: {'Fe'} are not present in the structure"): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'Fe': ['1s']}, + correction_energies=orm.Dict(correction_energies), + ) + run(builder) + with pytest.raises(ValidationError, match="The following elements: {'O'} are required for analysis but their pseudopotentials are not provided."): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': ['1s'], 'O': ['1s']}, + correction_energies=orm.Dict(correction_energies), + ) + run(builder) + with pytest.raises(ValidationError, match='All atom indices must be within the range of the number of atoms in the structure.'): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': ['1s']}, + atom_indices = [0, 100], + correction_energies=orm.Dict(correction_energies), + ) + run(builder) + with pytest.raises(ValidationError, match="The following elements: {'O'} are required for analysis but their pseudopotentials are not provided."): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': ['1s']}, + atom_indices = [8], + correction_energies=orm.Dict(correction_energies), + ) + run(builder) From f2d6e3fa353e035780e3dc56c14d213b1414b8d6 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Mon, 10 Feb 2025 17:50:57 +0100 Subject: [PATCH 4/7] collect the energy and multiplicity properly --- .../functions/xspectra/get_xps_spectra.py | 20 ++++++----- src/aiida_qe_xspec/workflows/xps.py | 33 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py b/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py index 2db6235..7b527da 100644 --- a/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py +++ b/src/aiida_qe_xspec/calculations/functions/xspectra/get_xps_spectra.py @@ -69,17 +69,21 @@ def get_spectra_by_element(core_levels, equivalent_sites_data, voight_gamma, voi ground_state_node = kwargs.pop('ground_state', None) correction_energies = kwargs.pop('correction_energies', orm.Dict()).get_dict() - ch_nodes = kwargs.pop('ch_nodes', {}) + output_params_ch_scf = kwargs.pop('output_params_ch_scf', {}) group_state_energy = ground_state_node.get_dict()['energy'] if ground_state_node is not None else None core_levels = core_levels.get_dict() equivalency_data = equivalent_sites_data.get_dict() - - data_dict = {element: {} for element in core_levels.keys()} - for key, xspectra_out_params in ch_nodes.items(): - element, _, site, orbital = key.split('_') - data_dict[element].setdefault(orbital, {}) - energy = xspectra_out_params.get_dict()['energy'] - data_dict[element][orbital][f'site_{site}'] = {'energy': energy, 'multiplicity': equivalency_data[f'site_{site}']['multiplicity']} + # collect the energy and multiplicity + data_dict = {} + for element, element_data in output_params_ch_scf.items(): + data_dict[element] = {} + for orbital, orbital_data in element_data.items(): + data_dict[element][orbital] = {} + for site, site_data in orbital_data.items(): + data_dict[element][orbital][site] = { + 'energy': site_data.get_dict()['energy'], + 'multiplicity': equivalency_data[site]['multiplicity'] + } result = {} chemical_shifts = deepcopy(data_dict) diff --git a/src/aiida_qe_xspec/workflows/xps.py b/src/aiida_qe_xspec/workflows/xps.py index 9cb5c88..26db7df 100644 --- a/src/aiida_qe_xspec/workflows/xps.py +++ b/src/aiida_qe_xspec/workflows/xps.py @@ -624,10 +624,11 @@ def run_all_scf(self): labels = {} for site in structures_to_process: abs_element = equivalent_sites_data[site]['symbol'] - labels.setdefault(abs_element, []) + labels.setdefault(abs_element, {}) for orbital in self.ctx.core_levels[abs_element]: + labels[abs_element].setdefault(orbital, {}) key = f'{abs_element}_{site}_{orbital}' - labels[abs_element].append(key) + labels[abs_element][orbital][site] = key inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='ch_scf')) structure = structures_to_process[site] inputs.pw.structure = structure @@ -660,24 +661,28 @@ def run_all_scf(self): def inspect_all_scf(self): """Check that all the PwBaseWorkChain sub-processes finished sucessfully.""" failed_work_chains = [] - for element, labels in self.ctx.labels.items(): - for label in labels: - work_chain = self.ctx[label] - if not work_chain.is_finished_ok: - failed_work_chains.append(work_chain) - self.report(f'PwBaseWorkChain for ({label}) failed with exit status {work_chain.exit_status}') + output_params_ch_scf = {} + for element, element_data in self.ctx.labels.items(): + output_params_ch_scf[element] = {} + for orbital, orbital_data in element_data.items(): + output_params_ch_scf[element][orbital] = {} + for site, label in orbital_data.items(): + work_chain = self.ctx[label] + if not work_chain.is_finished_ok: + failed_work_chains.append(work_chain) + self.report(f'PwBaseWorkChain for ({label}) failed with exit status {work_chain.exit_status}') + else: + output_params_ch_scf[element][orbital][site] = work_chain.outputs.output_parameters if len(failed_work_chains) > 0: return self.exit_codes.ERROR_SUB_PROCESS_FAILED_CH_SCF + self.ctx.output_params_ch_scf = output_params_ch_scf def results(self): """Compile all output spectra, organise and post-process all computed spectra, and send to outputs.""" - output_params_ch_scf = {} - for labels in self.ctx.labels.values(): - for label in labels: - output_params_ch_scf[label] = self.ctx[label].outputs.output_parameters - self.out('output_parameters_ch_scf', output_params_ch_scf) - kwargs = {'ch_nodes': output_params_ch_scf} + self.out('output_parameters_ch_scf', self.ctx.output_params_ch_scf) + + kwargs = {'output_params_ch_scf': self.ctx.output_params_ch_scf} if self.inputs.calc_binding_energy: kwargs['ground_state'] = self.ctx['ground_state'].outputs.output_parameters kwargs['correction_energies'] = self.inputs.correction_energies From 37bbfc913b4a06c041c17662b8e46aa147ed5eea Mon Sep 17 00:00:00 2001 From: superstar54 Date: Mon, 10 Feb 2025 18:02:30 +0100 Subject: [PATCH 5/7] Update the tests --- tests/test_xps.py | 48 +++++++++-------------------------------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/tests/test_xps.py b/tests/test_xps.py index ee2b80d..d2bcc4c 100644 --- a/tests/test_xps.py +++ b/tests/test_xps.py @@ -4,24 +4,7 @@ from aiida_qe_xspec.workflows.xps import XpsWorkChain import numpy as np from aiida import orm - - -def load_core_hole_pseudos(core_level_list, pseudo_group='pseudo_demo_pbe'): - """Load the core hole pseudos.""" - pseudo_group = orm.QueryBuilder().append(orm.Group, filters={'label': pseudo_group}).one()[0] - all_correction_energies = pseudo_group.base.extras.get('correction', {}) - pseudos = {} - elements_list = [] - correction_energies = {} - for label in core_level_list: - element = label.split('_')[0] - pseudos[element] = { - 'core_hole': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == label), - 'gipaw': next(pseudo for pseudo in pseudo_group.nodes if pseudo.label == f'{element}_gs'), - } - correction_energies[element] = all_correction_energies[label]['core'] - all_correction_energies[label]['exp'] - elements_list.append(element) - return pseudos, correction_energies +from aiida_qe_xspec.utils import load_core_hole_pseudos def test_solid(): @@ -30,51 +13,38 @@ def test_solid(): atoms = bulk('Si') structure = orm.StructureData(ase=atoms) code = orm.load_code('qe-7.2-pw@localhost') - parameters = { - 'CONTROL': { - 'calculation': 'scf', - }, - 'SYSTEM': { - 'ecutwfc': 30, - 'ecutrho': 200, - 'occupations': 'smearing', - 'smearing': 'gaussian', - }, - } # Load the pseudopotential family. kpoints = orm.KpointsData() kpoints.set_kpoints_mesh([5, 5, 5]) # - metadata = { - 'options': { + options = { 'resources': { 'num_machines': 1, - 'num_mpiprocs_per_machine': 1, + 'num_mpiprocs_per_machine': 2, }, } - } structure_preparation_settings = { 'supercell_min_parameter': orm.Float(1.0), 'is_molecule_input': orm.Bool(False), } # Load the pseudopotential family. - core_level_list = ['Si_2p'] + core_level_list = {'Si': ['2p']} core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_level_list, 'pseudo_demo_pbe') core_hole_treatments={'Si': 'xch_smear'} builder = XpsWorkChain.get_builder_from_protocol( structure=structure, code=code, protocol='fast', - pseudos=core_hole_pseudos, - elements_list=['Si'], + core_hole_pseudos=core_hole_pseudos, + core_levels={'Si': ['2p']}, calc_binding_energy=orm.Bool(True), - parameters=orm.Dict(dict=parameters), correction_energies=orm.Dict(correction_energies), core_hole_treatments=core_hole_treatments, structure_preparation_settings=structure_preparation_settings, kpoints=kpoints, - metadata=metadata, + overrides = {'ch_scf': {'pseudo_family': 'SSSP/1.2/PBE/efficiency'}}, + options=options, ) builder.pop('relax') _, node = run_get_node(builder) - np.isclose(node.outputs.binding_energies.Si_be.get_dict()['site_0'], 99.8438, atol=1e-2) + np.isclose(node.outputs.binding_energies.get_dict()['Si']['2p']['site_0']['energy'], 99.8438, atol=1e-2) From f4ea8f35cf6d0ce446c88b5ebd6b5b2189448bb0 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Tue, 11 Feb 2025 21:22:38 +0100 Subject: [PATCH 6/7] add more input validation --- src/aiida_qe_xspec/workflows/xps.py | 105 +++++++++++++++++++++++----- tests/test_xps_inputs_validation.py | 44 ++++++++++-- 2 files changed, 127 insertions(+), 22 deletions(-) diff --git a/src/aiida_qe_xspec/workflows/xps.py b/src/aiida_qe_xspec/workflows/xps.py index 26db7df..d34402e 100644 --- a/src/aiida_qe_xspec/workflows/xps.py +++ b/src/aiida_qe_xspec/workflows/xps.py @@ -28,47 +28,116 @@ def validate_inputs(inputs, _): elements_present = {kind.symbol for kind in structure.kinds} abs_atom_marker = inputs['abs_atom_marker'].value core_hole_pseudos = inputs['core_hole_pseudos'] + + # 1. Check the absorbing atom marker does not clash with existing kinds if abs_atom_marker in elements_present: raise ValidationError( f'The marker given for the absorbing atom ("{abs_atom_marker}") matches an existing Kind in the ' f'input structure ({elements_present}).' ) + + # 2. Validate `core_levels`, if provided if 'core_levels' in inputs: - # keys of core_levels should be a subset of the structure elements and core_hole_pseudos core_levels = inputs['core_levels'].get_dict() + + # (a) Must be a dictionary {element: list_of_orbitals} + for element, orbitals in core_levels.items(): + if not isinstance(orbitals, list): + raise ValidationError( + f'`core_levels` for element "{element}" must be a list of strings, e.g. ["1s", "2s"], ' + f'but got: {orbitals}' + ) + + # (b) The keys of `core_levels` should be a subset of the structure's elements: if not set(core_levels.keys()).issubset(elements_present): elements_not_present = set(core_levels.keys()) - elements_present raise ValidationError( - f'The following elements: {elements_not_present} are not present in the' - f' structure ({elements_present}) provided.' + f'The following elements: {elements_not_present} in `core_levels` ' + f'are not present in the structure ({elements_present}).' ) + + # (c) The keys of `core_levels` should be a subset of `core_hole_pseudos`: if not set(core_levels.keys()).issubset(set(core_hole_pseudos.keys())): - elements_not_present = set(core_levels.keys()) - set(core_hole_pseudos.keys()) + missing = set(core_levels.keys()) - set(core_hole_pseudos.keys()) raise ValidationError( - f'The following elements: {elements_not_present} are required for analysis but ' - f'their pseudopotentials are not provided.' + f'Elements {missing} are requested in `core_levels` but no corresponding ' + f'pseudopotentials are found in `core_hole_pseudos`.' ) + + # (d) Check the orbitals themselves for consistency (typos, recognized labels, etc.) + # Define a minimal set of valid orbitals here; extend as needed. + VALID_ORBITALS = {'1s', '2s', '2p', '3s', '3p', '3d', '4s', '4p', '4d', '4f', '5s', '5p', '5d'} + for element, orbitals in core_levels.items(): + for orb in orbitals: + if orb not in VALID_ORBITALS: + raise ValidationError( + f'Unrecognized orbital "{orb}" for element "{element}". ' + f'Valid orbitals are: {VALID_ORBITALS}' + ) + # Check that this orbital also exists in the excited-state pseudo dictionary + # (i.e., `core_hole_pseudos[element]` must have a key with the same label). + if orb not in core_hole_pseudos[element].keys(): + raise ValidationError( + f'No pseudopotential entry found for orbital "{orb}" under element "{element}" ' + f'in `core_hole_pseudos`. Found: {list(core_hole_pseudos[element].keys())}' + ) + + # 3. Validate atom_indices, if provided if 'atom_indices' in inputs: - # indices should be in the range of the number of atoms in the structure - if not all(0 <= index < len(structure.sites) for index in inputs['atom_indices'].get_list()): - raise ValidationError('All atom indices must be within the range of the number of atoms in the structure.') - # all the elements corresponding to the atom indices should be in the core_hole_pseudos - elements = {structure.get_kind(structure.sites[i].kind_name).symbol for i in inputs['atom_indices'].get_list()} + atom_indices = inputs['atom_indices'].get_list() + # (a) Indices must be in range + if not all(0 <= index < len(structure.sites) for index in atom_indices): + raise ValidationError('All atom indices in `atom_indices` must be valid indices within the structure.') + + # (b) The elements for those atoms must be in `core_hole_pseudos` + elements = {structure.get_kind(structure.sites[i].kind_name).symbol for i in atom_indices} if not elements.issubset(set(core_hole_pseudos.keys())): elements_not_present = elements - set(core_hole_pseudos.keys()) raise ValidationError( - f'The following elements: {elements_not_present} are required for analysis but' - f' their pseudopotentials are not provided.' + f'The following elements: {elements_not_present} are required for analysis but ' + f'no pseudopotentials are provided for them in `core_hole_pseudos`.' ) + # 4. Validate correction_energies if calc_binding_energy=True if inputs['calc_binding_energy'].value: - elements1 = set(inputs['core_levels'].keys()) - elements2 = set(inputs['correction_energies'].get_dict().keys()) - if elements1 != elements2: + if 'core_levels' not in inputs: raise ValidationError( - f'The ``correction_energies`` provided ({elements1}) does not match the list of' - f' absorbing elements ({elements2})' + '`calc_binding_energy=True` was requested, but `core_levels` is not provided.' ) + if 'correction_energies' not in inputs: + raise ValidationError( + '`calc_binding_energy=True` was requested, but `correction_energies` is not provided.' + ) + + core_levels = inputs['core_levels'].get_dict() + elements_in_core_levels = set(core_levels.keys()) + correction_dict = inputs['correction_energies'].get_dict() + elements_in_corrections = set(correction_dict.keys()) + + # (a) Must have same elements + if elements_in_core_levels != elements_in_corrections: + raise ValidationError( + f'The elements in `correction_energies` ({elements_in_corrections}) do not match ' + f'the elements in `core_levels` ({elements_in_core_levels}).' + ) + + # (b) Must have same orbitals per element + for element, orbitals in core_levels.items(): + # orbitals is guaranteed to be a list from earlier checks + corrections_for_elem = correction_dict[element] + if not isinstance(corrections_for_elem, dict): + raise ValidationError( + f'`correction_energies[{element}]` must be a dictionary of orbital_name: float_value. ' + f'Got {type(corrections_for_elem)} instead.' + ) + orbitals_in_corrections = set(corrections_for_elem.keys()) + orbitals_in_core_levels = set(orbitals) + if orbitals_in_core_levels != orbitals_in_corrections: + raise ValidationError( + f'For element "{element}", the orbitals in `correction_energies` ({orbitals_in_corrections}) ' + f'do not match the orbitals in `core_levels` ({orbitals_in_core_levels}).' + ) + class XpsWorkChain(ProtocolMixin, WorkChain): """Workchain to compute X-ray photoelectron spectra (XPS) for a given structure. diff --git a/tests/test_xps_inputs_validation.py b/tests/test_xps_inputs_validation.py index 7ef362d..cb2f470 100644 --- a/tests/test_xps_inputs_validation.py +++ b/tests/test_xps_inputs_validation.py @@ -11,7 +11,7 @@ def test_validate_get_builder_from_protocol(etfa_molecule): code = orm.load_code('qe-7.2-pw@localhost') core_levels = {'C': ['1s']} core_hole_pseudos, correction_energies = load_core_hole_pseudos(core_levels, 'pseudo_demo_pbe') - with pytest.raises(ValidationError, match="The following elements: {'Fe'} are not present in the structure"): + with pytest.raises(ValidationError, match="The following elements: {'Fe'} in `core_levels` are not present in the structure"): builder = XpsWorkChain.get_builder_from_protocol( structure=etfa_molecule, code=code, @@ -20,7 +20,7 @@ def test_validate_get_builder_from_protocol(etfa_molecule): correction_energies=orm.Dict(correction_energies), ) run(builder) - with pytest.raises(ValidationError, match="The following elements: {'O'} are required for analysis but their pseudopotentials are not provided."): + with pytest.raises(ValidationError, match="Elements {'O'} are requested in `core_levels` but no corresponding pseudopotentials are found in"): builder = XpsWorkChain.get_builder_from_protocol( structure=etfa_molecule, code=code, @@ -29,7 +29,7 @@ def test_validate_get_builder_from_protocol(etfa_molecule): correction_energies=orm.Dict(correction_energies), ) run(builder) - with pytest.raises(ValidationError, match='All atom indices must be within the range of the number of atoms in the structure.'): + with pytest.raises(ValidationError, match='All atom indices in `atom_indices` must be valid indices within the structure.'): builder = XpsWorkChain.get_builder_from_protocol( structure=etfa_molecule, code=code, @@ -39,7 +39,7 @@ def test_validate_get_builder_from_protocol(etfa_molecule): correction_energies=orm.Dict(correction_energies), ) run(builder) - with pytest.raises(ValidationError, match="The following elements: {'O'} are required for analysis but their pseudopotentials are not provided."): + with pytest.raises(ValidationError, match="The following elements: {'O'} are required for analysis but no pseudopotentials are provided for them in `core_hole_pseudos`."): builder = XpsWorkChain.get_builder_from_protocol( structure=etfa_molecule, code=code, @@ -49,3 +49,39 @@ def test_validate_get_builder_from_protocol(etfa_molecule): correction_energies=orm.Dict(correction_energies), ) run(builder) + with pytest.raises(ValidationError, match="`core_levels` for element \"C\" must be a list of strings,"): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': '1s'}, + correction_energies=orm.Dict(correction_energies), + ) + run(builder) + with pytest.raises(ValidationError, match="Unrecognized orbital \"1 s\" for element \"C\". Valid orbitals are:"): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': ['1 s']}, + correction_energies=orm.Dict(correction_energies), + ) + run(builder) + with pytest.raises(ValidationError, match='`calc_binding_energy=True` was requested, but `correction_energies` is not provided.'): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': ['1s']}, + ) + builder.calc_binding_energy = orm.Bool(True) + run(builder) + with pytest.raises(ValidationError, match="No pseudopotential entry found for orbital \"2s\" under element \"C\" in `core_hole_pseudos`"): + builder = XpsWorkChain.get_builder_from_protocol( + structure=etfa_molecule, + code=code, + core_hole_pseudos=core_hole_pseudos, + core_levels={'C': ['1s', '2s']}, + correction_energies=orm.Dict(correction_energies), + ) + run(builder) From 3b52fcb9fb1aa239342b5f9bc8858e9375c87d55 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Wed, 12 Feb 2025 10:01:12 +0100 Subject: [PATCH 7/7] Update help message of core_hole_pseudos --- src/aiida_qe_xspec/workflows/xps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aiida_qe_xspec/workflows/xps.py b/src/aiida_qe_xspec/workflows/xps.py index d34402e..1c4610f 100644 --- a/src/aiida_qe_xspec/workflows/xps.py +++ b/src/aiida_qe_xspec/workflows/xps.py @@ -185,8 +185,8 @@ def define(cls, spec): valid_type=(orm.UpfData, UpfData), dynamic=True, help=( - 'Dynamic namespace for pairs of excited-state pseudopotentials for each absorbing' - ' element. Must use the mapping "{element}" : {Upf}".' + 'Dynamic namespace for ground-state and excited-state pseudopotentials for each absorbing' + ' element. Must use the mapping: {"element" : {"gipaw" : , "1s" : \}\}' ) ) spec.input(