From f239ca89d3ce0a0f5f4bd16d00d4bf7e40337e0a Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero <34098967+AndresOrtegaGuerrero@users.noreply.github.com> Date: Thu, 23 May 2024 21:56:27 +0200 Subject: [PATCH] Feature Spin-Orbit coupling (#708) * Feature Spin-Orbit coupling --- src/aiidalab_qe/app/configuration/advanced.py | 33 +++++++++ src/aiidalab_qe/app/configuration/pseudos.py | 68 +++++++++++++++---- src/aiidalab_qe/app/result/summary_viewer.py | 3 + .../app/static/workflow_summary.jinja | 7 ++ src/aiidalab_qe/common/setup_pseudos.py | 4 ++ src/aiidalab_qe/workflows/__init__.py | 12 +--- tests/test_pseudo.py | 18 +++-- tests/test_result/test_summary_report.yml | 1 + tests_integration/test_image.py | 2 +- 9 files changed, 116 insertions(+), 32 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced.py b/src/aiidalab_qe/app/configuration/advanced.py index a92b1394..bc5e51fa 100644 --- a/src/aiidalab_qe/app/configuration/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced.py @@ -209,6 +209,21 @@ def __init__(self, default_protocol=None, **kwargs): (self.etot_conv_thr, "disabled"), lambda override: not override, ) + # Spin-Orbit calculation + self.spin_orbit = ipw.ToggleButtons( + options=[ + ("Off", "wo_soc"), + ("On", "soc"), + ], + description="Spin-Orbit:", + value="wo_soc", + style={"description_width": "initial"}, + ) + ipw.dlink( + (self.override, "value"), + (self.spin_orbit, "disabled"), + lambda override: not override, + ) self.pseudo_family_selector = PseudoFamilySelector() self.pseudo_setter = PseudoSetter() @@ -217,6 +232,12 @@ def __init__(self, default_protocol=None, **kwargs): (self.pseudo_setter, "pseudo_family"), ) self.kpoints_distance.observe(self._display_mesh, "value") + + # Link with PseudoWidget + ipw.dlink( + (self.spin_orbit, "value"), + (self.pseudo_family_selector, "spin_orbit"), + ) self.children = [ self.title, ipw.HBox( @@ -245,6 +266,8 @@ def __init__(self, default_protocol=None, **kwargs): self.kpoints_description, ipw.HBox([self.kpoints_distance, self.mesh_grid]), self.hubbard_widget, + # Spin-Orbit calculation + self.spin_orbit, self.pseudo_family_selector, self.pseudo_setter, ] @@ -436,6 +459,12 @@ def get_panel_value(self): self.etot_conv_thr.value ) + # Spin-Orbit calculation + if self.spin_orbit.value == "soc": + parameters["pw"]["parameters"]["SYSTEM"]["lspinorb"] = True + parameters["pw"]["parameters"]["SYSTEM"]["noncolin"] = True + parameters["pw"]["parameters"]["SYSTEM"]["nspin"] = 4 + return parameters def set_insulator_magnetization(self, parameters): @@ -483,6 +512,10 @@ def set_panel_value(self, parameters): self.total_charge.value = parameters["pw"]["parameters"]["SYSTEM"].get( "tot_charge", 0 ) + if "lspinorb" in system: + self.spin_orbit.value = "soc" + else: + self.spin_orbit.value = "wo_soc" # van der waals correction self.van_der_waals.value = self.dftd3_version.get( system.get("dftd3_version"), diff --git a/src/aiidalab_qe/app/configuration/pseudos.py b/src/aiidalab_qe/app/configuration/pseudos.py index 7815c860..912041c2 100644 --- a/src/aiidalab_qe/app/configuration/pseudos.py +++ b/src/aiidalab_qe/app/configuration/pseudos.py @@ -30,6 +30,20 @@ class PseudoFamilySelector(ipw.VBox): """

Accuracy and precision

""" ) + PSEUDO_HELP_SOC = """
+ Spin-orbit coupling (SOC) calculations are supported exclusively with PseudoDojo pseudopotentials. + PseudoDojo offers these pseudopotentials in two versions: standard and stringent. + Here, we utilize the FR (fully relativistic) type from PseudoDojo. + Please ensure you choose appropriate cutoff values for your calculations. +
""" + + PSEUDO_HELP_WO_SOC = """
+ If you are unsure, select 'SSSP efficiency', which for + most calculations will produce sufficiently accurate results at + comparatively small computational costs. If your calculations require a + higher accuracy, select 'SSSP accuracy' or 'PseudoDojo stringent', which will be computationally + more expensive. SSSP is the standard solid-state pseudopotentials. + The PseudoDojo used here has the SR relativistic type.
""" description = ipw.HTML( """
@@ -45,15 +59,7 @@ class PseudoFamilySelector(ipw.VBox): Pseudopotential family
""" ) - pseudo_family_help = ipw.HTML( - """
- If you are unsure, select 'SSSP efficiency', which for - most calculations will produce sufficiently accurate results at - comparatively small computational costs. If your calculations require a - higher accuracy, select 'SSSP accuracy' or 'PseudoDojo stringent', which will be computationally - more expensive. SSSP is the standard solid-state pseudopotentials. - The PseudoDojo used here has the SR relativistic type.
""" - ) + pseudo_family_help = ipw.HTML(PSEUDO_HELP_WO_SOC) dft_functional_prompt = ipw.HTML( """ @@ -68,6 +74,7 @@ class PseudoFamilySelector(ipw.VBox): ) protocol = tl.Unicode(allow_none=True) disabled = tl.Bool() + spin_orbit = tl.Unicode() # output pseudo family widget which is the string of the pseudo family (of the AiiDA group). value = tl.Unicode(allow_none=True) @@ -150,9 +157,14 @@ def set_value(self, _=None): functional = self.dft_functional.value # XXX (jusong.yu): a validator is needed to check the family string is consistent with the list of pseudo families defined in the setup_pseudos.py if library == "PseudoDojo": - pseudo_family_string = ( - f"PseudoDojo/{PSEUDODOJO_VERSION}/{functional}/SR/{accuracy}/upf" - ) + if self.spin_orbit == "soc": + pseudo_family_string = ( + f"PseudoDojo/{PSEUDODOJO_VERSION}/{functional}/FR/{accuracy}/upf" + ) + else: + pseudo_family_string = ( + f"PseudoDojo/{PSEUDODOJO_VERSION}/{functional}/SR/{accuracy}/upf" + ) elif library == "SSSP": pseudo_family_string = f"SSSP/{SSSP_VERSION}/{functional}/{accuracy}" else: @@ -190,6 +202,24 @@ def reset(self): # stay the same while xc selection is changed. self._update_settings_from_protocol(self.protocol) + @tl.observe("spin_orbit") + def _update_library_selection(self, _): + """Update the library selection according to the spin orbit value.""" + if self.spin_orbit == "soc": + self.library_selection.options = [ + "PseudoDojo standard", + "PseudoDojo stringent", + ] + self.pseudo_family_help.value = self.PSEUDO_HELP_SOC + else: + self.library_selection.options = [ + "SSSP efficiency", + "SSSP precision", + "PseudoDojo standard", + "PseudoDojo stringent", + ] + self.pseudo_family_help.value = self.PSEUDO_HELP_WO_SOC + @tl.observe("protocol") def _protocol_changed(self, _): """Input protocol changed, update the value of widgets.""" @@ -198,9 +228,17 @@ def _protocol_changed(self, _): def _update_settings_from_protocol(self, protocol): """Update the widget values from the given protocol, and trigger the callback.""" # FIXME: this rely on the aiida-quantumespresso, which is not ideal - pseudo_family_string = PwBaseWorkChain.get_protocol_inputs(protocol)[ - "pseudo_family" - ] + + if self.spin_orbit == "soc": + if protocol in ["fast", "moderate"]: + pseudo_family_string = "PseudoDojo/0.4/PBE/FR/standard/upf" + else: + pseudo_family_string = "PseudoDojo/0.4/PBE/FR/stringent/upf" + else: + pseudo_family_string = PwBaseWorkChain.get_protocol_inputs(protocol)[ + "pseudo_family" + ] + pseudo_family = PseudoFamily.from_string(pseudo_family_string) self.load_from_pseudo_family(pseudo_family) diff --git a/src/aiidalab_qe/app/result/summary_viewer.py b/src/aiidalab_qe/app/result/summary_viewer.py index cec3f803..0bc1b728 100644 --- a/src/aiidalab_qe/app/result/summary_viewer.py +++ b/src/aiidalab_qe/app/result/summary_viewer.py @@ -124,6 +124,9 @@ def generate_report_parameters(qeapp_wc): qeapp_wc.inputs.structure.pbc, "xyz" ) + # Spin-Oribit coupling + report["spin_orbit"] = pw_parameters["SYSTEM"].get("lspinorb", False) + # DFT+U hubbard_dict = ui_parameters["advanced"].pop("hubbard_parameters", None) if hubbard_dict: diff --git a/src/aiidalab_qe/app/static/workflow_summary.jinja b/src/aiidalab_qe/app/static/workflow_summary.jinja index 58f18141..594e4615 100644 --- a/src/aiidalab_qe/app/static/workflow_summary.jinja +++ b/src/aiidalab_qe/app/static/workflow_summary.jinja @@ -122,6 +122,13 @@ {{ hubbard_u }} {% endif %} + {% if spin_orbit %} + + Spin-Orbit + {{ spin_orbit }} + + {% endif %} + diff --git a/src/aiidalab_qe/common/setup_pseudos.py b/src/aiidalab_qe/common/setup_pseudos.py index 3e73a9e3..f367dccf 100644 --- a/src/aiidalab_qe/common/setup_pseudos.py +++ b/src/aiidalab_qe/common/setup_pseudos.py @@ -28,6 +28,10 @@ f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/standard/upf", f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/stringent/upf", f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/stringent/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/FR/standard/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/FR/standard/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/FR/stringent/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/FR/stringent/upf", } diff --git a/src/aiidalab_qe/workflows/__init__.py b/src/aiidalab_qe/workflows/__init__.py index 372f9e80..04ebc048 100644 --- a/src/aiidalab_qe/workflows/__init__.py +++ b/src/aiidalab_qe/workflows/__init__.py @@ -20,6 +20,8 @@ # because we want to decouple the workflows from the app, so I copied it here # instead of importing it. # load entry points + + def get_entries(entry_point_name="aiidalab_qe.property"): from importlib.metadata import entry_points @@ -190,15 +192,6 @@ def get_builder_from_protocol( relax_builder.pop("base_final_scf", None) # never run a final scf builder.relax = relax_builder - # remove starting magnetization if tot_magnetization is set - if ( - relax_builder["base"]["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") - is not None - ): - builder.relax["base"]["pw"]["parameters"]["SYSTEM"].pop( - "starting_magnetization", None - ) - if properties is None: properties = [] builder.properties = orm.List(list=properties) @@ -211,7 +204,6 @@ def get_builder_from_protocol( plugin_builder = entry_point["get_builder"]( codes, builder.structure, copy.deepcopy(parameters), **kwargs ) - # check if the plugin has a clean_workdir input plugin_workchain = entry_point["workchain"] if plugin_workchain.spec().has_input("clean_workdir"): plugin_builder.clean_workdir = clean_workdir diff --git a/tests/test_pseudo.py b/tests/test_pseudo.py index f3599531..2412b63b 100644 --- a/tests/test_pseudo.py +++ b/tests/test_pseudo.py @@ -104,7 +104,7 @@ def test_setup_pseudos_cmd(tmp_path): def test_pseudos_installation(): """Test install_pseudos""" # Test by compare the pseudos_to_install before and after the installation - assert len(pseudos_to_install()) == 8 + assert len(pseudos_to_install()) == 12 EXPECTED_PSEUDOS = { f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/standard/upf", f"SSSP/{SSSP_VERSION}/PBE/efficiency", @@ -114,13 +114,13 @@ def test_pseudos_installation(): [_ for _ in _install_pseudos(EXPECTED_PSEUDOS)] # Two pseudos are installed - assert len(pseudos_to_install()) == 6 + assert len(pseudos_to_install()) == 10 @pytest.mark.usefixtures("aiida_profile_clean") def test_download_and_install_pseudo_from_file(tmp_path): """Test download and install pseudo from file.""" - assert len(pseudos_to_install()) == 8 + assert len(pseudos_to_install()) == 12 EXPECTED_PSEUDOS = { f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/standard/upf", f"SSSP/{SSSP_VERSION}/PBE/efficiency", @@ -129,14 +129,14 @@ def test_download_and_install_pseudo_from_file(tmp_path): # Download the pseudos to the tmp_path but not install [_ for _ in _install_pseudos(EXPECTED_PSEUDOS, download_only=True, cwd=tmp_path)] - assert len(pseudos_to_install()) == 8 + assert len(pseudos_to_install()) == 12 assert len(list(tmp_path.iterdir())) == 2 # Install the pseudos from the tmp_path [_ for _ in _install_pseudos(EXPECTED_PSEUDOS, cwd=tmp_path)] # Two pseudos are installed - assert len(pseudos_to_install()) == 6 + assert len(pseudos_to_install()) == 10 def test_pseudos_family_selector_widget(): @@ -147,7 +147,7 @@ def test_pseudos_family_selector_widget(): assert w.override.value is False w.override.value = True - + w.spin_orbit = "wo_soc" # test the default value assert w.value == f"SSSP/{SSSP_VERSION}/PBEsol/efficiency" @@ -163,6 +163,12 @@ def test_pseudos_family_selector_widget(): w.library_selection.value = "PseudoDojo stringent" assert w.value == f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/stringent/upf" + # Test spin-orbit change will update + + w.spin_orbit = "soc" + w.protocol = "moderate" + assert w.value == f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/FR/standard/upf" + @pytest.mark.usefixtures("sssp") def test_pseudos_setter_widget(generate_structure_data, generate_upf_data): diff --git a/tests/test_result/test_summary_report.yml b/tests/test_result/test_summary_report.yml index d41f91b1..b1ebaa50 100644 --- a/tests/test_result/test_summary_report.yml +++ b/tests/test_result/test_summary_report.yml @@ -26,6 +26,7 @@ relax_method: positions_cell relaxed: positions_cell scf_kpoints_distance: 0.5 smearing: cold +spin_orbit: false tot_charge: 0.0 tot_magnetization: false vdw_corr: none diff --git a/tests_integration/test_image.py b/tests_integration/test_image.py index 1a4841b2..e134df02 100755 --- a/tests_integration/test_image.py +++ b/tests_integration/test_image.py @@ -26,4 +26,4 @@ def test_pseudos_families_are_installed(aiidalab_exec, nb_user): assert "PseudoDojo" in output # Two lines of header, 8 pseudos - assert len(output.splitlines()) == 10 + assert len(output.splitlines()) == 14