aiidalab-chemshell 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiidalab_chemshell/__init__.py +5 -0
- aiidalab_chemshell/common/__init__.py +1 -0
- aiidalab_chemshell/common/chemshell.py +53 -0
- aiidalab_chemshell/common/database.py +170 -0
- aiidalab_chemshell/common/file_handling.py +89 -0
- aiidalab_chemshell/common/navigation.py +81 -0
- aiidalab_chemshell/common/node_viewers.py +166 -0
- aiidalab_chemshell/history.py +124 -0
- aiidalab_chemshell/main.py +70 -0
- aiidalab_chemshell/models/__init__.py +1 -0
- aiidalab_chemshell/models/process.py +38 -0
- aiidalab_chemshell/models/resources.py +35 -0
- aiidalab_chemshell/models/results.py +11 -0
- aiidalab_chemshell/models/structure.py +34 -0
- aiidalab_chemshell/models/workflow.py +31 -0
- aiidalab_chemshell/process.py +141 -0
- aiidalab_chemshell/utils.py +106 -0
- aiidalab_chemshell/wizards/__init__.py +1 -0
- aiidalab_chemshell/wizards/main_app.py +70 -0
- aiidalab_chemshell/wizards/resources.py +173 -0
- aiidalab_chemshell/wizards/results.py +84 -0
- aiidalab_chemshell/wizards/structure.py +138 -0
- aiidalab_chemshell/wizards/workflows/__init__.py +5 -0
- aiidalab_chemshell/wizards/workflows/geometry_optimisation.py +153 -0
- aiidalab_chemshell/wizards/workflows/main_view.py +127 -0
- aiidalab_chemshell/wizards/workflows/neb.py +1 -0
- aiidalab_chemshell-0.0.1.dist-info/METADATA +32 -0
- aiidalab_chemshell-0.0.1.dist-info/RECORD +31 -0
- aiidalab_chemshell-0.0.1.dist-info/WHEEL +5 -0
- aiidalab_chemshell-0.0.1.dist-info/licenses/LICENSE +28 -0
- aiidalab_chemshell-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package for general common components."""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Module containing common settings for configuring ChemShell."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BasisSetOptions(Enum):
|
|
7
|
+
"""Pre-defined basis set levels for simplified ChemShell inputs."""
|
|
8
|
+
|
|
9
|
+
FAST = 0
|
|
10
|
+
BALANCED = auto()
|
|
11
|
+
QUALITY = auto()
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def label(self) -> str:
|
|
15
|
+
"""Convert enum value to a string representation for ChemShell input."""
|
|
16
|
+
match self:
|
|
17
|
+
case BasisSetOptions.FAST:
|
|
18
|
+
return "3-21G"
|
|
19
|
+
case BasisSetOptions.BALANCED:
|
|
20
|
+
return "cc-pvdz"
|
|
21
|
+
case BasisSetOptions.QUALITY:
|
|
22
|
+
return "aug-cc-pvtz"
|
|
23
|
+
case "":
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkflowOptions(Enum):
|
|
28
|
+
"""Enum defining the available ChemShell based AiiDA workflows."""
|
|
29
|
+
|
|
30
|
+
GEOMETRY = 0
|
|
31
|
+
NEB = auto()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def label(self) -> str:
|
|
35
|
+
"""Convert enum value into a more human readable string."""
|
|
36
|
+
match self:
|
|
37
|
+
case WorkflowOptions.GEOMETRY:
|
|
38
|
+
return "Geometry Optimisation"
|
|
39
|
+
case WorkflowOptions.NEB:
|
|
40
|
+
return "Nudged Elastic Band"
|
|
41
|
+
case _:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def tab_label(self) -> str:
|
|
46
|
+
"""Create a tab title for the given enum option."""
|
|
47
|
+
match self:
|
|
48
|
+
case WorkflowOptions.GEOMETRY:
|
|
49
|
+
return "Optimisation"
|
|
50
|
+
case WorkflowOptions.NEB:
|
|
51
|
+
return "NEB"
|
|
52
|
+
case _:
|
|
53
|
+
return "ChemShell"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Module for components relating to AiiDA database management."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
import ipywidgets as ipw
|
|
6
|
+
import traitlets as tl
|
|
7
|
+
from aiida.orm import (
|
|
8
|
+
CalcFunctionNode,
|
|
9
|
+
CalcJobNode,
|
|
10
|
+
Node,
|
|
11
|
+
QueryBuilder,
|
|
12
|
+
WorkChainNode,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AiiDADatabaseWidget(ipw.VBox, tl.HasTraits):
|
|
17
|
+
"""Widget for AiiDA database querying."""
|
|
18
|
+
|
|
19
|
+
data_object = tl.Instance(Node, allow_none=True)
|
|
20
|
+
|
|
21
|
+
def __init__(self, title: str = "", query: list | None = None):
|
|
22
|
+
if query is None:
|
|
23
|
+
query = []
|
|
24
|
+
self.title = title
|
|
25
|
+
self.query_type = tuple(query)
|
|
26
|
+
|
|
27
|
+
qbuilder = QueryBuilder().append((CalcJobNode, WorkChainNode), project="label")
|
|
28
|
+
|
|
29
|
+
self.drop_down = ipw.Dropdown(
|
|
30
|
+
options=sorted({"All"}.union({i[0] for i in qbuilder.iterall() if i[0]})),
|
|
31
|
+
value="All",
|
|
32
|
+
description="Process Label",
|
|
33
|
+
disabled=True,
|
|
34
|
+
style={"description_width": "120px"},
|
|
35
|
+
layout={"width": "50%"},
|
|
36
|
+
)
|
|
37
|
+
self.drop_down.observe(self.search, names="value")
|
|
38
|
+
|
|
39
|
+
# Disable process labels selection if we are not looking for the calculated
|
|
40
|
+
# structures.
|
|
41
|
+
def disable_drop_down(change):
|
|
42
|
+
self.drop_down.disabled = not change["new"] == "calculated"
|
|
43
|
+
|
|
44
|
+
# Select structures kind.
|
|
45
|
+
self.mode = ipw.RadioButtons(
|
|
46
|
+
options=["all", "uploaded", "calculated"], layout={"width": "25%"}
|
|
47
|
+
)
|
|
48
|
+
self.mode.observe(self.search, names="value")
|
|
49
|
+
self.mode.observe(disable_drop_down, names="value")
|
|
50
|
+
|
|
51
|
+
# Date range.
|
|
52
|
+
# Note: there is Date picker widget, but it currently does not work in Safari:
|
|
53
|
+
# https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#Date-picker
|
|
54
|
+
date_text = ipw.HTML(value="<p>Select the date range:</p>")
|
|
55
|
+
self.start_date_widget = ipw.Text(
|
|
56
|
+
value="", description="From: ", style={"description_width": "120px"}
|
|
57
|
+
)
|
|
58
|
+
self.end_date_widget = ipw.Text(value="", description="To: ")
|
|
59
|
+
|
|
60
|
+
# Search button.
|
|
61
|
+
btn_search = ipw.Button(
|
|
62
|
+
description="Search",
|
|
63
|
+
button_style="info",
|
|
64
|
+
layout={"width": "initial", "margin": "2px 0 0 2em"},
|
|
65
|
+
)
|
|
66
|
+
btn_search.on_click(self.search)
|
|
67
|
+
|
|
68
|
+
age_selection = ipw.VBox(
|
|
69
|
+
[
|
|
70
|
+
date_text,
|
|
71
|
+
ipw.HBox([self.start_date_widget, self.end_date_widget, btn_search]),
|
|
72
|
+
],
|
|
73
|
+
layout={"border": "1px solid #fafafa", "padding": "1em"},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
h_line = ipw.HTML("<hr>")
|
|
77
|
+
box = ipw.VBox([age_selection, h_line, ipw.HBox([self.mode, self.drop_down])])
|
|
78
|
+
|
|
79
|
+
self.results = ipw.Dropdown(layout={"width": "900px"})
|
|
80
|
+
self.results.observe(self._on_select_structure, names="value")
|
|
81
|
+
self.search()
|
|
82
|
+
super().__init__([box, h_line, self.results])
|
|
83
|
+
|
|
84
|
+
def search(self, _=None) -> None:
|
|
85
|
+
"""Search structures in the AiiDA database."""
|
|
86
|
+
qbuild = QueryBuilder()
|
|
87
|
+
|
|
88
|
+
# If the date range is valid, use it for the search
|
|
89
|
+
try:
|
|
90
|
+
start_date = datetime.datetime.strptime(
|
|
91
|
+
self.start_date_widget.value, "%Y-%m-%d"
|
|
92
|
+
)
|
|
93
|
+
end_date = datetime.datetime.strptime(
|
|
94
|
+
self.end_date_widget.value, "%Y-%m-%d"
|
|
95
|
+
) + datetime.timedelta(hours=24)
|
|
96
|
+
|
|
97
|
+
# Otherwise revert to the standard (i.e. last 7 days)
|
|
98
|
+
except ValueError:
|
|
99
|
+
start_date = datetime.datetime.now() - datetime.timedelta(days=7)
|
|
100
|
+
end_date = datetime.datetime.now() + datetime.timedelta(hours=24)
|
|
101
|
+
|
|
102
|
+
self.start_date_widget.value = start_date.strftime("%Y-%m-%d")
|
|
103
|
+
self.end_date_widget.value = end_date.strftime("%Y-%m-%d")
|
|
104
|
+
|
|
105
|
+
filters = {}
|
|
106
|
+
filters["ctime"] = {"and": [{">": start_date}, {"<=": end_date}]}
|
|
107
|
+
|
|
108
|
+
if self.mode.value == "uploaded":
|
|
109
|
+
qbuild2 = (
|
|
110
|
+
QueryBuilder()
|
|
111
|
+
.append(self.query_type, project=["id"], tag="structures")
|
|
112
|
+
.append(Node, with_outgoing="structures")
|
|
113
|
+
)
|
|
114
|
+
processed_nodes = [n[0] for n in qbuild2.all()]
|
|
115
|
+
if processed_nodes:
|
|
116
|
+
filters["id"] = {"!in": processed_nodes}
|
|
117
|
+
qbuild.append(self.query_type, filters=filters)
|
|
118
|
+
|
|
119
|
+
elif self.mode.value == "calculated":
|
|
120
|
+
if self.drop_down.value == "All":
|
|
121
|
+
qbuild.append((CalcJobNode, WorkChainNode), tag="calcjobworkchain")
|
|
122
|
+
else:
|
|
123
|
+
qbuild.append(
|
|
124
|
+
(CalcJobNode, WorkChainNode),
|
|
125
|
+
filters={"label": self.drop_down.value},
|
|
126
|
+
tag="calcjobworkchain",
|
|
127
|
+
)
|
|
128
|
+
qbuild.append(
|
|
129
|
+
self.query_type,
|
|
130
|
+
with_incoming="calcjobworkchain",
|
|
131
|
+
filters=filters,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
elif self.mode.value == "edited":
|
|
135
|
+
qbuild.append(CalcFunctionNode)
|
|
136
|
+
qbuild.append(
|
|
137
|
+
self.query_type,
|
|
138
|
+
with_incoming=CalcFunctionNode,
|
|
139
|
+
filters=filters,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
elif self.mode.value == "all":
|
|
143
|
+
qbuild.append(self.query_type, filters=filters)
|
|
144
|
+
|
|
145
|
+
qbuild.order_by({self.query_type: {"ctime": "desc"}})
|
|
146
|
+
matches = {n[0] for n in qbuild.iterall()}
|
|
147
|
+
matches = sorted(matches, reverse=True, key=lambda n: n.ctime)
|
|
148
|
+
|
|
149
|
+
options = [(f"Select a Node ({len(matches)} found)", False)]
|
|
150
|
+
for mch in matches:
|
|
151
|
+
label = f"PK: {mch.pk}"
|
|
152
|
+
label += " | " + mch.ctime.strftime("%Y-%m-%d %H:%M")
|
|
153
|
+
label += " | " + mch.base.extras.get("formula", "")
|
|
154
|
+
label += " | " + mch.node_type.split(".")[-2]
|
|
155
|
+
label += " | " + mch.label
|
|
156
|
+
label += " | " + mch.description
|
|
157
|
+
options.append((label, mch))
|
|
158
|
+
|
|
159
|
+
self.results.options = options
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
def _on_select_structure(self, _) -> None:
|
|
163
|
+
self.data_object = self.results.value or None
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
def disable(self, val: bool) -> None:
|
|
167
|
+
"""Disable the widget."""
|
|
168
|
+
self.results.disabled = True
|
|
169
|
+
# self.
|
|
170
|
+
return
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Module for providing functionality to deal with files."""
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
import traitlets as tl
|
|
6
|
+
from aiida.orm import SinglefileData
|
|
7
|
+
from ipywidgets import FileUpload, HBox, Text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileUploadWidget(HBox, tl.HasTraits):
|
|
11
|
+
"""A widget for uploading files."""
|
|
12
|
+
|
|
13
|
+
file = tl.Instance(SinglefileData, allow_none=True)
|
|
14
|
+
|
|
15
|
+
def __init__(self, description: str = "File: ", **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
FileUploadWidget constructor.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
**kwargs :
|
|
22
|
+
Keyword arguments passed to the parent class's constructor.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(**kwargs)
|
|
25
|
+
self.file_dict = None
|
|
26
|
+
|
|
27
|
+
self.file_upload = FileUpload(
|
|
28
|
+
accept="",
|
|
29
|
+
multiple=False,
|
|
30
|
+
description="Upload",
|
|
31
|
+
layout={"width": "20%"},
|
|
32
|
+
)
|
|
33
|
+
self.file_handle = Text(
|
|
34
|
+
value="",
|
|
35
|
+
placeholder="",
|
|
36
|
+
description=description,
|
|
37
|
+
disabled=True,
|
|
38
|
+
layout={"width": "70%"},
|
|
39
|
+
)
|
|
40
|
+
self.children = [self.file_handle, self.file_upload]
|
|
41
|
+
|
|
42
|
+
self.file_upload.observe(self._on_file_upload, names="value")
|
|
43
|
+
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def has_file(self) -> bool:
|
|
48
|
+
"""True if a file has been uploaded."""
|
|
49
|
+
return self.file is not None
|
|
50
|
+
|
|
51
|
+
def _on_file_upload(self, _):
|
|
52
|
+
"""Handle file upload events."""
|
|
53
|
+
if self.file_upload.value:
|
|
54
|
+
self.file_dict = self.file_upload.value[
|
|
55
|
+
list(self.file_upload.value.keys())[0]
|
|
56
|
+
]
|
|
57
|
+
self.file_handle.value = self.file_dict["metadata"]["name"]
|
|
58
|
+
self.file = self.get_aiida_file_object()
|
|
59
|
+
else:
|
|
60
|
+
self.file_handle.value = ""
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
def get_file_contents(self) -> BytesIO | None:
|
|
64
|
+
"""Get the contents of the uploaded file as a BytesIO object."""
|
|
65
|
+
if self.file_dict is not None:
|
|
66
|
+
return BytesIO(self.file_dict["content"])
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def filename(self) -> str:
|
|
70
|
+
"""Get the name of the uploaded file."""
|
|
71
|
+
if self.file_dict is not None:
|
|
72
|
+
return self.file_dict["metadata"]["name"]
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
def get_aiida_file_object(self):
|
|
76
|
+
"""Get the uploaded file as an AiiDA SinglefileData object."""
|
|
77
|
+
if self.file_dict is not None:
|
|
78
|
+
return SinglefileData(
|
|
79
|
+
file=self.get_file_contents(),
|
|
80
|
+
filename=self.filename(),
|
|
81
|
+
label=self.filename(),
|
|
82
|
+
description=self.file_handle.description,
|
|
83
|
+
)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def disable(self, val: bool) -> None:
|
|
87
|
+
"""Disable the file upload widget."""
|
|
88
|
+
self.file_upload.disabled = val
|
|
89
|
+
return
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Module handling navigation controls within the app."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
import ipywidgets as ipw
|
|
6
|
+
|
|
7
|
+
from aiidalab_chemshell.utils import open_link_in_new_tab
|
|
8
|
+
|
|
9
|
+
_APPS_DIRECTORY = "/apps/apps/"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QuickAccessButtons(ipw.HBox):
|
|
13
|
+
"""Quick access buttons present in the apps header and start banner."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
QuickAccessButtons constructor.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
**kwargs :
|
|
22
|
+
Keyword arguments passed to the `ipywidgets.HBox.__init__()`.
|
|
23
|
+
"""
|
|
24
|
+
self.new_calc_link = ipw.Button(
|
|
25
|
+
description="New Calculation",
|
|
26
|
+
disabled=False,
|
|
27
|
+
button_style="success",
|
|
28
|
+
tooltip="Start a new calculation",
|
|
29
|
+
icon="plus",
|
|
30
|
+
)
|
|
31
|
+
self.new_calc_link.on_click(
|
|
32
|
+
partial(
|
|
33
|
+
open_link_in_new_tab,
|
|
34
|
+
_APPS_DIRECTORY + "aiidalab-chemshell/notebooks/main.ipynb",
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.history_link = ipw.Button(
|
|
39
|
+
description="History",
|
|
40
|
+
disabled=False,
|
|
41
|
+
button_style="primary",
|
|
42
|
+
tooltip="View Calculation History",
|
|
43
|
+
icon="history",
|
|
44
|
+
)
|
|
45
|
+
self.history_link.on_click(
|
|
46
|
+
partial(
|
|
47
|
+
open_link_in_new_tab,
|
|
48
|
+
_APPS_DIRECTORY + "aiidalab-chemshell/notebooks/history.ipynb",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.resource_setup_link = ipw.Button(
|
|
53
|
+
description="Setup Resources",
|
|
54
|
+
disabled=False,
|
|
55
|
+
button_style="primary",
|
|
56
|
+
tooltip="Configure Computational Resources",
|
|
57
|
+
icon="cogs",
|
|
58
|
+
)
|
|
59
|
+
self.resource_setup_link.on_click(
|
|
60
|
+
partial(open_link_in_new_tab, _APPS_DIRECTORY + "home/code_setup.ipynb")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.docs_link = ipw.Button(
|
|
64
|
+
description="Documentation",
|
|
65
|
+
disabled=False,
|
|
66
|
+
button_style="info",
|
|
67
|
+
tooltip="Open Documentation",
|
|
68
|
+
icon="book",
|
|
69
|
+
)
|
|
70
|
+
self.docs_link.on_click(
|
|
71
|
+
partial(open_link_in_new_tab, "https://github.com/stfc/aiidalab-chemshell")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
children = [
|
|
75
|
+
self.new_calc_link,
|
|
76
|
+
self.history_link,
|
|
77
|
+
self.resource_setup_link,
|
|
78
|
+
self.docs_link,
|
|
79
|
+
]
|
|
80
|
+
super().__init__(children=children, layout={"margin": "auto"}, **kwargs)
|
|
81
|
+
return
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Defines a custom AiiDA node visualiser."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
5
|
+
|
|
6
|
+
import ase
|
|
7
|
+
from aiida.orm import ArrayData, Node, ProcessNode, SinglefileData, StructureData
|
|
8
|
+
from aiidalab_widgets_base.loaders import LoadingWidget
|
|
9
|
+
from aiidalab_widgets_base.viewers import AIIDA_VIEWER_MAPPING
|
|
10
|
+
from IPython.display import clear_output, display
|
|
11
|
+
from ipywidgets import HTML, DOMWidget, Dropdown, Output, VBox
|
|
12
|
+
from traitlets import Instance, observe
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CustomAiidaNodeViewWidget(VBox):
|
|
16
|
+
"""
|
|
17
|
+
Custom viewer based on a specific AiiDA node type.
|
|
18
|
+
|
|
19
|
+
An extension of the aiida_widgets_base.viewers.AiidaNodeViewWidget
|
|
20
|
+
enabling more customisability when registering viewers with nodes
|
|
21
|
+
returned from ChemShell jobs. The main outline is taken from the base
|
|
22
|
+
aiidalab_widgets_base viewer with an extended viewer() method which
|
|
23
|
+
allows handling of node types which the base viewer has no registered
|
|
24
|
+
visualisation widgets.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
node = Instance(Node, allow_none=True)
|
|
28
|
+
|
|
29
|
+
def __init__(self, **kwargs):
|
|
30
|
+
"""CustomAiidaNodeViewWidget Constructor."""
|
|
31
|
+
self._output = Output()
|
|
32
|
+
self.node_views = {}
|
|
33
|
+
self.node_view_loading_message = LoadingWidget("Loading Node View")
|
|
34
|
+
super().__init__(**kwargs)
|
|
35
|
+
self.add_class("aiida-node-view-widget")
|
|
36
|
+
|
|
37
|
+
@observe("node")
|
|
38
|
+
def _observe_node(self, change):
|
|
39
|
+
if not ((node := change["new"]) and node != change["old"]):
|
|
40
|
+
return
|
|
41
|
+
if node.uuid in self.node_views:
|
|
42
|
+
self.children = [self.node_views[node.uuid]]
|
|
43
|
+
return
|
|
44
|
+
self.children = [self.node_view_loading_message]
|
|
45
|
+
node_view = self._viewer(node)
|
|
46
|
+
if isinstance(node_view, DOMWidget):
|
|
47
|
+
self.node_views[node.uuid] = node_view
|
|
48
|
+
self.children = [node_view]
|
|
49
|
+
else:
|
|
50
|
+
with self._output:
|
|
51
|
+
clear_output()
|
|
52
|
+
if change["new"]:
|
|
53
|
+
display(node_view)
|
|
54
|
+
self.children = [self._output]
|
|
55
|
+
|
|
56
|
+
def _viewer(self, node: Node, **kwargs):
|
|
57
|
+
"""Create a viewer based on the type of Node being visualised."""
|
|
58
|
+
_viewer = AIIDA_VIEWER_MAPPING.get(node.node_type)
|
|
59
|
+
if isinstance(node, ProcessNode):
|
|
60
|
+
# Allow to register specific viewers based on node.process_type
|
|
61
|
+
_viewer = AIIDA_VIEWER_MAPPING.get(node.process_type, _viewer)
|
|
62
|
+
|
|
63
|
+
if _viewer:
|
|
64
|
+
return _viewer(node, **kwargs)
|
|
65
|
+
|
|
66
|
+
# Handle custom ChemShell specific visualisation
|
|
67
|
+
if isinstance(node, SinglefileData):
|
|
68
|
+
# Singlefile data output generally refers to a structure file
|
|
69
|
+
# output from ChemShell jobs
|
|
70
|
+
structure = self._get_structure_data_object_from_file(
|
|
71
|
+
node.filename, node.content
|
|
72
|
+
)
|
|
73
|
+
if structure:
|
|
74
|
+
_viewer = AIIDA_VIEWER_MAPPING.get(structure.node_type)
|
|
75
|
+
if _viewer:
|
|
76
|
+
return _viewer(structure, **kwargs)
|
|
77
|
+
|
|
78
|
+
if isinstance(node, ArrayData):
|
|
79
|
+
return AiidaArrayDataViewWidget(node, **kwargs)
|
|
80
|
+
# No viewer registered for this type, return node itself
|
|
81
|
+
return node
|
|
82
|
+
|
|
83
|
+
def _get_structure_data_object_from_file(
|
|
84
|
+
self, fname: str, content: bytes
|
|
85
|
+
) -> StructureData | None:
|
|
86
|
+
"""
|
|
87
|
+
Create an ase strucure object from a structure file.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
fname : str
|
|
92
|
+
The file name the structure is being read from.
|
|
93
|
+
content : bytes
|
|
94
|
+
The content of the structure file byte encoded.
|
|
95
|
+
|
|
96
|
+
Return
|
|
97
|
+
------
|
|
98
|
+
structure : ase.Atoms | None
|
|
99
|
+
The ASE atomic structure object or None if the file could not be read.
|
|
100
|
+
"""
|
|
101
|
+
suffix = "".join(Path(fname).suffixes)
|
|
102
|
+
with NamedTemporaryFile(suffix=suffix) as tmpf:
|
|
103
|
+
tmpf.write(content)
|
|
104
|
+
tmpf.flush()
|
|
105
|
+
try:
|
|
106
|
+
structure = ase.io.read(tmpf.name, index=":")[0]
|
|
107
|
+
except (KeyError, ase.io.formats.UnknownFileTypeError):
|
|
108
|
+
node = None
|
|
109
|
+
else:
|
|
110
|
+
# ASE doesn't correctly interpret atomic units so convert all units to
|
|
111
|
+
# angstrom
|
|
112
|
+
for i in range(len(structure)):
|
|
113
|
+
structure.positions[i] = structure.positions[i] * 0.529177
|
|
114
|
+
node = StructureData()
|
|
115
|
+
node.set_ase(structure)
|
|
116
|
+
return node
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AiidaArrayDataViewWidget(VBox):
|
|
120
|
+
"""Custom widget to display array data produced from ChemShell jobs."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, array: ArrayData, **kwargs):
|
|
123
|
+
"""AiidaArrayDataViewWidget Constructor.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
array : ArrayData
|
|
128
|
+
The AiiDA ArrayData object to display.
|
|
129
|
+
"""
|
|
130
|
+
super().__init__(**kwargs)
|
|
131
|
+
self.array = array
|
|
132
|
+
self.array_names = array.get_arraynames()
|
|
133
|
+
|
|
134
|
+
self.array_selector = Dropdown(
|
|
135
|
+
options=self.array_names,
|
|
136
|
+
description="Array Label:",
|
|
137
|
+
disabled=False,
|
|
138
|
+
layout={"width": "30%"},
|
|
139
|
+
)
|
|
140
|
+
self._render_array({"new": self.array_selector.index, "old": -1})
|
|
141
|
+
self.array_selector.observe(self._render_array, "index")
|
|
142
|
+
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
def _render_array(self, change) -> None:
|
|
146
|
+
"""Create a HTML table based on the currently selected array."""
|
|
147
|
+
index = change["new"]
|
|
148
|
+
if index == change["old"]:
|
|
149
|
+
return
|
|
150
|
+
values = self.array.get_array(self.array_names[index])
|
|
151
|
+
# Construct HTML Table
|
|
152
|
+
html = "<table style='width:100%; border: 1px solid #ddd; text-align: left; "
|
|
153
|
+
html += "border-collapse: collapse;'>"
|
|
154
|
+
html += "<tr style='background-color: #2196F3; color: white;'>"
|
|
155
|
+
html += "<th>Atom Index</th><th>X</th><th>Y</th><th>Z</th></tr>"
|
|
156
|
+
|
|
157
|
+
for idx, row in enumerate(values):
|
|
158
|
+
bg_color = "#f9f9f9" if idx % 2 == 0 else "#ffffff"
|
|
159
|
+
html += f"<tr style='background-color: {bg_color};'>"
|
|
160
|
+
html += f"<td><b>{idx}</b></td><td>{row[0]:.6f}</td><td>{row[1]:.6f}</td>"
|
|
161
|
+
html += f"<td>{row[2]:.6f}</td>"
|
|
162
|
+
html += "</tr>"
|
|
163
|
+
html += "</table>"
|
|
164
|
+
|
|
165
|
+
self.children = [self.array_selector, HTML(html)]
|
|
166
|
+
return
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Defines the process history applicaion page."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from aiida.orm import CalcJobNode, WorkChainNode
|
|
6
|
+
from aiidalab_widgets_base import ProcessNodesTreeWidget
|
|
7
|
+
from IPython.display import display
|
|
8
|
+
from ipywidgets import HTML, VBox, dlink
|
|
9
|
+
|
|
10
|
+
from aiidalab_chemshell.common.database import AiiDADatabaseWidget
|
|
11
|
+
from aiidalab_chemshell.common.navigation import QuickAccessButtons
|
|
12
|
+
from aiidalab_chemshell.common.node_viewers import CustomAiidaNodeViewWidget
|
|
13
|
+
from aiidalab_chemshell.models.process import ProcessModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HistoryApp:
|
|
17
|
+
"""The process history page's main app."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""HistoryApp constructor."""
|
|
21
|
+
self.model = HistoryModel()
|
|
22
|
+
self.view = HistoryAppView(self.model)
|
|
23
|
+
display(self.view)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HistoryModel(ProcessModel):
|
|
27
|
+
"""MVC Model for process history app data management."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HistoryAppView(VBox):
|
|
33
|
+
"""Main view for the process history page."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, model: HistoryModel, **kwargs):
|
|
36
|
+
"""
|
|
37
|
+
HistoryAppView Constructor.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
model : HistoryModel
|
|
42
|
+
The MVC model component to associate with this view app.
|
|
43
|
+
"""
|
|
44
|
+
self.model = model
|
|
45
|
+
logo = HTML(
|
|
46
|
+
"""
|
|
47
|
+
<div class="app-container logo" style="width: 300px;">
|
|
48
|
+
<img src="../images/alc.svg" alt="ALC AiiDAlab App Logo" />
|
|
49
|
+
</div>
|
|
50
|
+
""",
|
|
51
|
+
layout={"margin": "auto"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
subtitle = HTML(
|
|
55
|
+
"""
|
|
56
|
+
<h2 id='subtitle'>AiiDAlab ChemShell</h2>
|
|
57
|
+
"""
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
nav_btns = QuickAccessButtons()
|
|
61
|
+
|
|
62
|
+
header = VBox(
|
|
63
|
+
children=[
|
|
64
|
+
logo,
|
|
65
|
+
subtitle,
|
|
66
|
+
],
|
|
67
|
+
layout={"margin": "auto"},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
footer = HTML(
|
|
71
|
+
f"""
|
|
72
|
+
<footer>
|
|
73
|
+
Copyright (c) {datetime.now().year} Ada Lovelace Centre
|
|
74
|
+
(STFC) <br>
|
|
75
|
+
</footer>
|
|
76
|
+
""",
|
|
77
|
+
layout={"align-content": "right"},
|
|
78
|
+
)
|
|
79
|
+
h_line = HTML("<hr>")
|
|
80
|
+
|
|
81
|
+
self.guide = HTML(
|
|
82
|
+
"""
|
|
83
|
+
<h3>ChemShell Process History</h3>
|
|
84
|
+
<p>
|
|
85
|
+
Search through past ChemShell processes and visualise inputs, outputs and
|
|
86
|
+
provenance relationships.
|
|
87
|
+
</p>
|
|
88
|
+
"""
|
|
89
|
+
)
|
|
90
|
+
self.lookup_widget = AiiDADatabaseWidget(
|
|
91
|
+
"Process Lookup", [CalcJobNode, WorkChainNode]
|
|
92
|
+
)
|
|
93
|
+
self.lookup_widget.observe(self._update_node_view, "data_object")
|
|
94
|
+
|
|
95
|
+
self.node_tree = ProcessNodesTreeWidget()
|
|
96
|
+
dlink((self.model, "process_uuid"), (self.node_tree, "value"))
|
|
97
|
+
self.node_view = CustomAiidaNodeViewWidget()
|
|
98
|
+
dlink(
|
|
99
|
+
(self.node_tree, "selected_nodes"),
|
|
100
|
+
(self.node_view, "node"),
|
|
101
|
+
transform=lambda nodes: nodes[0] if nodes else None,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
super().__init__(
|
|
105
|
+
layout={},
|
|
106
|
+
children=[
|
|
107
|
+
header,
|
|
108
|
+
nav_btns,
|
|
109
|
+
self.guide,
|
|
110
|
+
self.lookup_widget,
|
|
111
|
+
h_line,
|
|
112
|
+
self.node_tree,
|
|
113
|
+
self.node_view,
|
|
114
|
+
footer,
|
|
115
|
+
],
|
|
116
|
+
**kwargs,
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
def _update_node_view(self, _) -> None:
|
|
121
|
+
"""Update the node view to the currently selected process node."""
|
|
122
|
+
if self.lookup_widget.data_object is not None:
|
|
123
|
+
self.model.process_uuid = self.lookup_widget.data_object.uuid
|
|
124
|
+
return
|