diff --git a/src/aiidalab_qe/app/result/structure/model.py b/src/aiidalab_qe/app/result/structure/model.py index 847868a8..6ffb47ea 100644 --- a/src/aiidalab_qe/app/result/structure/model.py +++ b/src/aiidalab_qe/app/result/structure/model.py @@ -4,5 +4,8 @@ class StructureResultsModel(ResultsModel): identifier = "structure" - include = True _this_process_label = "PwRelaxWorkChain" + + @property + def include(self): + return "relax" in self.properties diff --git a/src/aiidalab_qe/app/result/summary/model.py b/src/aiidalab_qe/app/result/summary/model.py index ec5d58a2..881a3339 100644 --- a/src/aiidalab_qe/app/result/summary/model.py +++ b/src/aiidalab_qe/app/result/summary/model.py @@ -38,7 +38,9 @@ class WorkChainSummaryModel(ResultsModel): identifier = "summary" - include = True + @property + def include(self): + return True def generate_report_html(self): """Read from the bulider parameters and generate a html for reporting diff --git a/src/aiidalab_qe/app/result/viewer/viewer.py b/src/aiidalab_qe/app/result/viewer/viewer.py index 7c391f9b..403b2aef 100644 --- a/src/aiidalab_qe/app/result/viewer/viewer.py +++ b/src/aiidalab_qe/app/result/viewer/viewer.py @@ -49,7 +49,7 @@ def __init__(self, node: orm.Node, model: WorkChainViewerModel, **kwargs): self._fetch_plugin_results() - # HACK + # HACK should be called from result step - fix! self.render() def render(self): @@ -114,9 +114,6 @@ def _add_structure_panel(self): def _fetch_plugin_results(self): entries = get_entry_items("aiidalab_qe.properties", "result") - needs_electronic_structure = all( - identifier in self._model.properties for identifier in ("bands", "pdos") - ) for identifier, entry in entries.items(): for key in ("panel", "model"): if key not in entry: @@ -126,10 +123,6 @@ def _fetch_plugin_results(self): panel = entry["panel"] model = entry["model"]() self._model.add_model(identifier, model) - if identifier == "electronic_structure" and needs_electronic_structure: - model.include = True - else: - model.include = identifier in self._model.properties self.results[identifier] = panel( identifier=identifier, model=model, diff --git a/src/aiidalab_qe/common/bands_pdos/bandpdoswidget.py b/src/aiidalab_qe/common/bands_pdos/bandpdoswidget.py index b67e22fb..527300bd 100644 --- a/src/aiidalab_qe/common/bands_pdos/bandpdoswidget.py +++ b/src/aiidalab_qe/common/bands_pdos/bandpdoswidget.py @@ -108,7 +108,6 @@ def render(self): description="Apply selection", icon="pencil", button_style="primary", - disabled=False, ) self.update_plot_button.on_click(self._update_plot) @@ -116,7 +115,6 @@ def render(self): description="Download Data", icon="download", button_style="primary", - disabled=False, layout=ipw.Layout(visibility="hidden"), ) self.download_button.on_click(self._model.download_data) diff --git a/src/aiidalab_qe/common/panel.py b/src/aiidalab_qe/common/panel.py index 9bc60553..4763d817 100644 --- a/src/aiidalab_qe/common/panel.py +++ b/src/aiidalab_qe/common/panel.py @@ -188,7 +188,6 @@ class ResultsModel(Model, HasProcess): this_process_uuid = tl.Unicode(allow_none=True) process_state_notification = tl.Unicode("") - include = False _this_process_label = "" CSS_MAP = { @@ -201,6 +200,10 @@ class ResultsModel(Model, HasProcess): "created": "info", } + @property + def include(self): + return self.identifier in self.properties + @property def has_results(self): return self.identifier in self.outputs @@ -217,34 +220,6 @@ def update_process_state_notification(self): """ - @tl.observe("process_uuid") - def _on_process_uuid_change(self, _): - super()._on_process_uuid_change(_) - if self.identifier != "summary": - self._set_this_process_uuid() - - def _set_this_process_uuid(self): - if not self.process_uuid: - return - try: - self.this_process_uuid = ( - orm.QueryBuilder() - .append( - orm.WorkChainNode, - filters={"uuid": self.process_node.uuid}, - tag="root_process", - ) - .append( - orm.WorkChainNode, - filters={"attributes.process_label": self._this_process_label}, - project="uuid", - with_incoming="root_process", - ) - .first(flat=True) - ) - except Exception: - self.this_process_uuid = None - def _fetch_this_process_node(self): return ( orm.QueryBuilder() @@ -282,7 +257,7 @@ class ResultsPanel(Panel, t.Generic[RM]): def __init__(self, model: RM, **kwargs): from aiidalab_qe.common.widgets import LoadingWidget - self.loading_message = LoadingWidget(f"Loading {self.identifier} results") + self.loading_message = LoadingWidget(f"Loading {self.title.lower()} results") super().__init__( children=[self.loading_message], @@ -338,8 +313,6 @@ def render(self): self.rendered = True - self._model.update_process_state_notification() - def _on_load_results_click(self, _): self.children = [self.loading_message] self._update_view() diff --git a/src/aiidalab_qe/plugins/bands/result/model.py b/src/aiidalab_qe/plugins/bands/result/model.py index b0ad1a25..7bb4d4aa 100644 --- a/src/aiidalab_qe/plugins/bands/result/model.py +++ b/src/aiidalab_qe/plugins/bands/result/model.py @@ -10,25 +10,17 @@ def get_bands_node(self): # Check for 'bands' or 'bands_projwfc' attributes within 'bands' output if self._has_bands: return self.outputs.bands.bands - elif self._has_bands_projwfc: + elif self._has_band_projections: return self.outputs.bands.bands_projwfc - elif self._has_bands_output: + elif self.has_results: # If neither 'bands' nor 'bands_projwfc' exist, use 'bands_output' itself # This is the case for compatibility with older versions of the plugin return self.outputs.bands - @property - def _has_bands_output(self): - return hasattr(self.outputs, "bands") - @property def _has_bands(self): - if not self._has_bands_output: - return False - return hasattr(self.outputs.bands, "bands") + return self.has_results and hasattr(self.outputs.bands, "bands") @property - def _has_bands_projwfc(self): - if not self._has_bands_output: - return False - return hasattr(self.outputs.bands, "bands_projwfc") + def _has_band_projections(self): + return self.has_results and hasattr(self.outputs.bands, "bands_projwfc") diff --git a/src/aiidalab_qe/plugins/electronic_structure/__init__.py b/src/aiidalab_qe/plugins/electronic_structure/__init__.py index fb50ec92..9fb9631a 100644 --- a/src/aiidalab_qe/plugins/electronic_structure/__init__.py +++ b/src/aiidalab_qe/plugins/electronic_structure/__init__.py @@ -1,8 +1,8 @@ -from .result import ElectronicStructureResult, ElectronicStructureResultModel +from .result import ElectronicStructureResults, ElectronicStructureResultsModel electronic_structure = { "result": { - "panel": ElectronicStructureResult, - "model": ElectronicStructureResultModel, + "panel": ElectronicStructureResults, + "model": ElectronicStructureResultsModel, }, } diff --git a/src/aiidalab_qe/plugins/electronic_structure/result/__init__.py b/src/aiidalab_qe/plugins/electronic_structure/result/__init__.py index 309e5c78..b78cb362 100644 --- a/src/aiidalab_qe/plugins/electronic_structure/result/__init__.py +++ b/src/aiidalab_qe/plugins/electronic_structure/result/__init__.py @@ -1,7 +1,7 @@ -from .model import ElectronicStructureResultModel -from .result import ElectronicStructureResult +from .model import ElectronicStructureResultsModel +from .result import ElectronicStructureResults __all__ = [ - "ElectronicStructureResultModel", - "ElectronicStructureResult", + "ElectronicStructureResultsModel", + "ElectronicStructureResults", ] diff --git a/src/aiidalab_qe/plugins/electronic_structure/result/model.py b/src/aiidalab_qe/plugins/electronic_structure/result/model.py index 99792320..4534a6f5 100644 --- a/src/aiidalab_qe/plugins/electronic_structure/result/model.py +++ b/src/aiidalab_qe/plugins/electronic_structure/result/model.py @@ -1,7 +1,12 @@ +from aiida import orm from aiidalab_qe.common.panel import ResultsModel -class ElectronicStructureResultModel(ResultsModel): +# TODO if combined, this model should extend `HasModels`, and effectively +# TODO reduce to a container of Bands and PDOS, similar to its results panel +class ElectronicStructureResultsModel(ResultsModel): + identifier = "electronic_structure" + def get_pdos_node(self): try: return self.outputs.pdos @@ -12,25 +17,86 @@ def get_bands_node(self): # Check for 'bands' or 'bands_projwfc' attributes within 'bands' output if self._has_bands: return self.outputs.bands.bands - elif self._has_bands_projwfc: + elif self._has_band_projections: return self.outputs.bands.bands_projwfc elif self._has_bands_output: # If neither 'bands' nor 'bands_projwfc' exist, use 'bands_output' itself # This is the case for compatibility with older versions of the plugin return self.outputs.bands + @property + def include(self): + # TODO `and` should be `or` if combined + return all(identifier in self.properties for identifier in ("bands", "pdos")) + + @property + def has_results(self): + # TODO first `and` should be `or` if combined + return self._has_bands and (self._has_dos and self._has_dos_projections) + + @property + def process_states(self): + nodes = self._fetch_electronic_structure_process_nodes() + return [ + node.process_state.value if node and node.process_state else "queued" + for node in nodes + ] + + def update_process_state_notification(self): + processes = ("Bands", "PDOS") + states = self.process_states + self.process_state_notification = "\n".join( + f""" +
+ {process} status: {state.upper()} +
+ """ + for process, state in zip(processes, states) + ) + @property def _has_bands_output(self): return hasattr(self.outputs, "bands") @property def _has_bands(self): - if not self._has_bands_output: - return False - return hasattr(self.outputs.bands, "bands") + return self._has_bands_output and hasattr(self.outputs.bands, "bands") @property - def _has_bands_projwfc(self): - if not self._has_bands_output: - return False - return hasattr(self.outputs.bands, "bands_projwfc") + def _has_band_projections(self): + return self._has_bands_output and hasattr(self.outputs.bands, "bands_projwfc") + + @property + def _has_pdos_output(self): + return hasattr(self.outputs, "pdos") + + @property + def _has_dos(self): + return self._has_pdos_output and hasattr(self.outputs.pdos, "dos") + + @property + def _has_dos_projections(self): + return self._has_pdos_output and hasattr(self.outputs.pdos, "projwfc") + + def _fetch_electronic_structure_process_nodes(self): + return ( + orm.QueryBuilder() + .append( + orm.WorkChainNode, + filters={"uuid": self.process_node.uuid}, + tag="root_process", + ) + .append( + orm.WorkChainNode, + filters={ + "attributes.process_label": { + "in": [ + "BandsWorkChain", + "PdosWorkChain", + ] + } + }, + with_incoming="root_process", + ) + .all(flat=True) + ) diff --git a/src/aiidalab_qe/plugins/electronic_structure/result/result.py b/src/aiidalab_qe/plugins/electronic_structure/result/result.py index 142ffaba..9b71b6d3 100644 --- a/src/aiidalab_qe/plugins/electronic_structure/result/result.py +++ b/src/aiidalab_qe/plugins/electronic_structure/result/result.py @@ -5,26 +5,19 @@ from aiidalab_qe.common.bands_pdos import BandPdosWidget, BandsPdosModel from aiidalab_qe.common.panel import ResultsPanel -from .model import ElectronicStructureResultModel +from .model import ElectronicStructureResultsModel -class ElectronicStructureResult(ResultsPanel[ElectronicStructureResultModel]): +class ElectronicStructureResults(ResultsPanel[ElectronicStructureResultsModel]): title = "Electronic Structure" identifier = "electronic_structure" workchain_labels = ["bands", "pdos"] - def render(self): - if self.rendered: - return - - pdos_node = self._model.get_pdos_node() + def _update_view(self): bands_node = self._model.get_bands_node() - + pdos_node = self._model.get_pdos_node() model = BandsPdosModel() widget = BandPdosWidget(model=model, bands=bands_node, pdos=pdos_node) widget.layout = ipw.Layout(width="1000px") widget.render() - self.children = [widget] - - self.rendered = True diff --git a/src/aiidalab_qe/plugins/pdos/result/model.py b/src/aiidalab_qe/plugins/pdos/result/model.py index a2d5c652..d69a7428 100644 --- a/src/aiidalab_qe/plugins/pdos/result/model.py +++ b/src/aiidalab_qe/plugins/pdos/result/model.py @@ -2,8 +2,20 @@ class PdosResultsModel(ResultsModel): + identifier = "pdos" + + _this_process_label = "PdosWorkChain" + def get_pdos_node(self): try: return self.outputs.pdos except AttributeError: return None + + @property + def _has_dos(self): + return self.has_results and hasattr(self.outputs.pdos, "dos") + + @property + def _has_dos_projections(self): + return self.has_results and hasattr(self.outputs.pdos, "projwfc") diff --git a/src/aiidalab_qe/plugins/pdos/result/result.py b/src/aiidalab_qe/plugins/pdos/result/result.py index fb77718e..848864e9 100644 --- a/src/aiidalab_qe/plugins/pdos/result/result.py +++ b/src/aiidalab_qe/plugins/pdos/result/result.py @@ -13,17 +13,10 @@ class PdosResults(ResultsPanel[PdosResultsModel]): identifier = "pdos" workchain_labels = ["pdos"] - def render(self): - if self.rendered: - return - + def _update_view(self): pdos_node = self._model.get_pdos_node() - model = BandsPdosModel() widget = BandPdosWidget(model=model, pdos=pdos_node) widget.layout = ipw.Layout(width="1000px") widget.render() - self.children = [widget] - - self.rendered = True diff --git a/tests/test_plugins_electronic_structure.py b/tests/test_plugins_electronic_structure.py index 4df2645b..0e68e808 100644 --- a/tests/test_plugins_electronic_structure.py +++ b/tests/test_plugins_electronic_structure.py @@ -3,15 +3,15 @@ def test_electronic_structure(generate_qeapp_workchain): from aiidalab_qe.common.bands_pdos import BandPdosWidget from aiidalab_qe.plugins.electronic_structure.result import ( - ElectronicStructureResult, - ElectronicStructureResultModel, + ElectronicStructureResults, + ElectronicStructureResultsModel, ) workchain = generate_qeapp_workchain() # generate structure for scf calculation - model = ElectronicStructureResultModel() + model = ElectronicStructureResultsModel() model.process_node = workchain.node - result = ElectronicStructureResult(model=model) + result = ElectronicStructureResults(model=model) result.render() widget = result.children[0]