Skip to content

Commit 6c47080

Browse files
authored
ChemShell QM/MM workflow (#13)
1 parent 2e183ae commit 6c47080

13 files changed

Lines changed: 989 additions & 265 deletions

File tree

main.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
},
3030
"outputs": [],
3131
"source": [
32+
"%%capture\n",
3233
"from aiida import load_profile \n",
3334
"load_profile();"
3435
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Package for general common components."""
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
Data,
11+
Node,
12+
QueryBuilder,
13+
WorkChainNode,
14+
)
15+
16+
17+
class AiiDADatabaseWidget(ipw.VBox, tl.HasTraits):
18+
"""Widget for AiiDA database querying."""
19+
20+
data_object = tl.Instance(Data, allow_none=True)
21+
22+
def __init__(self, title: str = "", query: list = None):
23+
if query is None:
24+
query = []
25+
self.title = title
26+
self.query_type = tuple(query)
27+
28+
qbuilder = QueryBuilder().append((CalcJobNode, WorkChainNode), project="label")
29+
30+
self.drop_down = ipw.Dropdown(
31+
options=sorted({"All"}.union({i[0] for i in qbuilder.iterall() if i[0]})),
32+
value="All",
33+
description="Process Label",
34+
disabled=True,
35+
style={"description_width": "120px"},
36+
layout={"width": "50%"},
37+
)
38+
self.drop_down.observe(self.search, names="value")
39+
40+
# Disable process labels selection if we are not looking for the calculated
41+
# structures.
42+
def disable_drop_down(change):
43+
self.drop_down.disabled = not change["new"] == "calculated"
44+
45+
# Select structures kind.
46+
self.mode = ipw.RadioButtons(
47+
options=["all", "uploaded", "calculated"], layout={"width": "25%"}
48+
)
49+
self.mode.observe(self.search, names="value")
50+
self.mode.observe(disable_drop_down, names="value")
51+
52+
# Date range.
53+
# Note: there is Date picker widget, but it currently does not work in Safari:
54+
# https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#Date-picker
55+
date_text = ipw.HTML(value="<p>Select the date range:</p>")
56+
self.start_date_widget = ipw.Text(
57+
value="", description="From: ", style={"description_width": "120px"}
58+
)
59+
self.end_date_widget = ipw.Text(value="", description="To: ")
60+
61+
# Search button.
62+
btn_search = ipw.Button(
63+
description="Search",
64+
button_style="info",
65+
layout={"width": "initial", "margin": "2px 0 0 2em"},
66+
)
67+
btn_search.on_click(self.search)
68+
69+
age_selection = ipw.VBox(
70+
[
71+
date_text,
72+
ipw.HBox([self.start_date_widget, self.end_date_widget, btn_search]),
73+
],
74+
layout={"border": "1px solid #fafafa", "padding": "1em"},
75+
)
76+
77+
h_line = ipw.HTML("<hr>")
78+
box = ipw.VBox([age_selection, h_line, ipw.HBox([self.mode, self.drop_down])])
79+
80+
self.results = ipw.Dropdown(layout={"width": "900px"})
81+
self.results.observe(self._on_select_structure, names="value")
82+
self.search()
83+
super().__init__([box, h_line, self.results])
84+
85+
def search(self, _=None) -> None:
86+
"""Search structures in the AiiDA database."""
87+
qbuild = QueryBuilder()
88+
89+
# If the date range is valid, use it for the search
90+
try:
91+
start_date = datetime.datetime.strptime(
92+
self.start_date_widget.value, "%Y-%m-%d"
93+
)
94+
end_date = datetime.datetime.strptime(
95+
self.end_date_widget.value, "%Y-%m-%d"
96+
) + datetime.timedelta(hours=24)
97+
98+
# Otherwise revert to the standard (i.e. last 7 days)
99+
except ValueError:
100+
start_date = datetime.datetime.now() - datetime.timedelta(days=7)
101+
end_date = datetime.datetime.now() + datetime.timedelta(hours=24)
102+
103+
self.start_date_widget.value = start_date.strftime("%Y-%m-%d")
104+
self.end_date_widget.value = end_date.strftime("%Y-%m-%d")
105+
106+
filters = {}
107+
filters["ctime"] = {"and": [{">": start_date}, {"<=": end_date}]}
108+
109+
if self.mode.value == "uploaded":
110+
qbuild2 = (
111+
QueryBuilder()
112+
.append(self.query_type, project=["id"], tag="structures")
113+
.append(Node, with_outgoing="structures")
114+
)
115+
processed_nodes = [n[0] for n in qbuild2.all()]
116+
if processed_nodes:
117+
filters["id"] = {"!in": processed_nodes}
118+
qbuild.append(self.query_type, filters=filters)
119+
120+
elif self.mode.value == "calculated":
121+
if self.drop_down.value == "All":
122+
qbuild.append((CalcJobNode, WorkChainNode), tag="calcjobworkchain")
123+
else:
124+
qbuild.append(
125+
(CalcJobNode, WorkChainNode),
126+
filters={"label": self.drop_down.value},
127+
tag="calcjobworkchain",
128+
)
129+
qbuild.append(
130+
self.query_type,
131+
with_incoming="calcjobworkchain",
132+
filters=filters,
133+
)
134+
135+
elif self.mode.value == "edited":
136+
qbuild.append(CalcFunctionNode)
137+
qbuild.append(
138+
self.query_type,
139+
with_incoming=CalcFunctionNode,
140+
filters=filters,
141+
)
142+
143+
elif self.mode.value == "all":
144+
qbuild.append(self.query_type, filters=filters)
145+
146+
qbuild.order_by({self.query_type: {"ctime": "desc"}})
147+
matches = {n[0] for n in qbuild.iterall()}
148+
matches = sorted(matches, reverse=True, key=lambda n: n.ctime)
149+
150+
options = [(f"Select a Structure ({len(matches)} found)", False)]
151+
for mch in matches:
152+
label = f"PK: {mch.pk}"
153+
label += " | " + mch.ctime.strftime("%Y-%m-%d %H:%M")
154+
label += " | " + mch.base.extras.get("formula", "")
155+
label += " | " + mch.node_type.split(".")[-2]
156+
label += " | " + mch.label
157+
label += " | " + mch.description
158+
options.append((label, mch))
159+
160+
self.results.options = options
161+
return
162+
163+
def _on_select_structure(self, _) -> None:
164+
self.data_object = self.results.value or None
165+
return
166+
167+
def disable(self, val: bool) -> None:
168+
"""Disable the widget."""
169+
self.results.disabled = True
170+
# self.
171+
return
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Module handling navigation controls within the app."""
2+
3+
from functools import partial
4+
5+
import ipywidgets as ipw
6+
7+
from aiidalab_alc.utils import open_link_in_new_tab
8+
9+
10+
class QuickAccessButtons(ipw.HBox):
11+
"""Quick access buttons present in the apps header and start banner."""
12+
13+
def __init__(self, **kwargs):
14+
"""
15+
QuickAccessButtons constructor.
16+
17+
Parameters
18+
----------
19+
**kwargs :
20+
Keyword arguments passed to the `ipywidgets.HBox.__init__()`.
21+
"""
22+
self.new_calc_link = ipw.Button(
23+
description="New Calculation",
24+
disabled=False,
25+
button_style="success",
26+
tooltip="Start a new calculation",
27+
icon="plus",
28+
)
29+
self.new_calc_link.on_click(
30+
partial(open_link_in_new_tab, "../alc-ux/main.ipynb")
31+
)
32+
33+
self.history_link = ipw.Button(
34+
description="History",
35+
disabled=False,
36+
button_style="primary",
37+
tooltip="View Calculation History",
38+
icon="history",
39+
)
40+
self.history_link.on_click(
41+
partial(open_link_in_new_tab, "../alc-ux/history.ipynb")
42+
)
43+
44+
self.resource_setup_link = ipw.Button(
45+
description="Setup Resources",
46+
disabled=False,
47+
button_style="primary",
48+
tooltip="Configure Computational Resources",
49+
icon="cogs",
50+
# on_click=partial(onLinkClickt get_app_dir() / "../home/code_setup.ipynb"),
51+
)
52+
self.resource_setup_link.on_click(
53+
partial(open_link_in_new_tab, "../home/code_setup.ipynb")
54+
)
55+
56+
self.docs_link = ipw.Button(
57+
description="Documentation",
58+
disabled=False,
59+
button_style="info",
60+
tooltip="Open Documentation",
61+
icon="book",
62+
)
63+
self.docs_link.on_click(
64+
partial(open_link_in_new_tab, "https://github.com/stfc/alc-ux")
65+
)
66+
67+
children = [
68+
self.new_calc_link,
69+
self.history_link,
70+
self.resource_setup_link,
71+
self.docs_link,
72+
]
73+
super().__init__(children=children, layout={"margin": "auto"}, **kwargs)
74+
return

0 commit comments

Comments
 (0)