diff --git a/qe.ipynb b/qe.ipynb
index 6f3e977e0..a4153e027 100644
--- a/qe.ipynb
+++ b/qe.ipynb
@@ -56,8 +56,6 @@
"metadata": {},
"outputs": [],
"source": [
- "from IPython.display import display\n",
- "\n",
"from aiidalab_qe.app.wrapper import AppWrapperContoller, AppWrapperModel, AppWrapperView\n",
"\n",
"model = AppWrapperModel()\n",
diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py
index d27fd74f4..3e8c108c5 100644
--- a/src/aiidalab_qe/app/main.py
+++ b/src/aiidalab_qe/app/main.py
@@ -70,9 +70,14 @@ def __init__(self, qe_auto_setup=True):
self._on_configuration_confirmation_change,
"confirmed",
)
- self.submit_model.observe(
- self._on_submission,
- "confirmed",
+ ipw.dlink(
+ (self.submit_step, "state"),
+ (self.results_step, "previous_step_state"),
+ )
+ ipw.dlink(
+ (self.submit_model, "process_node"),
+ (self.results_model, "process_uuid"),
+ lambda node: node.uuid if node is not None else None,
)
# Add the application steps to the application
@@ -145,9 +150,6 @@ def _on_configuration_confirmation_change(self, _):
self._update_submission_step()
self._update_blockers()
- def _on_submission(self, _):
- self._update_results_step()
-
def _render_step(self, step_index):
step = self.steps[step_index][1]
step.render()
@@ -166,10 +168,6 @@ def _update_submission_step(self):
self.submit_model.input_structure = None
self.submit_model.input_parameters = {}
- def _update_results_step(self):
- node = self.submit_model.process_node
- self.results_model.process_uuid = node.uuid if node is not None else None
-
def _update_blockers(self):
self.submit_model.external_submission_blockers = [
f"Unsaved changes in the {title} step. Please confirm the changes before submitting."
diff --git a/src/aiidalab_qe/app/result/__init__.py b/src/aiidalab_qe/app/result/__init__.py
index 386f7cd0a..b5590d756 100644
--- a/src/aiidalab_qe/app/result/__init__.py
+++ b/src/aiidalab_qe/app/result/__init__.py
@@ -1,19 +1,16 @@
import ipywidgets as ipw
import traitlets as tl
-from aiida import orm
from aiida.engine import ProcessState
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.widgets import LoadingWidget
-from aiidalab_widgets_base import (
- ProcessMonitor,
- ProcessNodesTreeWidget,
- WizardAppWidgetStep,
-)
-from aiidalab_widgets_base.viewers import viewer as node_viewer
+from aiidalab_widgets_base import ProcessMonitor, WizardAppWidgetStep
+from .components import ResultsComponent
+from .components.status import WorkChainStatusModel, WorkChainStatusPanel
+from .components.summary import WorkChainSummary, WorkChainSummaryModel
+from .components.viewer import WorkChainResultsViewer, WorkChainResultsViewerModel
from .model import ResultsStepModel
-from .viewer import WorkChainViewer, WorkChainViewerModel
PROCESS_COMPLETED = "
Workflow completed successfully!
"
PROCESS_EXCEPTED = "Workflow is excepted!
"
@@ -21,6 +18,8 @@
class ViewQeAppWorkChainStatusAndResultsStep(ipw.VBox, WizardAppWidgetStep):
+ previous_step_state = tl.UseEnum(WizardAppWidgetStep.State)
+
def __init__(self, model: ResultsStepModel, **kwargs):
super().__init__(
children=[LoadingWidget("Loading results step")],
@@ -28,6 +27,10 @@ def __init__(self, model: ResultsStepModel, **kwargs):
)
self._model = model
+ self.observe(
+ self._on_previous_step_state_change,
+ "previous_step_state",
+ )
self._model.observe(
self._on_process_change,
"process_uuid",
@@ -35,10 +38,6 @@ def __init__(self, model: ResultsStepModel, **kwargs):
self.rendered = False
- self.node_views = {} # node-view cache
-
- self.node_view_loading_message = LoadingWidget("Loading node view")
-
def render(self):
if self.rendered:
return
@@ -57,15 +56,6 @@ def render(self):
)
self.kill_button.on_click(self._on_kill_button_click)
- self.update_results_button = ipw.Button(
- description="Update results",
- tooltip="Trigger the update of the results.",
- button_style="success",
- icon="refresh",
- layout=ipw.Layout(width="auto", display="block"),
- )
- self.update_results_button.on_click(self._on_update_results_button_click)
-
self.clean_scratch_button = ipw.Button(
description="Clean remote data",
tooltip="Clean the remote folders of the workchain.",
@@ -85,48 +75,75 @@ def render(self):
(self.process_info, "value"),
)
- self.process_tree = ProcessNodesTreeWidget()
- self.process_tree.observe(
- self._on_node_selection_change,
- "selected_nodes",
+ summary_model = WorkChainSummaryModel()
+ self.summary_panel = WorkChainSummary(model=summary_model)
+ self._model.add_model("summary", summary_model)
+
+ results_model = WorkChainResultsViewerModel()
+ self.results_panel = WorkChainResultsViewer(model=results_model)
+ self._model.add_model("results", results_model)
+
+ status_model = WorkChainStatusModel()
+ self.status_panel = WorkChainStatusPanel(model=status_model)
+ self._model.add_model("status", status_model)
+
+ self.panels = {
+ "Summary": self.summary_panel,
+ "Results": self.results_panel,
+ "Status": self.status_panel,
+ }
+
+ self.toggle_controls = ipw.ToggleButtons(
+ options=[*self.panels.keys()],
+ tooltips=[
+ "A summary of calculation parameters",
+ "The calculation results",
+ "A detailed progress status of the workflow",
+ ],
+ icons=[
+ "file-text-o",
+ "bar-chart",
+ "tasks",
+ ],
+ value=None,
)
- ipw.dlink(
- (self._model, "process_uuid"),
- (self.process_tree, "value"),
+ self.toggle_controls.add_class("results-step-toggles")
+ self.toggle_controls.observe(
+ self._on_toggle_change,
+ "value",
)
- self.node_view_container = ipw.VBox()
-
- self.process_monitor = ProcessMonitor(
- timeout=0.2,
- callbacks=[
- self.process_tree.update,
- self._update_status,
- self._update_state,
+ self.container = ipw.VBox(
+ children=[
+ self.status_panel,
],
)
- self.children = [
- InAppGuide(identifier="results-step"),
- self.process_info,
- ipw.HBox(
- children=[
- self.kill_button,
- self.update_results_button,
- self.clean_scratch_button,
- ],
- layout=ipw.Layout(margin="0 3px"),
- ),
- self.process_tree,
- self.node_view_container,
- ]
+ if self._model.has_process:
+ self._update_children()
+ elif self.previous_step_state is not WizardAppWidgetStep.State.SUCCESS:
+ self.children = [
+ ipw.HTML("""
+
+ No process detected. Please submit a calculation.
+
+ """),
+ ]
self.rendered = True
self._update_kill_button_layout()
self._update_clean_scratch_button_layout()
- # This triggers the start of the monitor on a separate threadF
+ self.toggle_controls.value = "Summary"
+
+ self.process_monitor = ProcessMonitor(
+ timeout=0.2,
+ callbacks=[
+ self._update_status,
+ self._update_state,
+ ],
+ )
ipw.dlink(
(self._model, "process_uuid"),
(self.process_monitor, "value"),
@@ -143,68 +160,54 @@ def reset(self):
def _on_state_change(self, _):
self._update_kill_button_layout()
+ def _on_previous_step_state_change(self, _):
+ if self.previous_step_state is WizardAppWidgetStep.State.SUCCESS:
+ process_node = self._model.fetch_process_node()
+ message = (
+ "Loading results"
+ if process_node and process_node.is_finished
+ else "Submitting calculation"
+ )
+ self.children = [LoadingWidget(message)]
+
+ def _on_toggle_change(self, change):
+ panel = self.panels[change["new"]]
+ self._toggle_view(panel)
+
def _on_process_change(self, _):
+ if self.rendered:
+ self._update_children()
self._model.update()
self._update_state()
self._update_kill_button_layout()
self._update_clean_scratch_button_layout()
- def _on_node_selection_change(self, change):
- self._update_node_view(change["new"])
-
def _on_kill_button_click(self, _):
self._model.kill_process()
self._update_kill_button_layout()
- def _on_update_results_button_click(self, _):
- self._update_node_view(self.process_tree.selected_nodes, refresh=True)
-
def _on_clean_scratch_button_click(self, _):
self._model.clean_remote_data()
self._update_clean_scratch_button_layout()
- def _update_node_view(self, nodes, refresh=False):
- """Update the node view based on the selected nodes.
-
- parameters
- ----------
- `nodes`: `list`
- List of selected nodes.
- `refresh`: `bool`, optional
- If True, the viewer will be refreshed.
- Occurs when user presses the "Update results" button.
- """
-
- if not nodes:
- return
- # only show the first selected node
- node = nodes[0]
-
- # check if the viewer is already added
- if node.uuid in self.node_views and not refresh:
- self.node_view = self.node_views[node.uuid]
- elif not isinstance(node, orm.WorkChainNode):
- self.node_view_container.children = [self.node_view_loading_message]
- self.node_view = node_viewer(node)
- self.node_views[node.uuid] = self.node_view
- elif node.process_label == "QeAppWorkChain":
- self.node_view_container.children = [self.node_view_loading_message]
- self.node_view = self._create_workchain_viewer(node)
- self.node_views[node.uuid] = self.node_view
- else:
- self.node_view = ipw.HTML("No viewer available for this node.")
-
- self.node_view_container.children = [self.node_view]
+ def _update_children(self):
+ self.children = [
+ InAppGuide(identifier="results-step"),
+ self.process_info,
+ ipw.HBox(
+ children=[
+ self.kill_button,
+ self.clean_scratch_button,
+ ],
+ layout=ipw.Layout(margin="0 3px"),
+ ),
+ self.toggle_controls,
+ self.container,
+ ]
- def _create_workchain_viewer(self, node: orm.WorkChainNode):
- model = WorkChainViewerModel()
- ipw.dlink(
- (self._model, "monitor_counter"),
- (model, "monitor_counter"),
- )
- node_view: WorkChainViewer = node_viewer(node, model=model) # type: ignore
- node_view.render()
- return node_view
+ def _toggle_view(self, panel: ResultsComponent):
+ self.container.children = [panel]
+ panel.render()
def _update_kill_button_layout(self):
if not self.rendered:
diff --git a/src/aiidalab_qe/app/result/components/__init__.py b/src/aiidalab_qe/app/result/components/__init__.py
new file mode 100644
index 000000000..e15937f18
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/__init__.py
@@ -0,0 +1,55 @@
+import typing as t
+
+import ipywidgets as ipw
+
+from aiidalab_qe.common.mixins import HasProcess
+from aiidalab_qe.common.mvc import Model
+from aiidalab_qe.common.widgets import LoadingWidget
+
+
+class ResultsComponentModel(Model, HasProcess):
+ identifier = "results"
+
+
+RCM = t.TypeVar("RCM", bound=ResultsComponentModel)
+
+
+class ResultsComponent(ipw.VBox, t.Generic[RCM]):
+ def __init__(self, model: RCM, **kwargs):
+ self.loading_message = LoadingWidget(f"Loading {model.identifier}")
+
+ super().__init__(
+ children=[self.loading_message],
+ **kwargs,
+ )
+
+ self._model = model
+ self._model.observe(
+ self._on_process_change,
+ "process_uuid",
+ )
+ self._model.observe(
+ self._on_monitor_counter_change,
+ "monitor_counter",
+ )
+
+ self.rendered = False
+
+ def render(self):
+ if self.rendered:
+ return
+ self._render()
+ self.rendered = True
+ self._post_render()
+
+ def _on_process_change(self, _):
+ pass
+
+ def _on_monitor_counter_change(self, _):
+ pass
+
+ def _render(self):
+ raise NotImplementedError
+
+ def _post_render(self):
+ pass
diff --git a/src/aiidalab_qe/app/result/components/status/__init__.py b/src/aiidalab_qe/app/result/components/status/__init__.py
new file mode 100644
index 000000000..fce38ce52
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/status/__init__.py
@@ -0,0 +1,7 @@
+from .model import WorkChainStatusModel
+from .status import WorkChainStatusPanel
+
+__all__ = [
+ "WorkChainStatusModel",
+ "WorkChainStatusPanel",
+]
diff --git a/src/aiidalab_qe/app/result/components/status/model.py b/src/aiidalab_qe/app/result/components/status/model.py
new file mode 100644
index 000000000..e644a4319
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/status/model.py
@@ -0,0 +1,5 @@
+from aiidalab_qe.app.result.components import ResultsComponentModel
+
+
+class WorkChainStatusModel(ResultsComponentModel):
+ identifier = "workflow process status monitors"
diff --git a/src/aiidalab_qe/app/result/components/status/status.py b/src/aiidalab_qe/app/result/components/status/status.py
new file mode 100644
index 000000000..30530bd8b
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/status/status.py
@@ -0,0 +1,91 @@
+import ipywidgets as ipw
+
+from aiida import orm
+from aiidalab_qe.app.result.components import ResultsComponent
+from aiidalab_qe.common.widgets import LoadingWidget
+from aiidalab_widgets_base import ProcessNodesTreeWidget
+from aiidalab_widgets_base.viewers import viewer as node_viewer
+
+from .model import WorkChainStatusModel
+
+
+class WorkChainStatusPanel(ResultsComponent[WorkChainStatusModel]):
+ def __init__(self, model: WorkChainStatusModel, **kwargs):
+ super().__init__(model=model, **kwargs)
+ self.node_views = {} # node-view cache
+ self.node_view_loading_message = LoadingWidget("Loading node view")
+
+ def _on_monitor_counter_change(self, _):
+ self._update_process_tree()
+
+ def _on_node_selection_change(self, change):
+ self._update_node_view(change["new"])
+
+ def _render(self):
+ self.simplified_status_view = ipw.HTML("Coming soon")
+
+ self.process_tree = ProcessNodesTreeWidget()
+ self.process_tree.observe(
+ self._on_node_selection_change,
+ "selected_nodes",
+ )
+ ipw.dlink(
+ (self._model, "process_uuid"),
+ (self.process_tree, "value"),
+ )
+
+ self.node_view_container = ipw.VBox()
+
+ self.accordion = ipw.Accordion(
+ children=[
+ self.simplified_status_view,
+ ipw.VBox(
+ children=[
+ self.process_tree,
+ self.node_view_container,
+ ],
+ ),
+ ],
+ selected_index=1,
+ )
+ titles = [
+ "Status overview",
+ "Advanced status view",
+ ]
+ for i, title in enumerate(titles):
+ self.accordion.set_title(i, title)
+
+ self.children = [self.accordion]
+
+ def _update_process_tree(self):
+ if self.rendered:
+ self.process_tree.update()
+
+ def _update_node_view(self, nodes, refresh=False):
+ """Update the node view based on the selected nodes.
+
+ parameters
+ ----------
+ `nodes`: `list`
+ List of selected nodes.
+ `refresh`: `bool`, optional
+ If True, the viewer will be refreshed.
+ Occurs when user presses the "Update results" button.
+ """
+
+ if not nodes:
+ return
+ # only show the first selected node
+ node = nodes[0]
+
+ # check if the viewer is already added
+ if node.uuid in self.node_views and not refresh:
+ self.node_view = self.node_views[node.uuid]
+ elif not isinstance(node, orm.WorkChainNode):
+ self.node_view_container.children = [self.node_view_loading_message]
+ self.node_view = node_viewer(node)
+ self.node_views[node.uuid] = self.node_view
+ else:
+ self.node_view = ipw.HTML("No viewer available for this node.")
+
+ self.node_view_container.children = [self.node_view]
diff --git a/src/aiidalab_qe/app/result/components/summary/__init__.py b/src/aiidalab_qe/app/result/components/summary/__init__.py
new file mode 100644
index 000000000..65236b9e8
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/summary/__init__.py
@@ -0,0 +1,7 @@
+from .model import WorkChainSummaryModel
+from .summary import WorkChainSummary
+
+__all__ = [
+ "WorkChainSummary",
+ "WorkChainSummaryModel",
+]
diff --git a/src/aiidalab_qe/app/result/utils/download_data.py b/src/aiidalab_qe/app/result/components/summary/download_data.py
similarity index 98%
rename from src/aiidalab_qe/app/result/utils/download_data.py
rename to src/aiidalab_qe/app/result/components/summary/download_data.py
index ae276c437..7478ee35c 100644
--- a/src/aiidalab_qe/app/result/utils/download_data.py
+++ b/src/aiidalab_qe/app/result/components/summary/download_data.py
@@ -81,7 +81,7 @@ def _download_data(self, button_instance):
Args:
button_instance (ipywidgets.Button): The button instance that was clicked.
"""
-
+ button_instance.disabled = True
if "archive" in button_instance.description:
what = "archive"
filename = f"export_qeapp_calculation_pk_{self.node.pk}.aiida"
@@ -97,6 +97,7 @@ def _download_data(self, button_instance):
self._download(payload=data, filename=filename)
del data
box.children = box.children[:1]
+ button_instance.disabled = False
@staticmethod
def _download(payload, filename):
diff --git a/src/aiidalab_qe/app/result/summary/model.py b/src/aiidalab_qe/app/result/components/summary/model.py
similarity index 96%
rename from src/aiidalab_qe/app/result/summary/model.py
rename to src/aiidalab_qe/app/result/components/summary/model.py
index 900ab8fd8..74b1b635a 100644
--- a/src/aiidalab_qe/app/result/summary/model.py
+++ b/src/aiidalab_qe/app/result/components/summary/model.py
@@ -1,5 +1,5 @@
from aiida_quantumespresso.workflows.pw.bands import PwBandsWorkChain
-from aiidalab_qe.common.panel import ResultsModel
+from aiidalab_qe.app.result.components import ResultsComponentModel
FUNCTIONAL_LINK_MAP = {
"PBE": "https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.77.3865",
@@ -35,15 +35,15 @@
}
-class WorkChainSummaryResultsModel(ResultsModel):
- identifier = "summary"
+class WorkChainSummaryModel(ResultsComponentModel):
+ identifier = "workflow summary"
@property
def include(self):
return True
def generate_report_html(self):
- """Read from the bulider parameters and generate a html for reporting
+ """Read from the builder parameters and generate a html for reporting
the inputs for the `QeAppWorkChain`.
"""
from importlib.resources import files
@@ -125,8 +125,7 @@ def _generate_report_parameters(self):
"""
from aiida.orm.utils.serialize import deserialize_unsafe
- if not (qeapp_wc := self.fetch_process_node()):
- return {"error": "WorkChain not found."}
+ qeapp_wc = self.fetch_process_node()
ui_parameters = qeapp_wc.base.extras.get("ui_parameters", {})
if isinstance(ui_parameters, str):
diff --git a/src/aiidalab_qe/app/result/viewer/outputs.py b/src/aiidalab_qe/app/result/components/summary/outputs.py
similarity index 93%
rename from src/aiidalab_qe/app/result/viewer/outputs.py
rename to src/aiidalab_qe/app/result/components/summary/outputs.py
index b2cabd47e..3cedb8c13 100644
--- a/src/aiidalab_qe/app/result/viewer/outputs.py
+++ b/src/aiidalab_qe/app/result/components/summary/outputs.py
@@ -16,7 +16,7 @@
from aiida.common import LinkType
from aiidalab_qe.app.static import styles, templates
-from ..utils.download_data import DownloadDataWidget
+from .download_data import DownloadDataWidget
class WorkChainOutputs(ipw.VBox):
@@ -38,11 +38,6 @@ def __init__(self, node, export_dir=None, **kwargs):
Creating archive...
"""
)
- self._download_archive_button = ipw.Button(
- description="Download output",
- icon="download",
- )
- self._download_archive_button.on_click(self._download_archive)
self._download_button_widget = DownloadDataWidget(workchain_node=self.node)
if node.exit_status != 0:
@@ -79,14 +74,6 @@ def __init__(self, node, export_dir=None, **kwargs):
def _default_busy(self):
return False
- @tl.observe("_busy")
- def _observe_busy(self, change):
- self._download_button_container.children = [
- self._create_archive_indicator
- if change["new"]
- else self._download_archive_button
- ]
-
def _download_archive(self, _):
fn_archive = self.export_dir.joinpath(str(self.node.uuid)).with_suffix(".zip")
fn_lockfile = fn_archive.with_suffix(".lock")
diff --git a/src/aiidalab_qe/app/result/components/summary/summary.py b/src/aiidalab_qe/app/result/components/summary/summary.py
new file mode 100644
index 000000000..aab7390da
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/summary/summary.py
@@ -0,0 +1,37 @@
+import ipywidgets as ipw
+
+from aiidalab_qe.app.result.components import ResultsComponent
+
+from .model import WorkChainSummaryModel
+from .outputs import WorkChainOutputs
+
+
+class WorkChainSummary(ResultsComponent[WorkChainSummaryModel]):
+ def __init__(self, model: WorkChainSummaryModel, **kwargs):
+ super().__init__(model=model, **kwargs)
+ self.has_report = False
+ self.has_output = False
+
+ def _on_process_change(self, _):
+ if not self.has_report:
+ self._render_summary()
+
+ def _on_monitor_counter_change(self, _):
+ if not self.has_output:
+ self._render_output()
+
+ def _render(self):
+ self._render_summary()
+
+ def _render_summary(self):
+ if not self._model.has_process:
+ return
+ report = self._model.generate_report_html()
+ self.children = [ipw.HTML(report)]
+ self.has_report = True
+
+ def _render_output(self):
+ process_node = self._model.fetch_process_node()
+ if process_node and process_node.is_terminated:
+ self.children += (WorkChainOutputs(node=process_node),)
+ self.has_output = True
diff --git a/src/aiidalab_qe/app/result/components/viewer/__init__.py b/src/aiidalab_qe/app/result/components/viewer/__init__.py
new file mode 100644
index 000000000..e9ab4b320
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/viewer/__init__.py
@@ -0,0 +1,7 @@
+from .model import WorkChainResultsViewerModel
+from .viewer import WorkChainResultsViewer
+
+__all__ = [
+ "WorkChainResultsViewer",
+ "WorkChainResultsViewerModel",
+]
diff --git a/src/aiidalab_qe/app/result/components/viewer/model.py b/src/aiidalab_qe/app/result/components/viewer/model.py
new file mode 100644
index 000000000..f43f1639e
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/viewer/model.py
@@ -0,0 +1,22 @@
+import ipywidgets as ipw
+
+from aiidalab_qe.app.result.components import ResultsComponentModel
+from aiidalab_qe.common.mixins import HasModels
+from aiidalab_qe.common.panel import ResultsModel
+
+
+class WorkChainResultsViewerModel(
+ ResultsComponentModel,
+ HasModels[ResultsModel],
+):
+ identifier = "workflow results"
+
+ def _link_model(self, model: ResultsModel):
+ ipw.dlink(
+ (self, "process_uuid"),
+ (model, "process_uuid"),
+ )
+ ipw.dlink(
+ (self, "monitor_counter"),
+ (model, "monitor_counter"),
+ )
diff --git a/src/aiidalab_qe/app/result/structure/__init__.py b/src/aiidalab_qe/app/result/components/viewer/structure/__init__.py
similarity index 100%
rename from src/aiidalab_qe/app/result/structure/__init__.py
rename to src/aiidalab_qe/app/result/components/viewer/structure/__init__.py
diff --git a/src/aiidalab_qe/app/result/structure/model.py b/src/aiidalab_qe/app/result/components/viewer/structure/model.py
similarity index 100%
rename from src/aiidalab_qe/app/result/structure/model.py
rename to src/aiidalab_qe/app/result/components/viewer/structure/model.py
diff --git a/src/aiidalab_qe/app/result/components/viewer/structure/structure.py b/src/aiidalab_qe/app/result/components/viewer/structure/structure.py
new file mode 100644
index 000000000..09b40faaa
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/viewer/structure/structure.py
@@ -0,0 +1,21 @@
+from aiidalab_qe.common.panel import ResultsPanel
+from aiidalab_widgets_base.viewers import StructureDataViewer
+
+from .model import StructureResultsModel
+
+
+class StructureResults(ResultsPanel[StructureResultsModel]):
+ title = "Final Geometry"
+ identifier = "structure"
+
+ def _render(self):
+ if not hasattr(self, "widget"):
+ self.widget = StructureDataViewer(structure=self._model.outputs.structure)
+ self.children = [self.widget]
+
+ # HACK to resize the NGL viewer in cases where it auto-rendered when its
+ # container was not displayed, which leads to a null width. This hack restores
+ # the original dimensions.
+ ngl = self.widget._viewer
+ ngl._set_size("100%", "300px")
+ ngl.control.zoom(0.0)
diff --git a/src/aiidalab_qe/app/result/components/viewer/viewer.py b/src/aiidalab_qe/app/result/components/viewer/viewer.py
new file mode 100644
index 000000000..8490719ad
--- /dev/null
+++ b/src/aiidalab_qe/app/result/components/viewer/viewer.py
@@ -0,0 +1,104 @@
+from __future__ import annotations
+
+import ipywidgets as ipw
+
+from aiidalab_qe.app.result.components import ResultsComponent
+from aiidalab_qe.app.utils import get_entry_items
+from aiidalab_qe.common.panel import ResultsPanel
+
+from .model import WorkChainResultsViewerModel
+from .structure import StructureResults, StructureResultsModel
+
+
+class WorkChainResultsViewer(ResultsComponent[WorkChainResultsViewerModel]):
+ def __init__(self, model: WorkChainResultsViewerModel, **kwargs):
+ super().__init__(model=model, **kwargs)
+ self.panels: dict[str, ResultsPanel] = {}
+ self._fetch_plugin_results()
+
+ def _on_process_change(self, _):
+ self._update_panels()
+ if self.rendered:
+ self._set_tabs()
+
+ def _on_tab_change(self, change):
+ if (tab_index := change["new"]) is None:
+ return
+ tab: ResultsPanel = self.tabs.children[tab_index] # type: ignore
+ tab.render()
+
+ def _render(self):
+ if node := self._model.fetch_process_node():
+ formula = node.inputs.structure.get_formula()
+ title = f"\nQE App Workflow (pk: {node.pk}) — {formula}
"
+ else:
+ title = "\nQE App Workflow
"
+
+ self.title = ipw.HTML(title)
+
+ self.tabs = ipw.Tab(selected_index=None)
+ self.tabs.observe(
+ self._on_tab_change,
+ "selected_index",
+ )
+
+ # TODO consider refactoring structure relaxation panel as a plugin
+ if "relax" in self._model.properties:
+ self._add_structure_panel()
+
+ self.children = [
+ self.title,
+ self.tabs,
+ ]
+
+ def _post_render(self):
+ self._set_tabs()
+
+ def _update_panels(self):
+ properties = self._model.properties
+ need_electronic_structure = "bands" in properties and "pdos" in properties
+ self.panels = {
+ identifier: panel
+ for identifier, panel in self.panels.items()
+ if identifier in properties
+ or (identifier == "electronic_structure" and need_electronic_structure)
+ }
+
+ def _set_tabs(self):
+ children = []
+ titles = []
+ for results in self.panels.values():
+ titles.append(results.title)
+ children.append(results)
+ self.tabs.children = children
+ for i, title in enumerate(titles):
+ self.tabs.set_title(i, title)
+ if children:
+ self.tabs.selected_index = 0
+
+ def _add_structure_panel(self):
+ structure_model = StructureResultsModel()
+ structure_model.process_uuid = self._model.process_uuid
+ self.structure_results = StructureResults(model=structure_model)
+ identifier = self.structure_results.identifier
+ self._model.add_model(identifier, structure_model)
+ self.panels = {
+ identifier: self.structure_results,
+ **self.panels,
+ }
+
+ def _fetch_plugin_results(self):
+ entries = get_entry_items("aiidalab_qe.properties", "result")
+ for identifier, entry in entries.items():
+ for key in ("panel", "model"):
+ if key not in entry:
+ raise ValueError(
+ f"Entry {identifier} is missing the results '{key}' key"
+ )
+ panel = entry["panel"]
+ model = entry["model"]()
+ self._model.add_model(identifier, model)
+ self.panels[identifier] = panel(
+ identifier=identifier,
+ model=model,
+ )
diff --git a/src/aiidalab_qe/app/result/model.py b/src/aiidalab_qe/app/result/model.py
index 71b62da2f..05ff4708f 100644
--- a/src/aiidalab_qe/app/result/model.py
+++ b/src/aiidalab_qe/app/result/model.py
@@ -6,11 +6,17 @@
from aiida import orm
from aiida.engine.processes import control
-from aiidalab_qe.common.mixins import HasProcess
+from aiidalab_qe.common.mixins import HasModels, HasProcess
from aiidalab_qe.common.mvc import Model
+from .components import ResultsComponentModel
-class ResultsStepModel(Model, HasProcess):
+
+class ResultsStepModel(
+ Model,
+ HasModels[ResultsComponentModel],
+ HasProcess,
+):
process_info = tl.Unicode("")
process_remote_folder_is_clean = tl.Bool(False)
@@ -35,7 +41,8 @@ def reset(self):
self.process_info = ""
def _update_process_remote_folder_state(self):
- if not (process_node := self.fetch_process_node()):
+ process_node = self.fetch_process_node()
+ if not (process_node and process_node.called_descendants):
return
cleaned = []
for called_descendant in process_node.called_descendants:
@@ -43,3 +50,13 @@ def _update_process_remote_folder_state(self):
with contextlib.suppress(Exception):
cleaned.append(called_descendant.outputs.remote_folder.is_empty)
self.process_remote_folder_is_clean = all(cleaned)
+
+ def _link_model(self, model: ResultsComponentModel):
+ tl.dlink(
+ (self, "process_uuid"),
+ (model, "process_uuid"),
+ )
+ tl.dlink(
+ (self, "monitor_counter"),
+ (model, "monitor_counter"),
+ )
diff --git a/src/aiidalab_qe/app/result/structure/structure.py b/src/aiidalab_qe/app/result/structure/structure.py
deleted file mode 100644
index fe2dc3d60..000000000
--- a/src/aiidalab_qe/app/result/structure/structure.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from aiidalab_qe.common.panel import ResultsPanel
-from aiidalab_widgets_base.viewers import StructureDataViewer
-
-from .model import StructureResultsModel
-
-
-class StructureResults(ResultsPanel[StructureResultsModel]):
- title = "Final Geometry"
- identifier = "structure"
-
- def render(self):
- if self.rendered:
- return
- widget = StructureDataViewer(structure=self._model.outputs.structure)
- self.children = [widget]
- self.rendered = True
diff --git a/src/aiidalab_qe/app/result/summary/__init__.py b/src/aiidalab_qe/app/result/summary/__init__.py
deleted file mode 100644
index b4ece5f76..000000000
--- a/src/aiidalab_qe/app/result/summary/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from .model import WorkChainSummaryResultsModel
-from .summary import WorkChainSummaryResultsPanel
-
-__all__ = [
- "WorkChainSummaryResultsModel",
- "WorkChainSummaryResultsPanel",
-]
diff --git a/src/aiidalab_qe/app/result/summary/summary.py b/src/aiidalab_qe/app/result/summary/summary.py
deleted file mode 100644
index 890c38779..000000000
--- a/src/aiidalab_qe/app/result/summary/summary.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import ipywidgets as ipw
-
-from aiidalab_qe.common.panel import ResultsPanel
-
-from .model import WorkChainSummaryResultsModel
-
-
-class WorkChainSummaryResultsPanel(ResultsPanel[WorkChainSummaryResultsModel]):
- title = "Workflow Summary"
- identifier = "summary"
-
- def render(self):
- if self.rendered:
- return
- report_html = self._model.generate_report_html()
- self.children = [ipw.HTML(report_html)]
- self.rendered = True
diff --git a/src/aiidalab_qe/app/result/utils/__init__.py b/src/aiidalab_qe/app/result/utils/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/aiidalab_qe/app/result/viewer/__init__.py b/src/aiidalab_qe/app/result/viewer/__init__.py
deleted file mode 100644
index 3c9110cb1..000000000
--- a/src/aiidalab_qe/app/result/viewer/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from .model import WorkChainViewerModel
-from .viewer import WorkChainViewer
-
-__all__ = [
- "WorkChainViewerModel",
- "WorkChainViewer",
-]
diff --git a/src/aiidalab_qe/app/result/viewer/model.py b/src/aiidalab_qe/app/result/viewer/model.py
deleted file mode 100644
index 2628d21c0..000000000
--- a/src/aiidalab_qe/app/result/viewer/model.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import ipywidgets as ipw
-
-from aiidalab_qe.common.mixins import HasModels, HasProcess
-from aiidalab_qe.common.mvc import Model
-from aiidalab_qe.common.panel import ResultsModel
-
-
-class WorkChainViewerModel(
- Model,
- HasModels[ResultsModel],
- HasProcess,
-):
- def _link_model(self, model: ResultsModel):
- ipw.dlink(
- (self, "monitor_counter"),
- (model, "monitor_counter"),
- )
diff --git a/src/aiidalab_qe/app/result/viewer/viewer.py b/src/aiidalab_qe/app/result/viewer/viewer.py
deleted file mode 100644
index d9228301b..000000000
--- a/src/aiidalab_qe/app/result/viewer/viewer.py
+++ /dev/null
@@ -1,121 +0,0 @@
-from __future__ import annotations
-
-import ipywidgets as ipw
-import traitlets as tl
-
-from aiidalab_qe.app.result.summary.model import WorkChainSummaryResultsModel
-from aiidalab_qe.app.utils import get_entry_items
-from aiidalab_qe.common.panel import ResultsPanel
-from aiidalab_widgets_base import register_viewer_widget
-
-from ..structure import StructureResults, StructureResultsModel
-from ..summary import WorkChainSummaryResultsPanel
-from .model import WorkChainViewerModel
-from .outputs import WorkChainOutputs
-
-
-@register_viewer_widget("process.workflow.workchain.WorkChainNode.")
-class WorkChainViewer(ipw.VBox):
- _results_shown = tl.Set()
-
- def __init__(self, node, model: WorkChainViewerModel, **kwargs):
- from aiidalab_qe.common.widgets import LoadingWidget
-
- super().__init__(
- children=[LoadingWidget("Loading result panels")],
- **kwargs,
- )
-
- self._model = model
- self._model.process_uuid = node.uuid
-
- self.rendered = False
-
- summary_model = WorkChainSummaryResultsModel()
- summary_model.process_uuid = node.uuid
- self.summary = WorkChainSummaryResultsPanel(model=summary_model)
- self._model.add_model("summary", summary_model)
-
- self.results: dict[str, ResultsPanel] = {
- "summary": self.summary,
- }
-
- # TODO consider refactoring structure relaxation panel as a plugin
- if "relax" in self._model.properties:
- self._add_structure_panel()
-
- self._fetch_plugin_results()
-
- def render(self):
- if self.rendered:
- return
-
- node = self._model.fetch_process_node()
-
- self.title = ipw.HTML()
-
- title = "
"
- if node:
- formula = node.inputs.structure.get_formula()
- title += f"\nQE App Workflow (pk: {node.pk}) — {formula}
"
- else:
- title += "\nQE App Workflow
"
-
- self.title.value = title
-
- self.tabs = ipw.Tab(selected_index=None)
-
- self.children = [
- self.title,
- self.tabs,
- ]
-
- self.rendered = True
-
- self._update_tabs()
-
- if node and node.is_finished:
- self._add_workflow_output_widget()
-
- def _update_tabs(self):
- children = []
- titles = []
- for identifier, model in [*self._model.get_models()]:
- if model.include:
- results = self.results[identifier]
- titles.append(results.title)
- children.append(results)
- self.tabs.children = children
- for i, title in enumerate(titles):
- self.tabs.set_title(i, title)
- self.tabs.selected_index = 0
- self.summary.render()
-
- def _add_workflow_output_widget(self):
- process_node = self._model.fetch_process_node()
- self.summary.children += (WorkChainOutputs(node=process_node),)
-
- def _add_structure_panel(self):
- structure_model = StructureResultsModel()
- structure_model.process_uuid = self._model.process_uuid
- self.structure_results = StructureResults(model=structure_model)
- identifier = self.structure_results.identifier
- self._model.add_model(identifier, structure_model)
- self.results[identifier] = self.structure_results
-
- def _fetch_plugin_results(self):
- entries = get_entry_items("aiidalab_qe.properties", "result")
- for identifier, entry in entries.items():
- for key in ("panel", "model"):
- if key not in entry:
- raise ValueError(
- f"Entry {identifier} is missing the results '{key}' key"
- )
- panel = entry["panel"]
- model = entry["model"]()
- model.process_uuid = self._model.process_uuid
- self.results[identifier] = panel(
- identifier=identifier,
- model=model,
- )
- self._model.add_model(identifier, model)
diff --git a/src/aiidalab_qe/app/static/styles/results.css b/src/aiidalab_qe/app/static/styles/results.css
new file mode 100644
index 000000000..be790bce8
--- /dev/null
+++ b/src/aiidalab_qe/app/static/styles/results.css
@@ -0,0 +1,11 @@
+.results-step-toggles {
+ display: flex;
+ margin: 10px auto;
+}
+.results-step-toggles div {
+ display: flex;
+ column-gap: 8px;
+}
+.results-step-toggles i {
+ margin-left: 4px;
+}
diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py
index e4f745abe..261960210 100644
--- a/src/aiidalab_qe/app/submission/__init__.py
+++ b/src/aiidalab_qe/app/submission/__init__.py
@@ -319,7 +319,7 @@ def _update_state(self, _=None):
self.state = self.State.FAIL
elif self.previous_step_state is not self.State.SUCCESS:
self.state = self.State.INIT
- elif self._model.process_node is not None:
+ elif self._model.confirmed:
self.state = self.State.SUCCESS
elif self._model.is_blocked:
self.state = self.State.READY
diff --git a/src/aiidalab_qe/app/submission/global_settings/model.py b/src/aiidalab_qe/app/submission/global_settings/model.py
index 43bee4696..eb8286a5d 100644
--- a/src/aiidalab_qe/app/submission/global_settings/model.py
+++ b/src/aiidalab_qe/app/submission/global_settings/model.py
@@ -125,7 +125,7 @@ def check_resources(self):
volume = self.input_structure.get_cell_volume()
code = orm.load_node(pw_code_model.selected)
- machine_cpus = code.computer.get_default_mpiprocs_per_machine()
+ machine_cpus = code.computer.get_default_mpiprocs_per_machine() or 1
large_system = (
num_sites > self._RUN_ON_LOCALHOST_NUM_SITES_WARN_THRESHOLD
@@ -145,7 +145,7 @@ def check_resources(self):
alert_message = ""
if (
- on_localhost and protocol == "precise" and localhost_cpus < 4
+ on_localhost and protocol == "precise" and machine_cpus < 4
): # This means that we are in a small deployment.
alert_message += (
f"Warning: The selected protocol is {protocol}, which is computationally demanding to run on localhost. "
diff --git a/src/aiidalab_qe/app/submission/model.py b/src/aiidalab_qe/app/submission/model.py
index 098b1f0c4..5302860f7 100644
--- a/src/aiidalab_qe/app/submission/model.py
+++ b/src/aiidalab_qe/app/submission/model.py
@@ -4,6 +4,7 @@
import ipywidgets as ipw
import traitlets as tl
+from IPython.display import Javascript, display
from aiida import orm
from aiida.engine import ProcessBuilderNamespace, submit
@@ -68,11 +69,10 @@ def is_blocked(self):
)
def confirm(self):
+ super().confirm()
+ self.unobserve_all("confirmed") # should no longer be unconfirmed
if not self.process_node:
self._submit()
- super().confirm()
- # Once submitted, nothing should unconfirm the model!
- self.unobserve_all("confirmed")
def update(self):
for _, model in self.get_models():
@@ -92,9 +92,13 @@ def update_process_label(self):
{"properties": []},
)
- soc_parameters = self.input_parameters["advanced"]["pw"]["parameters"][
- "SYSTEM"
- ].get("lspinorb", False)
+ soc_parameters = (
+ self.input_parameters.get("advanced", {})
+ .get("pw", {})
+ .get("parameters", {})
+ .get("SYSTEM", {})
+ .get("lspinorb", False)
+ )
soc_info = "spin-orbit coupling" if soc_parameters else ""
@@ -232,6 +236,12 @@ def _submit(self):
)
self.process_node = process_node
+ self._update_url()
+
+ def _update_url(self):
+ pk = self.process_node.pk
+ display(Javascript(f"window.history.pushState(null, '', '?pk={pk}');"))
+
def _link_model(self, model: ResourceSettingsModel):
for dependency in model.dependencies:
dependency_parts = dependency.split(".")
diff --git a/src/aiidalab_qe/common/mixins.py b/src/aiidalab_qe/common/mixins.py
index c08bc73ad..911bc81f4 100644
--- a/src/aiidalab_qe/common/mixins.py
+++ b/src/aiidalab_qe/common/mixins.py
@@ -55,9 +55,13 @@ def _link_model(self, model: T):
class HasProcess(tl.HasTraits):
- process_uuid = tl.Unicode(allow_none=True)
+ process_uuid = tl.Unicode(None, allow_none=True)
monitor_counter = tl.Int(0) # used for continuous updates
+ @property
+ def has_process(self):
+ return self.fetch_process_node() is not None
+
@property
def inputs(self):
process_node = self.fetch_process_node()
diff --git a/src/aiidalab_qe/common/panel.py b/src/aiidalab_qe/common/panel.py
index f0d1f60fe..1c8d0ddcb 100644
--- a/src/aiidalab_qe/common/panel.py
+++ b/src/aiidalab_qe/common/panel.py
@@ -116,8 +116,6 @@ class SettingsPanel(Panel, t.Generic[SM]):
title = "Settings"
def __init__(self, model: SM, **kwargs):
- from aiidalab_qe.common.widgets import LoadingWidget
-
self.loading_message = LoadingWidget(f"Loading {model.identifier} settings")
super().__init__(
@@ -530,10 +528,6 @@ class ResultsModel(Model, HasProcess):
"created": "info",
}
- @property
- def include(self):
- return self.identifier in self.properties
-
@property
def has_results(self):
node = self._fetch_child_process_node()
@@ -617,21 +611,54 @@ class ResultsPanel(Panel, t.Generic[RM]):
workchain_labels = []
def __init__(self, model: RM, **kwargs):
- from aiidalab_qe.common.widgets import LoadingWidget
-
self.loading_message = LoadingWidget(f"Loading {self.title.lower()} results")
+ super().__init__(**kwargs)
+
self._model = model
- if self.identifier != "summary":
- self._model.observe(
- self._on_monitor_counter_change,
- "monitor_counter",
- )
+ self._model.observe(
+ self._on_process_change,
+ "process_uuid",
+ )
+ self._model.observe(
+ self._on_monitor_counter_change,
+ "monitor_counter",
+ )
self.rendered = False
+ self.has_controls = False
self.links = []
+ def render(self):
+ if self.rendered:
+ if self.identifier == "structure":
+ self._render()
+ return
+ if self.has_controls or not self._model.has_process:
+ return
+ if not self._model.has_results:
+ self._render_controls()
+ else:
+ self._load_results()
+
+ def _on_process_change(self, _):
+ pass
+
+ def _on_monitor_counter_change(self, _):
+ self._model.update_process_status_notification()
+
+ def _on_load_results_click(self, _):
+ self._load_results()
+
+ def _load_results(self):
+ self.children = [self.loading_message]
+ self._render()
+ self.rendered = True
+ self._post_render()
+ self.has_controls = False
+
+ def _render_controls(self):
self.process_status_notification = ipw.HTML()
ipw.dlink(
(self._model, "process_status_notification"),
@@ -651,29 +678,24 @@ def __init__(self, model: RM, **kwargs):
)
self.load_results_button.on_click(self._on_load_results_click)
- super().__init__(
- children=[
- self.process_status_notification,
- ipw.HBox(
- children=[
- self.load_results_button,
- ipw.HTML("""
+ self.children = [
+ self.process_status_notification,
+ ipw.HBox(
+ children=[
+ self.load_results_button,
+ ipw.HTML("""
Note: Load time may vary depending on the size of the calculation
"""),
- ]
- ),
- ],
- **kwargs,
- )
+ ]
+ ),
+ ]
- def render(self):
- raise NotImplementedError()
+ self.has_controls = True
- def _on_load_results_click(self, _):
- self.children = [self.loading_message]
- self.render()
+ def _render(self):
+ raise NotImplementedError()
- def _on_monitor_counter_change(self, _):
- self._model.update_process_status_notification()
+ def _post_render(self):
+ pass
diff --git a/src/aiidalab_qe/plugins/bands/result/result.py b/src/aiidalab_qe/plugins/bands/result/result.py
index 9e27a5330..46cbd47ff 100644
--- a/src/aiidalab_qe/plugins/bands/result/result.py
+++ b/src/aiidalab_qe/plugins/bands/result/result.py
@@ -13,13 +13,10 @@ class BandsResultsPanel(ResultsPanel[BandsResultsModel]):
identifier = "bands"
workchain_labels = ["bands"]
- def render(self):
- if self.rendered:
- return
+ def _render(self):
bands_node = self._model.get_bands_node()
model = BandsPdosModel()
widget = BandsPdosWidget(model=model, bands=bands_node)
widget.layout = ipw.Layout(width="1000px")
widget.render()
self.children = [widget]
- self.rendered = True
diff --git a/src/aiidalab_qe/plugins/electronic_structure/result/result.py b/src/aiidalab_qe/plugins/electronic_structure/result/result.py
index 252e575c5..f2cb4934d 100644
--- a/src/aiidalab_qe/plugins/electronic_structure/result/result.py
+++ b/src/aiidalab_qe/plugins/electronic_structure/result/result.py
@@ -13,9 +13,7 @@ class ElectronicStructureResultsPanel(ResultsPanel[ElectronicStructureResultsMod
identifier = "electronic_structure"
workchain_labels = ["bands", "pdos"]
- def render(self):
- if self.rendered:
- return
+ def _render(self):
bands_node = self._model.get_bands_node()
pdos_node = self._model.get_pdos_node()
model = BandsPdosModel()
@@ -23,4 +21,3 @@ def render(self):
widget.layout = ipw.Layout(width="1000px")
widget.render()
self.children = [widget]
- self.rendered = True
diff --git a/src/aiidalab_qe/plugins/pdos/result/result.py b/src/aiidalab_qe/plugins/pdos/result/result.py
index e8e3c4f9f..7cc9db57b 100644
--- a/src/aiidalab_qe/plugins/pdos/result/result.py
+++ b/src/aiidalab_qe/plugins/pdos/result/result.py
@@ -13,13 +13,10 @@ class PdosResultsPanel(ResultsPanel[PdosResultsModel]):
identifier = "pdos"
workchain_labels = ["pdos"]
- def render(self):
- if self.rendered:
- return
+ def _render(self):
pdos_node = self._model.get_pdos_node()
model = BandsPdosModel()
widget = BandsPdosWidget(model=model, pdos=pdos_node)
widget.layout = ipw.Layout(width="1000px")
widget.render()
self.children = [widget]
- self.rendered = True
diff --git a/src/aiidalab_qe/plugins/xas/result/result.py b/src/aiidalab_qe/plugins/xas/result/result.py
index dc108e65f..4d94dc4d3 100644
--- a/src/aiidalab_qe/plugins/xas/result/result.py
+++ b/src/aiidalab_qe/plugins/xas/result/result.py
@@ -15,10 +15,7 @@ class XasResultsPanel(ResultsPanel[XasResultsModel]):
identifier = "xas"
workchain_labels = ["xas"]
- def render(self):
- if self.rendered:
- return
-
+ def _render(self):
variable_broad_select = ipw.Checkbox(
description="Use variable energy broadening.",
style={"description_width": "initial", "opacity": 0.5},
@@ -181,8 +178,7 @@ def render(self):
self.plot,
]
- self.rendered = True
-
+ def _post_render(self):
self._model.update_spectrum_options()
def _update_plot(self, _):
diff --git a/src/aiidalab_qe/plugins/xps/result/result.py b/src/aiidalab_qe/plugins/xps/result/result.py
index bdd10b894..4efbbc43e 100644
--- a/src/aiidalab_qe/plugins/xps/result/result.py
+++ b/src/aiidalab_qe/plugins/xps/result/result.py
@@ -15,10 +15,10 @@ class XpsResultsPanel(ResultsPanel[XpsResultsModel]):
experimental_data = None # Placeholder for experimental data
- def render(self):
- if self.rendered:
- return
+ def _on_file_upload(self, change):
+ self._model.upload_experimental_data(change["new"])
+ def _render(self):
spectra_type = ipw.ToggleButtons()
ipw.dlink(
(self._model, "spectra_type_options"),
@@ -174,13 +174,9 @@ def render(self):
upload_container,
]
- self.rendered = True
-
+ def _post_render(self):
self._model.update_spectrum_options()
- def _on_file_upload(self, change):
- self._model.upload_experimental_data(change["new"])
-
def _update_plot(self, _):
if not self.rendered:
return
diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py
index 10d9bf902..a6067bab4 100644
--- a/tests/test_plugins_bands.py
+++ b/tests/test_plugins_bands.py
@@ -8,7 +8,7 @@ def test_result(generate_qeapp_workchain):
model = BandsResultsModel()
model.process_uuid = workchain.node.uuid
result = BandsResultsPanel(model=model)
- result.render()
+ result._render()
widget = result.children[0]
model = widget._model
diff --git a/tests/test_plugins_pdos.py b/tests/test_plugins_pdos.py
index f2e0c79da..8bad6bd03 100644
--- a/tests/test_plugins_pdos.py
+++ b/tests/test_plugins_pdos.py
@@ -8,7 +8,7 @@ def test_result(generate_qeapp_workchain):
model = PdosResultsModel()
model.process_uuid = workchain.node.uuid
result = PdosResultsPanel(model=model)
- result.render()
+ result._render()
widget = result.children[0]
model = widget._model
diff --git a/tests/test_result.py b/tests/test_result.py
index 101670675..4b2362a15 100644
--- a/tests/test_result.py
+++ b/tests/test_result.py
@@ -1,8 +1,11 @@
from bs4 import BeautifulSoup
from aiidalab_qe.app.main import App
-from aiidalab_qe.app.result.summary import WorkChainSummaryResultsModel
-from aiidalab_qe.app.result.viewer import WorkChainViewer, WorkChainViewerModel
+from aiidalab_qe.app.result.components.summary import WorkChainSummaryModel
+from aiidalab_qe.app.result.components.viewer import (
+ WorkChainResultsViewer,
+ WorkChainResultsViewerModel,
+)
def test_result_step(app_to_submit, generate_qeapp_workchain):
@@ -27,22 +30,20 @@ def test_kill_and_clean_buttons(app_to_submit, generate_qeapp_workchain):
def test_workchainview(generate_qeapp_workchain):
"""Test the result tabs are properly updated"""
-
workchain = generate_qeapp_workchain()
workchain.node.seal()
- model = WorkChainViewerModel()
+ model = WorkChainResultsViewerModel()
+ viewer = WorkChainResultsViewer(model=model)
model.process_uuid = workchain.node.uuid
- viewer = WorkChainViewer(workchain.node, model=model)
viewer.render()
- assert len(viewer.tabs.children) == 5
- assert viewer.tabs._titles["0"] == "Workflow Summary" # type: ignore
- assert viewer.tabs._titles["1"] == "Final Geometry" # type: ignore
+ assert len(viewer.tabs.children) == 4
+ assert viewer.tabs._titles["0"] == "Final Geometry" # type: ignore
def test_summary_report(data_regression, generate_qeapp_workchain):
"""Test the summary report can be properly generated."""
workchain = generate_qeapp_workchain()
- model = WorkChainSummaryResultsModel()
+ model = WorkChainSummaryModel()
model.process_uuid = workchain.node.uuid
report_parameters = model._generate_report_parameters()
data_regression.check(report_parameters)
@@ -53,7 +54,7 @@ def test_summary_report_advanced_settings(data_regression, generate_qeapp_workch
workchain = generate_qeapp_workchain(
spin_type="collinear", electronic_type="metal", initial_magnetic_moments=0.1
)
- model = WorkChainSummaryResultsModel()
+ model = WorkChainSummaryModel()
model.process_uuid = workchain.node.uuid
report_parameters = model._generate_report_parameters()
assert report_parameters["initial_magnetic_moments"]["Si"] == 0.1
@@ -62,7 +63,7 @@ def test_summary_report_advanced_settings(data_regression, generate_qeapp_workch
def test_summary_view(generate_qeapp_workchain):
"""Test the report html can be properly generated."""
workchain = generate_qeapp_workchain()
- model = WorkChainSummaryResultsModel()
+ model = WorkChainSummaryModel()
model.process_uuid = workchain.node.uuid
report_html = model.generate_report_html()
parsed = BeautifulSoup(report_html, "html.parser")