From 89c70a287c0c50b844a9ae3f9e3ebb975004970e Mon Sep 17 00:00:00 2001
From: Miki Bonacci <46074008+mikibonacci@users.noreply.github.com>
Date: Wed, 2 Oct 2024 15:15:27 +0200
Subject: [PATCH] Adding then supercell estimator in the Settings Panel. (#88)
This fixes #34
* Adding then supercell estimator in the Settings Panel.
Limitations:
- use only the structure and the supercell size, not the other parameters (symprec, distinguish_kinds, is_symmetry)
- to understand how we can adapt this to consider also the HubbardStructureData
- performance is low (especially for the `_reset_supercell` reaction.
* Ensuring performance for the supercell estimation
- update the number of supercell only when all the three vectors are updated (from hint) or reset
- we provide also spinning loading icon when we compute the number of supercells.
- missing: symprec integration.
* Added also the symprec in the estimation of the supercells to be computed
- adding a reset for symprec
- adding upper and lower bound 1 and 1e-7 (if out of this range, automatically reset to max or min)
- added tests for all the new widgets/logics
---
src/aiidalab_qe_vibroscopy/app/settings.py | 183 ++++++++++++++++++++-
tests/test_settings.py | 48 ++++++
2 files changed, 228 insertions(+), 3 deletions(-)
diff --git a/src/aiidalab_qe_vibroscopy/app/settings.py b/src/aiidalab_qe_vibroscopy/app/settings.py
index a2ed646..49f6043 100644
--- a/src/aiidalab_qe_vibroscopy/app/settings.py
+++ b/src/aiidalab_qe_vibroscopy/app/settings.py
@@ -15,6 +15,60 @@
from aiidalab_qe.common.panel import Panel
+import sys
+import os
+
+from aiida.plugins import DataFactory
+
+HubbardStructureData = DataFactory("quantumespresso.hubbard_structure")
+
+# spinner for waiting time (supercell estimations)
+spinner_html = """
+
+
+"""
+
+
+def disable_print(func):
+ def wrapper(*args, **kwargs):
+ # Save the current standard output
+ original_stdout = sys.stdout
+ # Redirect standard output to os.devnull
+ sys.stdout = open(os.devnull, "w")
+ try:
+ # Call the function
+ result = func(*args, **kwargs)
+ finally:
+ # Restore the original standard output
+ sys.stdout.close()
+ sys.stdout = original_stdout
+ return result
+
+ return wrapper
+
+
class Setting(Panel):
title = "Vibrational Settings"
@@ -103,6 +157,7 @@ def change_supercell(_=None):
)
for elem in [self._sc_x, self._sc_y, self._sc_z]:
elem.observe(change_supercell, names="value")
+ elem.observe(self._estimate_supercells, names="value")
self.supercell_selector = ipw.HBox(
children=[
@@ -124,19 +179,41 @@ def change_supercell(_=None):
self.supercell_hint_button = ipw.Button(
description="Size hint",
disabled=False,
- width="500px",
+ width="100px",
button_style="info",
)
# supercell hint (15A lattice params)
self.supercell_hint_button.on_click(self._suggest_supercell)
+ # reset supercell
+ self.supercell_reset_button = ipw.Button(
+ description="Reset hint",
+ disabled=False,
+ layout=ipw.Layout(width="100px"),
+ button_style="warning",
+ )
+ # supercell reset reaction
+ self.supercell_reset_button.on_click(self._reset_supercell)
+
+ # Estimate the number of supercells for frozen phonons.
+ self.supercell_number_estimator = ipw.HTML(
+ description="Number of supercells to be computed:",
+ value="0",
+ style={"description_width": "initial"},
+ )
+
## end supercell hint.
self.supercell_widget = ipw.VBox(
[
self.hint_button_help,
ipw.HBox(
- [self.supercell_selector, self.supercell_hint_button],
+ [
+ self.supercell_selector,
+ self.supercell_hint_button,
+ self.supercell_reset_button,
+ self.supercell_number_estimator,
+ ],
),
]
)
@@ -145,10 +222,26 @@ def change_supercell(_=None):
self.symmetry_symprec = ipw.FloatText(
value=1e-5,
+ max=1,
+ min=1e-7, # Ensure the value is always positive
+ step=1e-4, # Step value of 1e-4
description="Symmetry tolerance (symprec):",
style={"description_width": "initial"},
layout={"width": "300px"},
)
+ self.symmetry_symprec.observe(self._estimate_supercells, "value")
+
+ # reset supercell
+ self.symmetry_symprec_reset_button = ipw.Button(
+ description="Reset symprec",
+ disabled=False,
+ layout=ipw.Layout(width="150px"),
+ button_style="warning",
+ )
+ # supercell reset reaction
+ self.symmetry_symprec_reset_button.on_click(
+ lambda _: setattr(self.symmetry_symprec, "value", 1e-5)
+ )
self.children = [
ipw.VBox(
@@ -174,11 +267,22 @@ def change_supercell(_=None):
],
),
self.supercell_widget,
- self.symmetry_symprec,
+ ipw.HBox(
+ [
+ self.symmetry_symprec,
+ self.symmetry_symprec_reset_button,
+ ],
+ ),
]
super().__init__(**kwargs)
+ # we define a block for the estimation of the supercell if we ask for hint,
+ # so that we call the estimator only at the end of the supercell hint generator,
+ # and now each time after the x, y, z generation (i.e., we don't lose time).
+ # see the methods below.
+ self.block = False
+
@tl.observe("input_structure")
def _update_input_structure(self, change):
if self.input_structure is not None:
@@ -204,13 +308,86 @@ def _suggest_supercell(self, _=None):
suggested_3D = 15 // np.array(s.cell.cellpar()[:3]) + 1
# if disabled, it means that it is a non-periodic direction.
+ # here we manually unobserve the `_estimate_supercells`, so it is faster
+ # and only compute when all the three directions are updated
+ self.block = True
for direction, suggested, original in zip(
[self._sc_x, self._sc_y, self._sc_z], suggested_3D, s.cell.cellpar()[:3]
):
direction.value = suggested if not direction.disabled else 1
+ self.block = False
+ self._estimate_supercells()
else:
return
+ @tl.observe("input_structure")
+ @disable_print
+ def _estimate_supercells(self, _=None):
+ """_summary_
+
+ Estimate the number of supercells to be computed for frozen phonon calculation.
+ """
+ if self.block:
+ return
+
+ symprec_value = self.symmetry_symprec.value
+
+ self.symmetry_symprec.value = max(1e-5, min(symprec_value, 1))
+
+ self.supercell_number_estimator.value = spinner_html
+
+ from aiida_phonopy.data.preprocess import PreProcessData
+
+ if self.input_structure:
+ preprocess_data = PreProcessData(
+ structure=self.input_structure,
+ supercell_matrix=[
+ [self._sc_x.value, 0, 0],
+ [0, self._sc_y.value, 0],
+ [0, 0, self._sc_z.value],
+ ],
+ symprec=self.symmetry_symprec.value,
+ distinguish_kinds=False,
+ is_symmetry=True,
+ )
+
+ supercells = preprocess_data.get_supercells_with_displacements()
+
+ # for now, we comment the following part, as the HubbardSD is generated in the submission step.
+ """if isinstance(self.input_structure, HubbardStructureData):
+ from aiida_vibroscopy.calculations.spectra_utils import get_supercells_for_hubbard
+ from aiida_vibroscopy.workflows.phonons.base import get_supercell_hubbard_structure
+ supercell = get_supercell_hubbard_structure(
+ self.input_structure,
+ self.input_structure,
+ metadata={"store_provenance": False},
+ )
+ supercells = get_supercells_for_hubbard(
+ preprocess_data=preprocess_data,
+ ref_structure=supercell,
+ metadata={"store_provenance": False},
+ )
+
+ else:
+ supercells = preprocess_data.get_supercells_with_displacements()
+ """
+ self.supercell_number_estimator.value = f"{len(supercells)}"
+
+ return
+
+ def _reset_supercell(self, _=None):
+ if self.input_structure is not None:
+ reset_supercell = []
+ self.block = True
+ for direction, periodic in zip(
+ [self._sc_x, self._sc_y, self._sc_z], self.input_structure.pbc
+ ):
+ reset_supercell.append(2 if periodic else 1)
+ (self._sc_x.value, self._sc_y.value, self._sc_z.value) = reset_supercell
+ self.block = False
+ self._estimate_supercells()
+ return
+
def get_panel_value(self):
"""Return a dictionary with the input parameters for the plugin."""
return {
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 88d7db6..d071fbe 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -75,3 +75,51 @@ def test_x_settings(generate_structure_data):
configure_step.workchain_settings.properties["vibronic"].run.value = True
parameters = configure_step.settings["vibronic"].get_panel_value()
assert parameters["supercell_selector"] == [2, 1, 1]
+
+
+@pytest.mark.usefixtures("sssp")
+def test_supercell_number_estimator(generate_structure_data):
+ """Test the settings of the vibroscopy app."""
+
+ from aiidalab_qe.app.configuration import ConfigureQeAppWorkChainStep
+
+ configure_step = ConfigureQeAppWorkChainStep()
+ structure = generate_structure_data("silicon")
+ configure_step.input_structure = structure
+ configure_step.workchain_settings.properties["vibronic"].run.value = True
+ parameters = configure_step.settings["vibronic"].get_panel_value()
+ assert parameters["supercell_selector"] == [2, 2, 2]
+ assert configure_step.settings["vibronic"].supercell_number_estimator.value == "1"
+ configure_step.settings["vibronic"]._suggest_supercell()
+ parameters = configure_step.settings["vibronic"].get_panel_value()
+ assert parameters["supercell_selector"] == [4, 4, 4]
+ assert configure_step.settings["vibronic"].supercell_number_estimator.value == "1"
+ configure_step.settings["vibronic"]._sc_x.value = 3
+ configure_step.settings["vibronic"]._sc_y.value = 2
+ configure_step.settings["vibronic"]._sc_z.value = 2
+ assert configure_step.settings["vibronic"].supercell_number_estimator.value == "4"
+ configure_step.settings["vibronic"]._reset_supercell()
+ configure_step.settings["vibronic"]._sc_x.value = 2
+ configure_step.settings["vibronic"]._sc_y.value = 2
+ configure_step.settings["vibronic"]._sc_z.value = 2
+ assert configure_step.settings["vibronic"].supercell_number_estimator.value == "1"
+
+
+@pytest.mark.usefixtures("sssp")
+def test_symprec(generate_structure_data):
+ """Test the settings of the vibroscopy app."""
+
+ from aiidalab_qe.app.configuration import ConfigureQeAppWorkChainStep
+
+ configure_step = ConfigureQeAppWorkChainStep()
+ structure = generate_structure_data("silicon")
+ configure_step.input_structure = structure
+ configure_step.workchain_settings.properties["vibronic"].run.value = True
+ parameters = configure_step.settings["vibronic"].get_panel_value()
+ assert parameters["symmetry_symprec"] == 1e-5
+ configure_step.settings["vibronic"].symmetry_symprec.value = 1
+ parameters = configure_step.settings["vibronic"].get_panel_value()
+ assert parameters["symmetry_symprec"] == 1
+ configure_step.settings["vibronic"].reset()
+ parameters = configure_step.settings["vibronic"].get_panel_value()
+ assert parameters["symmetry_symprec"] == 1e-5