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(
""""""
)
- 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