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"\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", ) - - 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 optimizedPK{{ 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

+ + + {% if relaxed %} @@ -30,19 +83,18 @@ -
Structure geometry optimization {{ relaxed | fmt_yes_no }}
Periodicity {{ periodicity }}
-
-

Advanced settings

+
+

Advanced settings

- + @@ -98,7 +150,7 @@ - + {% if material_magnetic == "collinear" %} {% if tot_magnetization %} @@ -110,6 +162,7 @@ {% endif %} + {% endif %} {% if hubbard_u %} @@ -122,9 +175,7 @@ {% endif %} -
Functional + {{ functional }} -
Pseudopotential libraryVan der Waals correction {{ vdw_corr }}
Total magnetization{{ initial_magnetic_moments }}
DFT+U{{ spin_orbit }}
-
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