From 860f59bd5b0a033a5bd5f28a5e6dd3732b384d86 Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero <34098967+AndresOrtegaGuerrero@users.noreply.github.com> Date: Sat, 5 Oct 2024 08:48:21 +0200 Subject: [PATCH] Feature/handling molecules (#834) - Include the option molecule for StructureData editor - Band structure calculation should be disabled - Cell Optimizatio not available if structure is molecule - kpoint_distance should be disable and force [1,1,1] - Realoading an old workflow should restore normal behavior of app --- src/aiidalab_qe/app/configuration/__init__.py | 5 +++ src/aiidalab_qe/app/configuration/advanced.py | 45 ++++++++++++++++--- src/aiidalab_qe/app/configuration/workflow.py | 36 +++++++++++++++ src/aiidalab_qe/app/main.py | 1 + src/aiidalab_qe/app/result/summary_viewer.py | 1 + src/aiidalab_qe/common/widgets.py | 7 +-- src/aiidalab_qe/plugins/pdos/setting.py | 8 ++++ tests/configuration/test_advanced.py | 37 +++++++++++++++ tests/conftest.py | 7 +++ 9 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/__init__.py b/src/aiidalab_qe/app/configuration/__init__.py index adebd3898..39f5f07ae 100644 --- a/src/aiidalab_qe/app/configuration/__init__.py +++ b/src/aiidalab_qe/app/configuration/__init__.py @@ -45,6 +45,11 @@ def __init__(self, **kwargs): (self.advanced_settings, "input_structure"), ) # + ipw.dlink( + (self, "input_structure"), + (self.workchain_settings, "input_structure"), + ) + # self.built_in_settings = [ self.workchain_settings, self.advanced_settings, diff --git a/src/aiidalab_qe/app/configuration/advanced.py b/src/aiidalab_qe/app/configuration/advanced.py index 2793d08e2..5df0e76cd 100644 --- a/src/aiidalab_qe/app/configuration/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced.py @@ -107,11 +107,7 @@ def __init__(self, default_protocol=None, **kwargs): style={"description_width": "initial"}, ) self.mesh_grid = ipw.HTML() - ipw.dlink( - (self.override, "value"), - (self.kpoints_distance, "disabled"), - lambda override: not override, - ) + self.create_kpoints_distance_link() self.kpoints_distance.observe(self._callback_value_set, "value") # Hubbard setting widget @@ -285,6 +281,20 @@ def __init__(self, default_protocol=None, **kwargs): # Default settings to trigger the callback self.reset() + def create_kpoints_distance_link(self): + """Create the dlink for override and kpoints_distance.""" + self.kpoints_distance_link = ipw.dlink( + (self.override, "value"), + (self.kpoints_distance, "disabled"), + lambda override: not override, + ) + + def remove_kpoints_distance_link(self): + """Remove the kpoints_distance_link.""" + if hasattr(self, "kpoints_distance_link"): + self.kpoints_distance_link.unlink() + del self.kpoints_distance_link + def _create_electron_maxstep_widgets(self): self.electron_maxstep = ipw.BoundedIntText( min=20, @@ -332,10 +342,22 @@ def _update_input_structure(self, change): self.hubbard_widget.update_widgets(change["new"]) if isinstance(self.input_structure, HubbardStructureData): self.override.value = True + if self.input_structure.pbc == (False, False, False): + self.kpoints_distance.value = 100.0 + self.kpoints_distance.disabled = True + if hasattr(self, "kpoints_distance_link"): + self.remove_kpoints_distance_link() + else: + # self.kpoints_distance.disabled = False + if not hasattr(self, "kpoints_distance_link"): + self.create_kpoints_distance_link() else: self.magnetization.input_structure = None self.pseudo_setter.structure = None self.hubbard_widget.update_widgets(None) + self.kpoints_distance.disabled = False + if not hasattr(self, "kpoints_distance_link"): + self.create_kpoints_distance_link() @tl.observe("electronic_type") def _electronic_type_changed(self, change): @@ -356,7 +378,14 @@ def _update_settings_from_protocol(self, protocol): parameters = PwBaseWorkChain.get_protocol_inputs(protocol) - self.kpoints_distance.value = parameters["kpoints_distance"] + if self.input_structure: + if self.input_structure.pbc == (False, False, False): + self.kpoints_distance.value = 100.0 + self.kpoints_distance.disabled = True + else: + self.kpoints_distance.value = parameters["kpoints_distance"] + else: + self.kpoints_distance.value = parameters["kpoints_distance"] num_atoms = len(self.input_structure.sites) if self.input_structure else 1 @@ -630,6 +659,10 @@ def reset(self): self.pseudo_setter._reset() else: self.pseudo_setter._reset() + if self.input_structure.pbc == (False, False, False): + self.kpoints_distance.value = 100.0 + self.kpoints_distance.disabled = True + # reset the magnetization self.magnetization.reset() # reset the hubbard widget diff --git a/src/aiidalab_qe/app/configuration/workflow.py b/src/aiidalab_qe/app/configuration/workflow.py index 408906ee5..cba0df521 100644 --- a/src/aiidalab_qe/app/configuration/workflow.py +++ b/src/aiidalab_qe/app/configuration/workflow.py @@ -4,7 +4,9 @@ """ import ipywidgets as ipw +import traitlets as tl +from aiida import orm from aiida_quantumespresso.common.types import RelaxType from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS from aiidalab_qe.app.utils import get_entry_items @@ -49,6 +51,8 @@ class WorkChainSettings(Panel): with less precision and the "precise" protocol to aim at best accuracy (at the price of longer/costlier calculations).""" ) + input_structure = tl.Instance(orm.StructureData, allow_none=True) + def __init__(self, **kwargs): # RelaxType: degrees of freedom in geometry optimization self.relax_type = ipw.ToggleButtons( @@ -141,6 +145,37 @@ def update_reminder_info(change, name=name): **kwargs, ) + @tl.observe("input_structure") + def _on_input_structure_change(self, change): + """Update the relax type options based on the input structure.""" + structure = change["new"] + if structure is None or structure.pbc != (False, False, False): + self.relax_type.options = [ + ("Structure as is", "none"), + ("Atomic positions", "positions"), + ("Full geometry", "positions_cell"), + ] + # Ensure the value is in the options + if self.relax_type.value not in [ + option[1] for option in self.relax_type.options + ]: + self.relax_type.value = "positions_cell" + + self.properties["bands"].run.disabled = False + elif structure.pbc == (False, False, False): + self.relax_type.options = [ + ("Structure as is", "none"), + ("Atomic positions", "positions"), + ] + # Ensure the value is in the options + if self.relax_type.value not in [ + option[1] for option in self.relax_type.options + ]: + self.relax_type.value = "positions" + + self.properties["bands"].run.value = False + self.properties["bands"].run.disabled = True + def get_panel_value(self): # Work chain settings relax_type = self.relax_type.value @@ -192,6 +227,7 @@ def set_panel_value(self, parameters): def reset(self): """Reset the panel to the default value.""" + self.input_structure = None for key in ["relax_type", "spin_type", "electronic_type"]: getattr(self, key).value = DEFAULT_PARAMETERS["workchain"][key] self.workchain_protocol.value = DEFAULT_PARAMETERS["workchain"]["protocol"] diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py index b02d4feb3..ab3d3d875 100644 --- a/src/aiidalab_qe/app/main.py +++ b/src/aiidalab_qe/app/main.py @@ -139,6 +139,7 @@ def _observe_process_selection(self, change): self.structure_step.structure = process.inputs.structure self.structure_step.confirm() self.submit_step.process = process + # set ui_parameters # print out error message if yaml format ui_parameters is not reachable ui_parameters = process.base.extras.get("ui_parameters", {}) diff --git a/src/aiidalab_qe/app/result/summary_viewer.py b/src/aiidalab_qe/app/result/summary_viewer.py index fca5db6a3..56fc24505 100644 --- a/src/aiidalab_qe/app/result/summary_viewer.py +++ b/src/aiidalab_qe/app/result/summary_viewer.py @@ -23,6 +23,7 @@ (True, True, True): "xyz", (True, True, False): "xy", (True, False, False): "x", + (False, False, False): "molecule", } VDW_CORRECTION_VERSION = { diff --git a/src/aiidalab_qe/common/widgets.py b/src/aiidalab_qe/common/widgets.py index 0d07650a2..5aaad1a7c 100644 --- a/src/aiidalab_qe/common/widgets.py +++ b/src/aiidalab_qe/common/widgets.py @@ -465,11 +465,7 @@ def __init__(self, title=""): layout={"width": "initial"}, ) self.periodicity = ipw.RadioButtons( - options=[ - "xyz", - "xy", - "x", - ], + options=["xyz", "xy", "x", "molecule"], value="xyz", description="Periodicty: ", layout={"width": "initial"}, @@ -613,6 +609,7 @@ def _select_periodicity(self, _=None): "xyz": (True, True, True), "xy": (True, True, False), "x": (True, False, False), + "molecule": (False, False, False), } new_structure = deepcopy(self.structure) new_structure.set_pbc(periodicity_options[self.periodicity.value]) diff --git a/src/aiidalab_qe/plugins/pdos/setting.py b/src/aiidalab_qe/plugins/pdos/setting.py index 60159d1b0..5ddc4c255 100644 --- a/src/aiidalab_qe/plugins/pdos/setting.py +++ b/src/aiidalab_qe/plugins/pdos/setting.py @@ -27,6 +27,8 @@ def __init__(self, **kwargs): self.description = ipw.HTML( """
By default, the tetrahedron method is used for PDOS calculation. If required you can apply Gaussian broadening with a custom degauss value. +
+ For molecules and systems with localized orbitals, it is recommended to use a custom degauss value.
""" ) # nscf kpoints setting widget @@ -87,6 +89,12 @@ def _procotol_changed(self, change): @tl.observe("input_structure") def _update_structure(self, _=None): self._display_mesh() + # For molecules this is compulsory + if self.input_structure and self.input_structure.pbc == (False, False, False): + self.nscf_kpoints_distance.value = 100 + self.nscf_kpoints_distance.disabled = True + self.use_pdos_degauss.value = True + self.use_pdos_degauss.disabled = True def _display_mesh(self, _=None): if self.input_structure is None: diff --git a/tests/configuration/test_advanced.py b/tests/configuration/test_advanced.py index 7891db223..0a177460c 100644 --- a/tests/configuration/test_advanced.py +++ b/tests/configuration/test_advanced.py @@ -95,6 +95,43 @@ def test_advanced_kpoints_settings(): assert w.value.get("kpoints_distance") == 0.5 +def test_advanced_molecule_settings(generate_structure_data): + """Test kpoint setting of advanced setting widget.""" + from aiidalab_qe.app.configuration.advanced import AdvancedSettings + + w = AdvancedSettings() + + # Check the disable of is bind to override switch + assert w.kpoints_distance.disabled is True + + w.override.value = True + assert w.kpoints_distance.disabled is False + + # create molecule + structure = generate_structure_data(name="H2O", pbc=(False, False, False)) + # Assign the molecule + w.input_structure = structure + + # Check override can not modify the kpoints_distance + assert w.kpoints_distance.disabled is True + w.override.value = True + assert w.kpoints_distance.disabled is True + + # Confirm the value of kpoints_distance is fixed + assert w.value.get("kpoints_distance") == 100.0 + + w.protocol = "fast" + assert w.value.get("kpoints_distance") == 100.0 + + # # Check reset + w.input_structure = None + w.reset() + + # # the protocol will not be reset + assert w.protocol == "fast" + assert w.value.get("kpoints_distance") == 0.5 + + def test_advanced_tot_charge_settings(): """Test TotCharge widget.""" from aiidalab_qe.app.configuration.advanced import AdvancedSettings diff --git a/tests/conftest.py b/tests/conftest.py index 20d9bae4e..844021450 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,6 +98,13 @@ def _generate_structure_data(name="silicon", pbc=(True, True, True)): structure.append_atom(position=(1.6, 0.92, 8.47), symbols="S") structure.append_atom(position=(1.6, 0.92, 11.6), symbols="S") + elif name == "H2O": + cell = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]] + structure = orm.StructureData(cell=cell) + structure.append_atom(position=(0.0, 0.0, 0.0), symbols="H") + structure.append_atom(position=(0.0, 0.0, 1.0), symbols="O") + structure.append_atom(position=(0.0, 1.0, 0.0), symbols="H") + structure.pbc = pbc return structure