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"\n

QE App Workflow (pk: {node.pk}) — {formula}

" + else: + title = "\n

QE 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"\n

QE App Workflow (pk: {node.pk}) — {formula}

" - else: - title += "\n

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