diff --git a/setup.cfg b/setup.cfg index afea65b8d..fc196c45b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,6 +55,7 @@ aiidalab_qe.app.structure.examples = * aiidalab_qe.properties = bands = aiidalab_qe.plugins.bands:bands pdos = aiidalab_qe.plugins.pdos:pdos + electronic_structure = aiidalab_qe.plugins.electronic_structure:electronic_structure [aiidalab] title = Quantum ESPRESSO diff --git a/src/aiidalab_qe/app/result/workchain_viewer.py b/src/aiidalab_qe/app/result/workchain_viewer.py index 47c2212d8..b90d76586 100644 --- a/src/aiidalab_qe/app/result/workchain_viewer.py +++ b/src/aiidalab_qe/app/result/workchain_viewer.py @@ -15,12 +15,10 @@ from filelock import FileLock, Timeout from IPython.display import HTML, display from jinja2 import Environment -from widget_bandsplot import BandsPlotWidget from aiidalab_qe.app import static from aiidalab_qe.app.utils import get_entry_items -from .electronic_structure import export_data from .summary_viewer import SummaryView @@ -51,38 +49,19 @@ def __init__(self, node, **kwargs): self.workflows_summary = SummaryView(self.node) self.summary_tab = ipw.VBox(children=[self.workflows_summary]) - self.structure_tab = ipw.VBox( - [ipw.Label("Structure not available.")], - layout=ipw.Layout(min_height="380px"), - ) - self.bands_tab = ipw.VBox( - [ipw.Label("Electronic Structure not available.")], - layout=ipw.Layout(min_height="380px"), - ) - self.result_tabs = ipw.Tab( - children=[self.summary_tab, self.structure_tab, self.bands_tab] - ) + # Only the summary tab is shown by default + self.result_tabs = ipw.Tab(children=[self.summary_tab]) self.result_tabs.set_title(0, "Workflow Summary") - self.result_tabs.set_title(1, "Final Geometry (n/a)") - self.result_tabs.set_title(2, "Electronic Structure (n/a)") - # add plugin specific settings - entries = get_entry_items("aiidalab_qe.properties", "result") + # get plugin result panels + # and save them the results dictionary self.results = {} + entries = get_entry_items("aiidalab_qe.properties", "result") for identifier, entry_point in entries.items(): - # only show the result tab if the property is selected to be run - # this will be repalced by the ui_parameters in the future PR - # if this is the old version without plugin specific ui_parameters, just skip - if identifier not in ui_parameters.get("workchain", {}).get( - "properties", [] - ): - continue result = entry_point(self.node) self.results[identifier] = result self.results[identifier].identifier = identifier - self.result_tabs.children += (result,) - self.result_tabs.set_title(len(self.result_tabs.children) - 1, result.title) # An ugly fix to the structure appearance problem # https://github.com/aiidalab/aiidalab-qe/issues/69 @@ -90,13 +69,13 @@ def on_selected_index_change(change): index = change["new"] # Accessing the viewer only if the corresponding tab is present. if self.result_tabs._titles[str(index)] == "Final Geometry": - self._structure_view._viewer.handle_resize() + self.structure_tab._viewer.handle_resize() def toggle_camera(): """Toggle camera between perspective and orthographic.""" - self._structure_view._viewer.camera = ( + self.structure_tab._viewer.camera = ( "perspective" - if self._structure_view._viewer.camera == "orthographic" + if self.structure_tab._viewer.camera == "orthographic" else "orthographic" ) @@ -121,89 +100,39 @@ def _update_view(self): with self.hold_trait_notifications(): if self.node.is_finished: self._show_workflow_output() + # if the structure is present in the workchain, + # the structure tab will be added. if ( "structure" not in self._results_shown and "structure" in self.node.outputs ): self._show_structure() + self.result_tabs.children += (self.structure_tab,) + # index of the last tab + index = len(self.result_tabs.children) - 1 + self.result_tabs.set_title(index, "Final Geometry") self._results_shown.add("structure") - if "electronic_structure" not in self._results_shown and ( - "bands" in self.node.outputs or "pdos" in self.node.outputs - ): - self._show_electronic_structure() - self._results_shown.add("electronic_structure") # update the plugin specific results - for result in self.result_tabs.children[3:]: + for result in self.results.values(): # check if the result is already shown - # check if the plugin workchain result is in the outputs - if ( - result.identifier not in self._results_shown - and result.identifier in self.node.outputs - ): - result._update_view() - self._results_shown.add(result.identifier) - - def _show_structure(self): - self._structure_view = StructureDataViewer( - structure=self.node.outputs.structure - ) - self.result_tabs.children[1].children = [self._structure_view] - self.result_tabs.set_title(1, "Final Geometry") - - def _show_electronic_structure(self): - group_dos_by = ipw.ToggleButtons( - options=[ - ("Atom", "atom"), - ("Orbital", "angular"), - ], - value="atom", - ) - settings = ipw.VBox( - children=[ - ipw.HBox( - children=[ - ipw.Label( - "DOS grouped by:", - layout=ipw.Layout( - justify_content="flex-start", width="120px" - ), - ), - group_dos_by, + if result.identifier not in self._results_shown: + # check if the all required results are in the outputs + results_ready = [ + label in self.node.outputs for label in result.workchain_labels ] - ), - ], - layout={"margin": "0 0 30px 30px"}, - ) - # - data = export_data(self.node, group_dos_by=group_dos_by.value) - bands_data = data.get("bands", None) - dos_data = data.get("dos", None) - _bands_plot_view = BandsPlotWidget( - bands=bands_data, - dos=dos_data, - ) + if all(results_ready): + result._update_view() + self._results_shown.add(result.identifier) + # add this plugin result panel + self.result_tabs.children += (result,) + # index of the last tab + index = len(self.result_tabs.children) - 1 + self.result_tabs.set_title(index, result.title) - def response(change): - data = export_data(self.node, group_dos_by=group_dos_by.value) - bands_data = data.get("bands", None) - dos_data = data.get("dos", None) - _bands_plot_view = BandsPlotWidget( - bands=bands_data, - dos=dos_data, - ) - self.result_tabs.children[2].children = [ - settings, - _bands_plot_view, - ] - - group_dos_by.observe(response, names="value") - # update the electronic structure tab - self.result_tabs.children[2].children = [ - settings, - _bands_plot_view, - ] - self.result_tabs.set_title(2, "Electronic Structure") + def _show_structure(self): + """Show the structure of the workchain.""" + self.structure_tab = StructureDataViewer(structure=self.node.outputs.structure) def _show_workflow_output(self): self.workflows_output = WorkChainOutputs(self.node) diff --git a/src/aiidalab_qe/common/panel.py b/src/aiidalab_qe/common/panel.py index 5bd304495..2a88d486e 100644 --- a/src/aiidalab_qe/common/panel.py +++ b/src/aiidalab_qe/common/panel.py @@ -99,6 +99,8 @@ class ResultPanel(Panel): """ title = "Result" + # to specify which plugins (outputs) are needed for this result panel. + workchain_labels = [] def __init__(self, node=None, **kwargs): self.node = node @@ -116,7 +118,7 @@ def outputs(self): if self.node is None: return None - return getattr(self.node.outputs, self.identifier) + return self.node.outputs def _update_view(self): """Update the result in the panel. diff --git a/src/aiidalab_qe/plugins/bands/result.py b/src/aiidalab_qe/plugins/bands/result.py index ee9b30bc6..905aee852 100644 --- a/src/aiidalab_qe/plugins/bands/result.py +++ b/src/aiidalab_qe/plugins/bands/result.py @@ -29,14 +29,15 @@ class Result(ResultPanel): """Result panel for the bands calculation.""" title = "Bands" + workchain_labels = ["bands"] def __init__(self, node=None, **kwargs): - super().__init__(node=node, identifier="bands", **kwargs) + super().__init__(node=node, **kwargs) def _update_view(self): from widget_bandsplot import BandsPlotWidget - bands_data = export_bands_data(self.outputs) + bands_data = export_bands_data(self.outputs.bands) _bands_plot_view = BandsPlotWidget( bands=bands_data, ) diff --git a/src/aiidalab_qe/plugins/electronic_structure/__init__.py b/src/aiidalab_qe/plugins/electronic_structure/__init__.py new file mode 100644 index 000000000..df4e300da --- /dev/null +++ b/src/aiidalab_qe/plugins/electronic_structure/__init__.py @@ -0,0 +1,5 @@ +from .result import Result + +electronic_structure = { + "result": Result, +} diff --git a/src/aiidalab_qe/app/result/electronic_structure.py b/src/aiidalab_qe/plugins/electronic_structure/result.py similarity index 78% rename from src/aiidalab_qe/app/result/electronic_structure.py rename to src/aiidalab_qe/plugins/electronic_structure/result.py index aec06a2a4..2a5d652a3 100644 --- a/src/aiidalab_qe/app/result/electronic_structure.py +++ b/src/aiidalab_qe/plugins/electronic_structure/result.py @@ -1,8 +1,13 @@ +"""Electronic structure results view widgets""" import json import random +import ipywidgets as ipw from aiida import orm from monty.json import jsanitize +from widget_bandsplot import BandsPlotWidget + +from aiidalab_qe.common.panel import ResultPanel def export_data(work_chain_node, group_dos_by="atom"): @@ -182,3 +187,48 @@ def cmap(label: str) -> str: random.seed(ascn) return "#%06x" % random.randint(0, 0xFFFFFF) + + +class Result(ResultPanel): + title = "Electronic Structure" + workchain_labels = ["bands", "pdos"] + + def __init__(self, node=None, **kwargs): + self.dos_group_label = ipw.Label( + "DOS grouped by:", + layout=ipw.Layout(justify_content="flex-start", width="120px"), + ) + self.group_dos_by = ipw.ToggleButtons( + options=[ + ("Atom", "atom"), + ("Orbital", "angular"), + ], + value="atom", + ) + self.settings = ipw.HBox( + children=[ + self.dos_group_label, + self.group_dos_by, + ], + layout={"margin": "0 0 30px 30px"}, + ) + self.group_dos_by.observe(self._observe_group_dos_by, names="value") + super().__init__(node=node, **kwargs) + + def _observe_group_dos_by(self, change): + """Update the view of the widget when the group_dos_by value changes.""" + self._update_view() + + def _update_view(self): + """Update the view of the widget.""" + # + data = export_data(self.node, group_dos_by=self.group_dos_by.value) + _bands_plot_view = BandsPlotWidget( + bands=data.get("bands", None), + dos=data.get("dos", None), + ) + # update the electronic structure tab + self.children = [ + self.settings, + _bands_plot_view, + ] diff --git a/src/aiidalab_qe/plugins/pdos/result.py b/src/aiidalab_qe/plugins/pdos/result.py index 5571f3f7f..b57be09ef 100644 --- a/src/aiidalab_qe/plugins/pdos/result.py +++ b/src/aiidalab_qe/plugins/pdos/result.py @@ -158,9 +158,10 @@ def export_pdos_data(outputs, group_dos_by="atom"): class Result(ResultPanel): title = "PDOS" + workchain_labels = ["pdos"] def __init__(self, node=None, **kwargs): - super().__init__(node=node, identifier="pdos", **kwargs) + super().__init__(node=node, **kwargs) def _update_view(self): """Update the view of the widget.""" @@ -190,13 +191,15 @@ def _update_view(self): layout={"margin": "0 0 30px 30px"}, ) # - dos_data = export_pdos_data(self.outputs, group_dos_by=group_dos_by.value) + dos_data = export_pdos_data(self.outputs.pdos, group_dos_by=group_dos_by.value) _bands_plot_view = BandsPlotWidget( dos=dos_data, ) def response(change): - dos_data = export_pdos_data(self.outputs, group_dos_by=group_dos_by.value) + dos_data = export_pdos_data( + self.outputs.pdos, group_dos_by=group_dos_by.value + ) _bands_plot_view = BandsPlotWidget( dos=dos_data, ) diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py index 8dcd10013..136f5658f 100644 --- a/tests/test_plugins_bands.py +++ b/tests/test_plugins_bands.py @@ -8,7 +8,6 @@ def test_result(generate_qeapp_workchain): assert data is not None # generate structure for scf calculation result = Result(wkchain.node) - assert result.identifier == "bands" result._update_view() assert isinstance(result.children[0], BandsPlotWidget) diff --git a/tests/test_plugins_electronic_structure.py b/tests/test_plugins_electronic_structure.py new file mode 100644 index 000000000..c9c107f43 --- /dev/null +++ b/tests/test_plugins_electronic_structure.py @@ -0,0 +1,20 @@ +def test_electronic_structure(generate_qeapp_workchain): + """Test the electronic structure tab.""" + from aiida import engine + + from aiidalab_qe.app.result.workchain_viewer import WorkChainViewer + + wkchain = generate_qeapp_workchain() + wkchain.node.set_exit_status(0) + wkchain.node.set_process_state(engine.ProcessState.FINISHED) + wcv = WorkChainViewer(wkchain.node) + # find the tab with the identifier "electronic_structure" + # the built-in summary and structure tabs is not a plugin panel, + # thus don't have identifiers + tab = [ + tab + for tab in wcv.result_tabs.children + if getattr(tab, "identifier", "") == "electronic_structure" + ][0] + # It should have two children: settings and the _bands_plot_view + assert len(tab.children) == 2 diff --git a/tests/test_plugins_pdos.py b/tests/test_plugins_pdos.py index ce5d14062..1f12717ae 100644 --- a/tests/test_plugins_pdos.py +++ b/tests/test_plugins_pdos.py @@ -6,7 +6,6 @@ def test_result(generate_qeapp_workchain): assert data is not None # generate structure for scf calculation result = Result(node=wkchain.node) - assert result.identifier == "pdos" result._update_view() assert len(result.children) == 2 diff --git a/tests/test_result.py b/tests/test_result.py index 2a4c17f1d..7fca7ba85 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -16,23 +16,6 @@ def test_workchainview(generate_qeapp_workchain): assert len(wcv.result_tabs.children) == 5 assert wcv.result_tabs._titles["0"] == "Workflow Summary" assert wcv.result_tabs._titles["1"] == "Final Geometry" - assert wcv.result_tabs._titles["2"] == "Electronic Structure" - - -def test_electronic_structure(generate_qeapp_workchain): - """test the report can be properly generated from the builder without errors""" - from aiida import engine - - from aiidalab_qe.app.result.workchain_viewer import WorkChainViewer - - wkchain = generate_qeapp_workchain() - wkchain.node.set_exit_status(0) - wkchain.node.set_process_state(engine.ProcessState.FINISHED) - wcv = WorkChainViewer(wkchain.node) - assert ( - "DOS grouped by:" - == wcv.result_tabs.children[2].children[0].children[0].children[0].value - ) def test_summary_report(data_regression, generate_qeapp_workchain):