Skip to content

Commit

Permalink
Implement consistent step blocker system (aiidalab#1171)
Browse files Browse the repository at this point in the history
This PR includes the following:
- Wizard-related widgets/models are moved to common/wizard.py
- `QeWizardStep` and corresponding model are extended by a confirmable and dependent (and both) flavors
- A new `HasBlockers` mixin is provided to encapsulate the relevant traits and functionality
- The confirmable wizard step flavor takes a confirmable wizard step model with the `Confirmable` and `HasBlockers` mixins
- Steps 1-3 are now confirmable steps
- Steps 2-4 remain dependent steps
- Steps 2-3 inherit both sets of functionality
- The SSSP installation widget is moved to step 1 and is blocking
- The QE install / code setup widget remains in step 3 and is blocking
- `HasBlockers` mixin is added to `SettingsModel` (panels in steps 2 and 3 can now block)

The design is flexible, modular, and extensible. Following the already-implemented use case of step 1, blockers could by introduced in step 2 covering invalid parameter combinations. Similarly in step 3, in addition to the QE setup blockers, additional blockers could be introduced covering invalid resource selection.
  • Loading branch information
edan-bainglass authored Feb 19, 2025
1 parent 6228414 commit 5d9618e
Show file tree
Hide file tree
Showing 21 changed files with 473 additions and 411 deletions.
55 changes: 18 additions & 37 deletions src/aiidalab_qe/app/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from __future__ import annotations

import ipywidgets as ipw
import traitlets as tl

from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.app.utils import get_entry_items
Expand All @@ -15,7 +14,7 @@
ConfigurationSettingsModel,
ConfigurationSettingsPanel,
)
from aiidalab_qe.common.widgets import QeDependentWizardStep
from aiidalab_qe.common.wizard import QeConfirmableDependentWizardStep

from .advanced import (
AdvancedConfigurationSettingsModel,
Expand All @@ -27,14 +26,18 @@
DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore


class ConfigureQeAppWorkChainStep(QeDependentWizardStep[ConfigurationStepModel]):
class ConfigureQeAppWorkChainStep(
QeConfirmableDependentWizardStep[ConfigurationStepModel]
):
missing_information_warning = "Missing input structure. Please set it first."

def __init__(self, model: ConfigurationStepModel, **kwargs):
super().__init__(model=model, **kwargs)
self._model.observe(
self._on_confirmation_change,
"confirmed",
super().__init__(
model=model,
confirm_kwargs={
"tooltip": "Confirm the currently selected settings and go to the next step",
},
**kwargs,
)
self._model.observe(
self._on_input_structure_change,
Expand Down Expand Up @@ -69,6 +72,8 @@ def __init__(self, model: ConfigurationStepModel, **kwargs):
self._fetch_plugin_calculation_settings()

def _render(self):
super()._render()

# RelaxType: degrees of freedom in geometry optimization
self.relax_type_help = ipw.HTML()
ipw.dlink(
Expand Down Expand Up @@ -115,22 +120,7 @@ def _render(self):
self.sub_steps.set_title(0, "Step 2.1: Select which properties to calculate")
self.sub_steps.set_title(1, "Step 2.2: Customize calculation parameters")

self.confirm_button = ipw.Button(
description="Confirm",
tooltip="Confirm the currently selected settings and go to the next step.",
button_style="success",
icon="check-circle",
disabled=True,
layout=ipw.Layout(width="auto"),
)
ipw.dlink(
(self, "state"),
(self.confirm_button, "disabled"),
lambda state: state != self.State.CONFIGURED,
)
self.confirm_button.on_click(self.confirm)

self.children = [
self.content.children = [
InAppGuide(identifier="configuration-step"),
ipw.HTML("""
<div style="padding-top: 0px; padding-bottom: 0px">
Expand All @@ -140,28 +130,22 @@ def _render(self):
self.relax_type_help,
self.relax_type,
self.sub_steps,
self.confirm_button,
]

self.children = [
self.content,
self.confirm_box,
]

def _post_render(self):
self._update_tabs()

def is_saved(self):
return self._model.confirmed

def confirm(self, _=None):
self._model.confirm()

def reset(self):
self._model.reset()
if self.rendered:
self.sub_steps.selected_index = None
self.tabs.selected_index = 0

@tl.observe("previous_step_state")
def _on_previous_step_state_change(self, _):
self._update_state()

def _on_tab_change(self, change):
if (tab_index := change["new"]) is None:
return
Expand All @@ -173,9 +157,6 @@ def _on_input_structure_change(self, _):
self._model.update()
self.reset()

def _on_confirmation_change(self, _):
self._update_state()

def _update_tabs(self):
children = []
titles = []
Expand Down
6 changes: 2 additions & 4 deletions src/aiidalab_qe/app/configuration/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@
from aiida_quantumespresso.common.types import RelaxType
from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.common.mixins import (
Confirmable,
HasInputStructure,
HasModels,
)
from aiidalab_qe.common.panel import ConfigurationSettingsModel
from aiidalab_qe.common.widgets import QeWizardStepModel
from aiidalab_qe.common.wizard import QeConfirmableWizardStepModel

DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore

NO_RELAXATION_OPTION = ("Structure as is", "none")


class ConfigurationStepModel(
QeWizardStepModel,
QeConfirmableWizardStepModel,
HasModels[ConfigurationSettingsModel],
HasInputStructure,
Confirmable,
):
identifier = "configuration"

Expand Down
6 changes: 3 additions & 3 deletions src/aiidalab_qe/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ class QeApp:
def __init__(
self,
process=None,
qe_auto_setup=True,
auto_setup=True,
bug_report_url=DEFAULT_BUG_REPORT_URL,
show_log=False,
):
"""Initialize the AiiDAlab QE application with the necessary setup."""

self.process = process
self.qe_auto_setup = qe_auto_setup
self.auto_setup = auto_setup
self.log_widget = None

self._load_styles()
Expand Down Expand Up @@ -83,7 +83,7 @@ def _load_styles(self):
def load(self):
"""Initialize the WizardApp and integrate the app into the main view."""
self.app = WizardApp(
qe_auto_setup=self.qe_auto_setup,
auto_setup=self.auto_setup,
log_widget=self.log_widget,
)
self.view.main.children = [self.app]
Expand Down
3 changes: 2 additions & 1 deletion src/aiidalab_qe/app/result/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

from aiida.engine import ProcessState
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.widgets import LoadingWidget, QeDependentWizardStep
from aiidalab_qe.common.widgets import LoadingWidget
from aiidalab_qe.common.wizard import QeDependentWizardStep
from aiidalab_widgets_base import ProcessMonitor, WizardAppWidgetStep

from .components import ResultsComponent
Expand Down
2 changes: 1 addition & 1 deletion src/aiidalab_qe/app/result/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from aiida import orm
from aiida.engine.processes import control
from aiidalab_qe.common.mixins import HasModels, HasProcess
from aiidalab_qe.common.widgets import QeWizardStepModel
from aiidalab_qe.common.wizard import QeWizardStepModel

from .components import ResultsComponentModel

Expand Down
5 changes: 5 additions & 0 deletions src/aiidalab_qe/app/static/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ footer {
padding: 0 5px;
margin: -1px;
}

.blocker-messages .alert {
margin-bottom: 0;
padding: 10px;
}
102 changes: 63 additions & 39 deletions src/aiidalab_qe/app/structure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
ShakeNBreakEditor,
)
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.widgets import CategorizedStructureExamplesWidget, QeWizardStep
from aiidalab_qe.common.widgets import CategorizedStructureExamplesWidget
from aiidalab_qe.common.wizard import QeConfirmableWizardStep
from aiidalab_widgets_base import (
BasicCellEditor,
BasicStructureEditor,
StructureManagerWidget,
StructureUploadWidget,
)

# The Examples list of (name, file) tuple curretly passed to
# The Examples list of (name, file) tuple currently passed to
# StructureExamplesWidget.
file_path = pathlib.Path(__file__).parent
Examples = [
Expand All @@ -39,33 +40,46 @@
]


class StructureSelectionStep(QeWizardStep[StructureStepModel]):
class StructureSelectionStep(QeConfirmableWizardStep[StructureStepModel]):
"""Integrated widget for the selection and edition of structure.
The widget includes a structure manager that allows to select a structure
from different sources. It also includes the structure editor. Both the
structure importers and the structure editors can be extended by plugins.
"""

def __init__(self, model: StructureStepModel, **kwargs):
super().__init__(model=model, **kwargs)
def __init__(self, model: StructureStepModel, auto_setup=True, **kwargs):
super().__init__(
model=model,
confirm_kwargs={
"tooltip": "Confirm the currently selected structure and go to the next step",
},
**kwargs,
)
self._model.observe(
self._on_installation_change,
["installing_sssp", "sssp_installed"],
)
self._model.observe(
self._on_confirmation_change,
"confirmed",
self._on_sssp_installed,
"sssp_installed",
)
self._model.observe(
self._on_input_structure_change,
"input_structure",
)
self._install_sssp(auto_setup)

def _render(self):
super()._render()

examples_by_category = {"Simple crystals": Examples}
plugin_structure_examples = {
item["title"]: item["structures"]
for item in get_entry_items(
"aiidalab_qe.properties", "structure_examples"
).values()
}
examples_by_category.update(plugin_structure_examples)
examples_by_category |= plugin_structure_examples

importers = [
StructureUploadWidget(title="Upload file"),
Expand Down Expand Up @@ -129,27 +143,7 @@ def _render(self):
(self.structure_name_text, "value"),
)

self.confirm_button = ipw.Button(
description="Confirm",
tooltip="Confirm the currently selected structure and go to the next step.",
button_style="success",
icon="check-circle",
layout=ipw.Layout(width="auto"),
)
ipw.dlink(
(self, "state"),
(self.confirm_button, "disabled"),
lambda state: state != self.State.CONFIGURED,
)
self.confirm_button.on_click(self.confirm)

self.message_area = ipw.HTML()
ipw.dlink(
(self._model, "message_area"),
(self.message_area, "value"),
)

self.children = [
self.content.children = [
InAppGuide(identifier="structure-step"),
ipw.HTML("""
<p>
Expand All @@ -161,33 +155,63 @@ def _render(self):
"""),
self.manager,
self.structure_name_text,
self.message_area,
self.confirm_button,
]
# after rendering the widget, nglview needs to be resized

self.confirm_box.children += (self.sssp_installation,)

self.children = [
self.content,
self.confirm_box,
]

def _post_render(self):
# After rendering the widget, nglview needs to be resized
# to properly display the structure
self.manager.viewer._viewer.handle_resize()

def is_saved(self):
return self._model.confirmed

def confirm(self, _=None):
self.manager.store_structure()
self._model.message_area = ""
self._model.confirm()
super().confirm()

def can_reset(self):
return self._model.confirmed

def reset(self):
self._model.reset()

def _on_installation_change(self, _):
self._model.update_blockers()

def _on_sssp_installed(self, _):
self._toggle_sssp_installation_widget()

def _on_input_structure_change(self, _):
self._model.update_widget_text()
self._update_state()

def _on_confirmation_change(self, _):
self._update_state()
def _install_sssp(self, auto_setup):
from aiidalab_qe.common.setup_pseudos import PseudosInstallWidget

self.sssp_installation = PseudosInstallWidget(auto_start=False)
ipw.dlink(
(self.sssp_installation, "busy"),
(self._model, "installing_sssp"),
)
ipw.dlink(
(self.sssp_installation, "installed"),
(self._model, "installing_sssp"),
lambda installed: not installed,
)
ipw.dlink(
(self.sssp_installation, "installed"),
(self._model, "sssp_installed"),
)
if auto_setup:
self.sssp_installation.refresh()

def _toggle_sssp_installation_widget(self):
sssp_installation_display = "none" if self._model.sssp_installed else "block"
self.sssp_installation.layout.display = sssp_installation_display

def _update_state(self):
if self._model.confirmed:
Expand Down
Loading

0 comments on commit 5d9618e

Please sign in to comment.