Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign summary view #1044

Merged
merged 15 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/aiidalab_qe/app/parameters/qeapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ codes:
code: xspectra-7.2@localhost
hp:
code: hp-7.2@localhost

summary_format: list
80 changes: 47 additions & 33 deletions src/aiidalab_qe/app/result/components/summary/download_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self, workchain_node):
button_style="primary",
disabled=False,
tooltip="Download the AiiDA archive of the simulation, ready to be shared or imported into another AiiDA profile",
layout=ipw.Layout(width="auto"),
layout=ipw.Layout(width="100%"),
)
self.download_archive_button.on_click(self._download_data_thread)

Expand All @@ -25,46 +25,48 @@ def __init__(self, workchain_node):
button_style="primary",
disabled=False,
tooltip="Download the raw data of the simulation, organized in intuitive directory paths.",
layout=ipw.Layout(width="auto"),
layout=ipw.Layout(width="100%"),
)
try:
# check that we can import the ProcessDumper (not implemented in old AiiDA versions)
# pre-commit: allow any unused imports in the next line
from aiida.tools.dumping.processes import ProcessDumper # noqa: F401

self.download_raw_button.on_click(self._download_data_thread)
dumper_is_available = True
self.dumper_is_available = True
except Exception:
dumper_is_available = False
self.dumper_is_available = False

self.download_raw_button.disabled = not dumper_is_available
self.download_raw_button.disabled = not self.dumper_is_available

self.node = workchain_node

super().__init__(
children=[
ipw.HTML(
"<h3>Download the data</h3>"
"It is possible to download raw data (i.e. input and output files) and/or the AiiDA archive (ready to be shared or imported into another AiiDA profile)"
),
ipw.HBox(
children=[self.download_raw_button],
layout=ipw.Layout(width="700px"), # Set the desired width here
),
ipw.HBox(
children=[self.download_archive_button],
layout=ipw.Layout(width="700px"), # Set the desired width here
),
],
)
self._downloading_message = ipw.HTML()

if not dumper_is_available:
self.children[1].children += (
ipw.HTML(
"<p style='color:red;'>The raw data download is not available because the AiiDA version is too old.</p>"
),
children = []

if not self.dumper_is_available:
children.append(
ipw.HTML("""
<p style="color:red; line-height: 140%;">
The raw data download is not available because the AiiDA
version is too old.
</p>
"""),
)

children.extend(
[
ipw.HBox(children=[self.download_raw_button]),
ipw.HBox(children=[self.download_archive_button]),
self._downloading_message,
]
)

super().__init__(
children=children,
)

def _download_data_thread(self, button_instance):
thread = Thread(target=lambda: self._download_data(button_instance))
thread.start()
Expand All @@ -81,23 +83,35 @@ def _download_data(self, button_instance):
Args:
button_instance (ipywidgets.Button): The button instance that was clicked.
"""
button_instance.disabled = True
self._disable_buttons()
if "archive" in button_instance.description:
what = "archive"
filename = f"export_qeapp_calculation_pk_{self.node.pk}.aiida"
box = self.children[2]
else:
what = "raw"
filename = f"export_{self.node.pk}_raw.zip"
box = self.children[1]

box.children += (ipw.HTML("Downloading data..."),)

self._show_downloading_message(what)
data = self.produce_bitestream(self.node, what=what)
self._download(payload=data, filename=filename)
del data
box.children = box.children[:1]
button_instance.disabled = False
self._hide_downloading_message()
self._enable_buttons()

def _show_downloading_message(self, what):
self._downloading_message.value = f"Downloading {what} data..."

def _hide_downloading_message(self):
self._downloading_message.value = ""

def _disable_buttons(self):
self.download_raw_button.disabled = True
self.download_archive_button.disabled = True

def _enable_buttons(self):
if self.dumper_is_available:
self.download_raw_button.disabled = False
self.download_archive_button.disabled = False

@staticmethod
def _download(payload, filename):
Expand Down
94 changes: 86 additions & 8 deletions src/aiidalab_qe/app/result/components/summary/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
from __future__ import annotations

import traitlets as tl
from importlib_resources import files
from jinja2 import Environment

from aiida import orm
from aiida.cmdline.utils.common import get_workchain_report
from aiida_quantumespresso.workflows.pw.bands import PwBandsWorkChain
from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.app.result.components import ResultsComponentModel
from aiidalab_qe.app.static import styles, templates
from aiidalab_qe.common.time import format_time, relative_time

DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore


FUNCTIONAL_LINK_MAP = {
"PBE": "https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.77.3865",
Expand Down Expand Up @@ -38,6 +52,10 @@
class WorkChainSummaryModel(ResultsComponentModel):
identifier = "workflow summary"

failed_calculation_report = tl.Unicode("")

has_failure_report = False

@property
def include(self):
return True
Expand All @@ -46,22 +64,21 @@ def generate_report_html(self):
"""Read from the builder parameters and generate a html for reporting
the inputs for the `QeAppWorkChain`.
"""
from importlib.resources import files

from jinja2 import Environment

from aiidalab_qe.app.static import styles, templates

def _fmt_yes_no(truthy):
return "Yes" if truthy else "No"
return "yes" if truthy else "no"

env = Environment()
env.filters.update(
{
"fmt_yes_no": _fmt_yes_no,
}
)
template = files(templates).joinpath("workflow_summary.jinja").read_text()
template = (
files(templates)
.joinpath(f"workflow_{DEFAULT['summary_format']}_summary.jinja")
.read_text()
)
style = files(styles).joinpath("style.css").read_text()
parameters = self._generate_report_parameters()
report = {key: value for key, value in parameters.items() if value is not None}
Expand Down Expand Up @@ -112,6 +129,23 @@ def generate_report_text(self, report_dict):

return report_string

def generate_failure_report(self):
"""Generate a html for reporting the failure of the `QeAppWorkChain`."""
if not (process_node := self.fetch_process_node()):
return
if process_node.exit_status == 0:
return
final_calcjob = self._get_final_calcjob(process_node)
env = Environment()
template = files(templates).joinpath("workflow_failure.jinja").read_text()
style = files(styles).joinpath("style.css").read_text()
self.failed_calculation_report = env.from_string(template).render(
style=style,
process_report=get_workchain_report(process_node, "REPORT"),
calcjob_exit_message=final_calcjob.exit_message,
)
self.has_failure_report = True

def _generate_report_parameters(self):
"""Generate the report parameters from the ui parameters and workchain's input.

Expand All @@ -134,7 +168,27 @@ def _generate_report_parameters(self):
# drop support for old ui parameters
if "workchain" not in ui_parameters:
return {}
initial_structure = qeapp_wc.inputs.structure
report = {
"pk": qeapp_wc.pk,
"uuid": str(qeapp_wc.uuid),
"label": qeapp_wc.label,
"description": qeapp_wc.description,
"creation_time": format_time(qeapp_wc.ctime),
"creation_time_relative": relative_time(qeapp_wc.ctime),
"modification_time": format_time(qeapp_wc.mtime),
"modification_time_relative": relative_time(qeapp_wc.mtime),
"formula": initial_structure.get_formula(),
"num_atoms": len(initial_structure.sites),
"space_group": "{} ({})".format(
*initial_structure.get_pymatgen().get_space_group_info()
),
"cell_lengths": "{:.3f} {:.3f} {:.3f}".format(
*initial_structure.cell_lengths
),
"cell_angles": "{:.0f} {:.0f} {:.0f}".format(
*initial_structure.cell_angles
),
"relaxed": None
if ui_parameters["workchain"]["relax_type"] == "none"
else ui_parameters["workchain"]["relax_type"],
Expand Down Expand Up @@ -208,7 +262,7 @@ def _generate_report_parameters(self):
qeapp_wc.inputs.structure.pbc, "xyz"
)

# Spin-Oribit coupling
# Spin-Orbit coupling
report["spin_orbit"] = pw_parameters["SYSTEM"].get("lspinorb", False)

if hubbard_dict := ui_parameters["advanced"].pop("hubbard_parameters", None):
Expand All @@ -229,3 +283,27 @@ def _generate_report_parameters(self):
qeapp_wc.inputs.pdos.nscf.kpoints_distance.base.attributes.get("value")
)
return report

@staticmethod
def _get_final_calcjob(node: orm.WorkChainNode) -> orm.CalcJobNode | None:
"""Get the final calculation job node called by a workchain node.

Parameters
----------
`node`: `orm.WorkChainNode`
The work chain node to get the final calculation job node from.

Returns
-------
`orm.CalcJobNode` | `None`
The final calculation job node called by the workchain node if available.
"""
try:
final_calcjob = [
process
for process in node.called_descendants
if isinstance(process, orm.CalcJobNode) and process.is_finished
][-1]
except IndexError:
final_calcjob = None
return final_calcjob
48 changes: 1 addition & 47 deletions src/aiidalab_qe/app/result/components/summary/outputs.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
from __future__ import annotations

import shutil
from importlib.resources import files
from pathlib import Path
from tempfile import TemporaryDirectory

import ipywidgets as ipw
import traitlets as tl
from filelock import FileLock, Timeout
from IPython.display import HTML, display
from jinja2 import Environment

from aiida import orm
from aiida.cmdline.utils.common import get_workchain_report
from aiida.common import LinkType
from aiidalab_qe.app.static import styles, templates

from .download_data import DownloadDataWidget

Expand All @@ -40,33 +36,8 @@ def __init__(self, node, export_dir=None, **kwargs):
)
self._download_button_widget = DownloadDataWidget(workchain_node=self.node)

if node.exit_status != 0:
title = ipw.HTML(
f"<h4>Workflow failed with exit status [{ node.exit_status }]</h4>"
)
final_calcjob = self._get_final_calcjob(node)
env = Environment()
template = files(templates).joinpath("workflow_failure.jinja").read_text()
style = files(styles).joinpath("style.css").read_text()
output = ipw.HTML(
env.from_string(template).render(
style=style,
process_report=get_workchain_report(node, "REPORT"),
calcjob_exit_message=final_calcjob.exit_message,
)
)
else:
title = ipw.HTML("<h4>Workflow completed successfully!</h4>")
output = ipw.HTML()

super().__init__(
children=[
ipw.VBox(
children=[self._download_button_widget, title],
layout=ipw.Layout(justify_content="space-between", margin="10px"),
),
output,
],
children=[self._download_button_widget],
**kwargs,
)

Expand Down Expand Up @@ -152,23 +123,6 @@ def _prepare_calcjob_io(cls, node: orm.WorkChainNode, root_folder: Path):

counter += 1

@staticmethod
def _get_final_calcjob(node: orm.WorkChainNode) -> orm.CalcJobNode | None:
"""Get the final calculation job node called by a work chain node.

:param node: Work chain node.
"""
try:
final_calcjob = [
process
for process in node.called_descendants
if isinstance(process, orm.CalcJobNode) and process.is_finished
][-1]
except IndexError:
final_calcjob = None

return final_calcjob

@staticmethod
def _write_calcjob_io(calcjob: orm.CalcJobNode, folder: Path) -> None:
"""Write the ``calcjob`` in and output files to ``folder``.
Expand Down
Loading
Loading