diff --git a/src/aiidalab_qe/app/parameters/qeapp.yaml b/src/aiidalab_qe/app/parameters/qeapp.yaml
index ca6798487..952d45b1d 100644
--- a/src/aiidalab_qe/app/parameters/qeapp.yaml
+++ b/src/aiidalab_qe/app/parameters/qeapp.yaml
@@ -40,3 +40,5 @@ codes:
code: xspectra-7.2@localhost
hp:
code: hp-7.2@localhost
+
+summary_format: list
diff --git a/src/aiidalab_qe/app/result/components/summary/download_data.py b/src/aiidalab_qe/app/result/components/summary/download_data.py
index 7478ee35c..d04886895 100644
--- a/src/aiidalab_qe/app/result/components/summary/download_data.py
+++ b/src/aiidalab_qe/app/result/components/summary/download_data.py
@@ -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)
@@ -25,7 +25,7 @@ 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)
@@ -33,38 +33,40 @@ def __init__(self, workchain_node):
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(
- "
Download the data
"
- "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(
- "The raw data download is not available because the AiiDA version is too old.
"
- ),
+ children = []
+
+ if not self.dumper_is_available:
+ children.append(
+ ipw.HTML("""
+
+ The raw data download is not available because the AiiDA
+ version is too old.
+
+ """),
)
+ 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()
@@ -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):
diff --git a/src/aiidalab_qe/app/result/components/summary/model.py b/src/aiidalab_qe/app/result/components/summary/model.py
index 74b1b635a..c81e75c4d 100644
--- a/src/aiidalab_qe/app/result/components/summary/model.py
+++ b/src/aiidalab_qe/app/result/components/summary/model.py
@@ -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",
@@ -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
@@ -46,14 +64,9 @@ 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(
@@ -61,7 +74,11 @@ def _fmt_yes_no(truthy):
"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}
@@ -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.
@@ -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"],
@@ -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):
@@ -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
diff --git a/src/aiidalab_qe/app/result/components/summary/outputs.py b/src/aiidalab_qe/app/result/components/summary/outputs.py
index 3cedb8c13..9992e9824 100644
--- a/src/aiidalab_qe/app/result/components/summary/outputs.py
+++ b/src/aiidalab_qe/app/result/components/summary/outputs.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import shutil
-from importlib.resources import files
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -9,12 +8,9 @@
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
@@ -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"Workflow failed with exit status [{ node.exit_status }]
"
- )
- 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("Workflow completed successfully!
")
- 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,
)
@@ -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``.
diff --git a/src/aiidalab_qe/app/result/components/summary/summary.py b/src/aiidalab_qe/app/result/components/summary/summary.py
index aab7390da..81712c343 100644
--- a/src/aiidalab_qe/app/result/components/summary/summary.py
+++ b/src/aiidalab_qe/app/result/components/summary/summary.py
@@ -1,24 +1,27 @@
import ipywidgets as ipw
+from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.app.result.components import ResultsComponent
from .model import WorkChainSummaryModel
from .outputs import WorkChainOutputs
+DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore
+
class WorkChainSummary(ResultsComponent[WorkChainSummaryModel]):
- def __init__(self, model: WorkChainSummaryModel, **kwargs):
- super().__init__(model=model, **kwargs)
- self.has_report = False
- self.has_output = False
+ has_settings_report = False
+ has_download_widget = False
def _on_process_change(self, _):
- if not self.has_report:
+ if not self.has_settings_report:
self._render_summary()
def _on_monitor_counter_change(self, _):
- if not self.has_output:
- self._render_output()
+ if not self.has_download_widget:
+ self._render_download_widget()
+ if not self._model.has_failure_report:
+ self._model.generate_failure_report()
def _render(self):
self._render_summary()
@@ -26,12 +29,55 @@ def _render(self):
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):
+ settings_summary = ipw.HTML(
+ value=self._model.generate_report_html(),
+ )
+ settings_summary.add_class("summary-panel")
+
+ self.output_download_container = ipw.VBox(
+ children=[
+ ipw.HTML("""
+
+
Download the data
+ Once the workflow is finished, you can 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.HTML("Download buttons will appear here when available."),
+ ],
+ )
+ self.output_download_container.add_class("summary-panel")
+
+ container = ipw.HBox(
+ children=[
+ settings_summary,
+ self.output_download_container,
+ ],
+ )
+ container.add_class("workflow-summary-container")
+ container.add_class(DEFAULT["summary_format"])
+
+ self.failed_calculation_report = ipw.HTML()
+ ipw.dlink(
+ (self._model, "failed_calculation_report"),
+ (self.failed_calculation_report, "value"),
+ )
+
+ self.children = [
+ container,
+ self.failed_calculation_report,
+ ]
+ self.has_settings_report = True
+
+ def _render_download_widget(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
+ output_download_widget = WorkChainOutputs(node=process_node)
+ output_download_widget.layout.width = "100%"
+ self.output_download_container.children = [
+ self.output_download_container.children[0], # type: ignore
+ output_download_widget,
+ ]
+ self.has_download_widget = True
diff --git a/src/aiidalab_qe/app/result/components/viewer/viewer.py b/src/aiidalab_qe/app/result/components/viewer/viewer.py
index 3357aa32d..288671e51 100644
--- a/src/aiidalab_qe/app/result/components/viewer/viewer.py
+++ b/src/aiidalab_qe/app/result/components/viewer/viewer.py
@@ -29,24 +29,12 @@ def _on_tab_change(self, change):
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",
)
-
- self.children = [
- self.title,
- self.tabs,
- ]
+ self.children = [self.tabs]
def _post_render(self):
self._set_tabs()
diff --git a/src/aiidalab_qe/app/static/styles/style.css b/src/aiidalab_qe/app/static/styles/style.css
index bd57b07c1..2fad9be5e 100644
--- a/src/aiidalab_qe/app/static/styles/style.css
+++ b/src/aiidalab_qe/app/static/styles/style.css
@@ -1,75 +1,42 @@
-:root {
- --lab-blue: #2097F3;
- --lab-background: #d3ecff;
-}
-
h3 {
- margin-top: 0px;
+ margin-top: 0px;
}
/* Tables */
table {
- font-family: arial, sans-serif;
- border-collapse: collapse;
- border: 1px solid #bbbbbb;
- width: 100%;
+ font-family: arial, sans-serif;
+ border-collapse: collapse;
+ border: 1px solid #bbbbbb;
+ width: 100%;
}
-td, tr {
- border-left: 2px solid var(--lab-blue);
- text-align: left;
- padding: 8px;
+td,
+tr {
+ text-align: left;
+ padding: 4px 8px;
}
tr {
- background-color: var(--lab-background);
+ background-color: var(--color-init);
}
tr:nth-child(even) {
- background-color: #ffffff;
+ background-color: #ffffff;
}
td:nth-child(even) {
- border-left: 0px;
+ border-left: 0px;
}
.row:after {
- content: "";
- display: table;
- clear: both;
+ content: "";
+ display: table;
+ clear: both;
}
.column {
- float: left;
- width: 50%;
- padding: 10px;
-}
-
-/* Error details */
-
-.error-container {
- margin: 10px;
-}
-
-.error {
- border: 1px solid #aaa;
- border-radius: 4px;
- padding: .5em .5em 0;
- width: 750px;
-}
-
-.summary {
- font-weight: bold;
- margin: -.5em -.5em 0;
- padding: .5em
-}
-
-.code-holder {
- overflow-x: scroll;
-}
-
-.code-holder code {
- background: inherit;
- white-space: pre;
+ float: left;
+ width: 50%;
+ padding: 10px;
}
diff --git a/src/aiidalab_qe/app/static/styles/summary.css b/src/aiidalab_qe/app/static/styles/summary.css
new file mode 100644
index 000000000..c15882a13
--- /dev/null
+++ b/src/aiidalab_qe/app/static/styles/summary.css
@@ -0,0 +1,93 @@
+.workflow-summary-container {
+ gap: 10px;
+}
+
+.workflow-summary-container.list {
+ width: 70%;
+ margin: 0 auto;
+}
+
+@media (max-width: 1199px) {
+ .workflow-summary-container.list {
+ width: 85%;
+ }
+}
+
+@media (max-width: 991px) {
+ .workflow-summary-container.list {
+ width: 100%;
+ }
+}
+
+.summary-panel {
+ border: 1px solid #9e9e9e;
+ padding: 0 20px 20px;
+ margin: 0;
+}
+
+.summary-panel:first-child {
+ width: 200%;
+}
+
+.summary-panel:first-child tr,
+.summary-panel:first-child td {
+ padding: 0 8px;
+}
+
+.summary-panel:first-child td:first-child {
+ width: 250px;
+}
+
+.settings-summary {
+ margin: 2px;
+ font-family: Arial, sans-serif;
+ line-height: 1.6;
+}
+
+.settings-summary ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.settings-summary ul ul {
+ margin-left: 20px;
+}
+
+.settings-summary a {
+ text-decoration: none;
+}
+
+/* Error details */
+
+.error-container {
+ margin: 20px 0 0;
+}
+
+.error-container p {
+ line-height: 1.4;
+}
+
+.error-message {
+ margin-bottom: 10px;
+}
+
+.error-message pre {
+ display: inline;
+}
+
+.error {
+ border: 1px solid #aaa;
+ padding: 0.5em 0.5em 0;
+}
+
+.summary {
+ font-weight: bold;
+ margin: -0.5em -0.5em 0;
+ padding: 0.5em;
+}
+
+.code-holder code {
+ background: inherit;
+ white-space: pre;
+}
diff --git a/src/aiidalab_qe/app/static/templates/workflow_failure.jinja b/src/aiidalab_qe/app/static/templates/workflow_failure.jinja
index b7adb3a8a..3146f972b 100644
--- a/src/aiidalab_qe/app/static/templates/workflow_failure.jinja
+++ b/src/aiidalab_qe/app/static/templates/workflow_failure.jinja
@@ -1,29 +1,26 @@
-
+
+
+ Oh no... It seems the workflow did not complete successfully.
+ Unfortunately, DFT calculations are not guaranteed to run to completion in all cases.
+ You can either consult the workflow report below or download the calculation files to help understand what
+ went wrong.
+
+
+ The returned error message of the final failed calculation is:
+
{{ calcjob_exit_message }}
+
+
+
+
+ View the full workflow report
+
+
+ {{ process_report }}
+
+
-
-Oh no... It seems the workflow did not complete successfully.
-Unfortunately, DFT calculations are not guaranteed to run to completion in all cases.
-You can either consult the workflow report below or download the calculation files to help understand what went wrong.
-
-
-
-The returned error message of the final failed calculation is:
-
-
{{ calcjob_exit_message }}
-
-
-
-
-
- View the full workflow report
-
-
- {{ process_report }}
-
-
-
diff --git a/src/aiidalab_qe/app/static/templates/workflow_list_summary.jinja b/src/aiidalab_qe/app/static/templates/workflow_list_summary.jinja
new file mode 100644
index 000000000..4588d070c
--- /dev/null
+++ b/src/aiidalab_qe/app/static/templates/workflow_list_summary.jinja
@@ -0,0 +1,147 @@
+
+
+
+
+
Workflow properties
+
+ -
+ PK: {{ pk }}
+
+ -
+ UUID: {{ uuid }}
+
+ -
+ Label: {{ label }}
+
+ -
+ Description: {{ description }}
+
+ -
+ Creation time: {{ creation_time }} ({{ creation_time_relative }})
+
+ -
+ Modification time: {{ modification_time }} ({{ modification_time_relative }})
+
+
+
+
+
Structure properties
+
+ -
+ Chemical formula: {{ formula }}
+
+ -
+ Number of atoms: {{ num_atoms }}
+
+ -
+ Space group: {{ space_group }}
+
+ -
+ Cell lengths in Å: {{ cell_lengths }}
+
+ -
+ Cell angles in °: {{ cell_angles }}
+
+
+
+
+
Basic settings
+
+ -
+ Structure geometry optimization: {{ relaxed | fmt_yes_no }}
+
+ {% if relaxed %}
+ -
+ Optimization method: {{ relax_method }}
+
+ {% endif %}
+ -
+ Protocol: {{ protocol }}
+
+ -
+ Magnetism: {{ material_magnetic }}
+
+ -
+ Electronic type: {{ electronic_type }}
+
+ -
+ Periodicity: {{ periodicity }}
+
+
+
+
+
Advanced settings
+
+ -
+ Functional:
+ {{ functional }}
+
+ -
+ Pseudopotential library:
+ {{ pseudo_library }} {{ pseudo_protocol }} v{{ pseudo_version
+ }}
+
+ -
+ Energy cutoff (wave functions): {{ energy_cutoff_wfc }} Ry
+
+ -
+ Energy cutoff (charge density): {{ energy_cutoff_rho }} Ry
+
+ -
+ Occupation type (SCF): {{ occupation_type }}
+
+ {% if occupation_type == "smearing" %}
+
+ -
+ Smearing width (degauss): {{ degauss }} Ry
+
+ -
+ Smearing type: {{ smearing }}
+
+
+ {% endif %}
+ -
+ K-point mesh distance (SCF): {{ scf_kpoints_distance }} Å-1
+
+ {% if bands_computed %}
+ -
+ K-point line distance (Bands): {{ bands_kpoints_distance }} Å-1
+
+ {% endif %}
+ {% if pdos_computed %}
+ -
+ K-point mesh distance (NSCF): {{ nscf_kpoints_distance }} Å-1
+
+ {% endif %}
+ -
+ Total charge: {{ tot_charge }}
+
+ -
+ Van der Waals correction: {{ vdw_corr }}
+
+ {% if material_magnetic == "collinear" %}
+ {% if tot_magnetization %}
+ -
+ Total magnetization: {{ tot_magnetization }}
+
+ {% else %}
+ -
+ Initial magnetic moments: {{ initial_magnetic_moments }}
+
+ {% endif %}
+ {% endif %}
+ {% if hubbard_u %}
+ -
+ DFT+U: {{ hubbard_u }}
+
+ {% endif %}
+ {% if spin_orbit %}
+ -
+ Spin-orbit coupling: {{ spin_orbit }}
+
+ {% endif %}
+
+
+
+
+
diff --git a/src/aiidalab_qe/app/static/templates/workflow_summary.jinja b/src/aiidalab_qe/app/static/templates/workflow_table_summary.jinja
similarity index 64%
rename from src/aiidalab_qe/app/static/templates/workflow_summary.jinja
rename to src/aiidalab_qe/app/static/templates/workflow_table_summary.jinja
index 7d96c9d24..7706dd014 100644
--- a/src/aiidalab_qe/app/static/templates/workflow_summary.jinja
+++ b/src/aiidalab_qe/app/static/templates/workflow_table_summary.jinja
@@ -1,11 +1,64 @@
-
-
-
Standard settings
+
+
Workflow properties
- Structure geometry optimized |
+ PK |
+ {{ pk }} |
+
+
+ UUID |
+ {{ uuid }} |
+
+
+ Label |
+ {{ label }} |
+
+
+ Description |
+ {{ description }} |
+
+
+ Creation time |
+ {{ creation_time }} ({{ creation_time_relative }}) |
+
+
+ Modification time |
+ {{ modification_time }} ({{ modification_time_relative }}) |
+
+
+
+
+
Structure properties
+
+
+ Chemical formula |
+ {{ formula }} |
+
+
+ Number of atoms |
+ {{ num_atoms }} |
+
+
+ Space group |
+ {{ space_group }} |
+
+
+ Cell lengths in Å |
+ {{ cell_lengths }} |
+
+
+ Cell angles in ° |
+ {{ cell_angles }} |
+
+
+
+
+
Basic settings
+
+
+ Structure geometry optimization |
{{ relaxed | fmt_yes_no }} |
{% if relaxed %}
@@ -30,19 +83,18 @@
Periodicity |
{{ periodicity }} |
-
-
-
Advanced settings
+
+
Advanced settings
Functional |
-
+ |
{{ functional }}
- |
+
Pseudopotential library |
@@ -98,7 +150,7 @@
Van der Waals correction |
{{ vdw_corr }} |
-
+ {% if material_magnetic == "collinear" %}
{% if tot_magnetization %}
Total magnetization |
@@ -110,6 +162,7 @@
{{ initial_magnetic_moments }} |
{% endif %}
+ {% endif %}
{% if hubbard_u %}
DFT+U |
@@ -122,9 +175,7 @@
{{ spin_orbit }} |
{% endif %}
-
-
diff --git a/src/aiidalab_qe/common/time.py b/src/aiidalab_qe/common/time.py
new file mode 100644
index 000000000..1e1382e9a
--- /dev/null
+++ b/src/aiidalab_qe/common/time.py
@@ -0,0 +1,25 @@
+from datetime import datetime
+
+from dateutil.relativedelta import relativedelta
+
+
+def format_time(time: datetime):
+ return time.strftime("%Y-%m-%d %H:%M:%S")
+
+
+def relative_time(time: datetime):
+ # TODO consider using humanize or arrow libraries for this
+ now = datetime.now(time.tzinfo)
+ delta = relativedelta(now, time)
+ if delta.years > 0:
+ return f"{delta.years} year{'s' if delta.years > 1 else ''} ago"
+ elif delta.months > 0:
+ return f"{delta.months} month{'s' if delta.months > 1 else ''} ago"
+ elif delta.days > 0:
+ return f"{delta.days} day{'s' if delta.days > 1 else ''} ago"
+ elif delta.hours > 0:
+ return f"{delta.hours} hour{'s' if delta.hours > 1 else ''} ago"
+ elif delta.minutes > 0:
+ return f"{delta.minutes} minute{'s' if delta.minutes > 1 else ''} ago"
+ else:
+ return f"{delta.seconds} second{'s' if delta.seconds > 1 else ''} ago"
diff --git a/tests/test_result.py b/tests/test_result.py
index 3bc2e7f84..db7de7038 100644
--- a/tests/test_result.py
+++ b/tests/test_result.py
@@ -50,6 +50,16 @@ def test_summary_report(data_regression, generate_qeapp_workchain):
model = WorkChainSummaryModel()
model.process_uuid = workchain.node.uuid
report_parameters = model._generate_report_parameters()
+ # Discard variable parameters
+ for key in (
+ "pk",
+ "uuid",
+ "creation_time",
+ "creation_time_relative",
+ "modification_time",
+ "modification_time_relative",
+ ):
+ report_parameters.pop(key)
data_regression.check(report_parameters)
@@ -71,15 +81,13 @@ def test_summary_view(generate_qeapp_workchain):
model.process_uuid = workchain.node.uuid
report_html = model.generate_report_html()
parsed = BeautifulSoup(report_html, "html.parser")
- # find the td with the text "Initial Magnetic Moments"
parameters = {
"Energy cutoff (wave functions)": "30.0 Ry",
"Total charge": "0.0",
- "Initial magnetic moments": "",
}
for key, value in parameters.items():
- td = parsed.find("td", text=key).find_next_sibling("td")
- assert td.text == value
+ li = parsed.find_all(lambda tag, key=key: tag.name == "li" and key in tag.text)
+ assert value in li[0].text
def test_structure_results_panel(generate_qeapp_workchain):
diff --git a/tests/test_result/test_summary_report.yml b/tests/test_result/test_summary_report.yml
index b1ebaa504..583bbbc82 100644
--- a/tests/test_result/test_summary_report.yml
+++ b/tests/test_result/test_summary_report.yml
@@ -1,14 +1,20 @@
bands_computed: true
bands_kpoints_distance: 0.1
+cell_angles: 60 60 60
+cell_lengths: 3.847 3.847 3.847
degauss: 0.01
+description: ''
electronic_type: metal
energy_cutoff_rho: 240.0
energy_cutoff_wfc: 30.0
+formula: Si2
functional: PBEsol
functional_link: https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.100.136406
initial_magnetic_moments: null
+label: ''
material_magnetic: none
nscf_kpoints_distance: 0.5
+num_atoms: 2
occupation_type: smearing
pdos_computed: true
periodicity: xyz
@@ -26,6 +32,7 @@ relax_method: positions_cell
relaxed: positions_cell
scf_kpoints_distance: 0.5
smearing: cold
+space_group: Fd-3m (227)
spin_orbit: false
tot_charge: 0.0
tot_magnetization: false