Skip to content

Commit d3cfbb0

Browse files
authored
Neb workflow (#22)
1 parent 5e59a57 commit d3cfbb0

7 files changed

Lines changed: 366 additions & 110 deletions

File tree

src/aiidalab_chemshell/common/chemshell.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class WorkflowOptions(Enum):
3030
GEOMETRY = 0
3131
SINGLE_POINT = auto()
3232
ATOMIC_ENERGIES = auto()
33-
# NEB = auto()
33+
NEB = auto()
3434

3535
@property
3636
def label(self) -> str:
@@ -42,8 +42,8 @@ def label(self) -> str:
4242
return "Single Point Energy"
4343
case WorkflowOptions.ATOMIC_ENERGIES:
4444
return "Isolated Atomic Energies"
45-
# case WorkflowOptions.NEB:
46-
# return "Nudged Elastic Band"
45+
case WorkflowOptions.NEB:
46+
return "Nudged Elastic Band"
4747
case _:
4848
return ""
4949

@@ -57,7 +57,7 @@ def tab_label(self) -> str:
5757
return "SP Energy"
5858
case WorkflowOptions.ATOMIC_ENERGIES:
5959
return "Atomic Energies"
60-
# case WorkflowOptions.NEB:
61-
# return "NEB"
60+
case WorkflowOptions.NEB:
61+
return "NEB"
6262
case _:
6363
return "ChemShell"
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Widget for selecting an input structure from various sources."""
2+
3+
from aiida.orm import SinglefileData, StructureData, TrajectoryData
4+
from aiidalab_widgets_base import SmilesWidget
5+
from ase import Atoms
6+
from ipywidgets import HTML, Tab, VBox, dlink
7+
from traitlets import HasTraits, Instance
8+
9+
from aiidalab_chemshell.common.database import AiiDADatabaseWidget
10+
from aiidalab_chemshell.common.file_handling import FileUploadWidget
11+
from aiidalab_chemshell.common.structure_viewer import StructureViewWidget
12+
13+
14+
class StructureSelectionWidget(VBox, HasTraits):
15+
"""Widget for selecting an input structre from various sources."""
16+
17+
structure_data = Instance(StructureData, allow_none=True)
18+
structure_file = Instance(SinglefileData, allow_none=True)
19+
trajectory_data = Instance(TrajectoryData, allow_none=True)
20+
21+
def __init__(self, **kwargs):
22+
"""StructureSelectionWidget constructor."""
23+
super().__init__(**kwargs)
24+
# upload file
25+
self.file_input_widget = VBox()
26+
self.file_uploader = FileUploadWidget(description="Structure file: ")
27+
self.file_input_widget.children = [
28+
self.file_uploader,
29+
]
30+
31+
# AiiDA database
32+
self.database_widget = AiiDADatabaseWidget(
33+
title="AiiDA Database",
34+
query=[SinglefileData, StructureData],
35+
)
36+
37+
self.smiles_widget = SmilesWidget(title="SMILES")
38+
39+
self.tabs = Tab()
40+
self.tabs.children = [
41+
self.file_input_widget,
42+
self.database_widget,
43+
self.smiles_widget,
44+
]
45+
for i, title in enumerate(["Upload File", "AiiDA Database", "SMILES String"]):
46+
self.tabs.set_title(i, title)
47+
48+
self.viewer = HTML("<p>No structure found...</p>")
49+
50+
self.children = [self.tabs, HTML("<h2>Viewer:</h2>"), self.viewer]
51+
52+
self.file_uploader.observe(self._on_file_upload, "file")
53+
self.database_widget.observe(self._on_database_search, "data_object")
54+
self.smiles_widget.observe(self._on_smiles_generation, "structure")
55+
56+
dlink((self.file_uploader, "file"), (self, "structure_file"))
57+
58+
return
59+
60+
def _on_file_upload(self, change: dict) -> None:
61+
"""When file upload button is pressed."""
62+
if change["new"] != change["old"]:
63+
self.viewer = StructureViewWidget()
64+
self.viewer.assign_structure_from_file(
65+
self.file_uploader.file.filename,
66+
self.file_uploader.file.content,
67+
)
68+
self._update_children()
69+
return
70+
71+
def _on_smiles_generation(self, change: dict) -> None:
72+
"""When SMILES string is inputted."""
73+
if change["new"] != change["old"]:
74+
self._create_viewer(change["new"])
75+
self.structure = StructureData(ase=change["new"])
76+
if self.structure_file:
77+
self.structure_file = None
78+
return
79+
80+
def _on_database_search(self, change: dict) -> None:
81+
"""When data is loaded from AiiDA database."""
82+
if change["new"] == change["old"]:
83+
return
84+
if isinstance(change["new"], SinglefileData):
85+
if self.structure_data:
86+
self.structure_data = None
87+
self.structure_file = change["new"]
88+
self._on_file_upload(change)
89+
elif isinstance(change["new"], StructureData):
90+
if self.structure_file:
91+
self.structure_file = None
92+
self.structure = change["new"]
93+
self._create_viewer(change["new"]._get_object_ase())
94+
else:
95+
self._create_viewer(None)
96+
return
97+
98+
def _create_viewer(self, structure: Atoms | None) -> None:
99+
"""Create a viewer widget with the loaded ase.Atoms structure object."""
100+
if structure:
101+
self.viewer = StructureViewWidget()
102+
self.viewer.assign_structure_from_ase(structure)
103+
else:
104+
self.viewer = HTML("<p>Could not visualise structure ...</p>")
105+
self._update_children()
106+
return
107+
108+
def _update_children(self) -> None:
109+
self.children = [
110+
self.tabs,
111+
self.viewer,
112+
]
113+
return
114+
115+
def disable(self, val: bool = True) -> None:
116+
"""Disable the widget and all children."""
117+
for child in self.tabs.children:
118+
try:
119+
child.disable(True)
120+
except AttributeError:
121+
pass # TODO: SmilesWidget cannot be easily disabled at present.

src/aiidalab_chemshell/models/workflow.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212

1313
from aiidalab_chemshell.common.chemshell import BasisSetOptions, WorkflowOptions
14+
from aiidalab_chemshell.models.structure import StructureInputModel
1415

1516

1617
class ChemShellWorkflowModel(HasTraits):
@@ -33,4 +34,6 @@ class ChemShellWorkflowModel(HasTraits):
3334
gradients = Bool(True)
3435
hessian = Bool(False)
3536

37+
structure_2 = StructureInputModel()
38+
3639
default_guide = ""

src/aiidalab_chemshell/process.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,19 @@ def submit_process(self):
103103
self._submit_optimisation_workflow()
104104
case WorkflowOptions.ATOMIC_ENERGIES:
105105
self._submit_atomic_energies_workflow()
106-
case WorkflowOptions.SINGLE_POINT:
107-
self._submit_core_calcjob()
108106
case _:
109-
print("ERROR :: Invalid Workflow Specified...")
107+
self._submit_core_calcjob()
110108
return
111109

112110
def _submit_core_calcjob(self) -> None:
111+
# Get the ChemShell code instance
113112
builder = load_code(self.model.resource_model.code_label).get_builder()
113+
# Configure the structure input
114114
if self.model.structure_model.has_file:
115115
builder.structure = self.model.structure_model.structure_file
116116
else:
117117
builder.structure = self.model.structure_model.structure
118+
# Configure the QM theory input parameters
118119
builder.qm_parameters = Dict(
119120
{
120121
"theory": self.model.workflow_model.qm_theory.name,
@@ -123,6 +124,7 @@ def _submit_core_calcjob(self) -> None:
123124
"basis": self.model.workflow_model.basis_set,
124125
}
125126
)
127+
# Configure MM parameters if QM/MM approach specified
126128
if self.model.workflow_model.use_mm:
127129
builder.mm_parameters = Dict(
128130
{
@@ -137,14 +139,25 @@ def _submit_core_calcjob(self) -> None:
137139
),
138140
}
139141
)
140-
builder.calculation_parameters = Dict(
141-
{
142-
"gradients": self.model.workflow_model.gradients,
143-
"hessian": self.model.workflow_model.hessian,
144-
}
145-
)
146-
if self.model.workflow_model.vibrational_analysis:
142+
# Configure additional SP based tasks
143+
if self.model.workflow_model.workflow == WorkflowOptions.NEB:
144+
builder.optimisation_parameters = Dict({"neb": "frozen"})
145+
if self.model.workflow_model.structure_2.has_file:
146+
builder.structure2 = (
147+
self.model.workflow_model.structure_2.structure_file
148+
)
149+
else:
150+
builder.structure2 = self.model.workflow_model.structure_2.structure
151+
elif self.model.workflow_model.vibrational_analysis:
147152
builder.optimisation_parameters = Dict({"thermal": True})
153+
else:
154+
builder.calculation_parameters = Dict(
155+
{
156+
"gradients": self.model.workflow_model.gradients,
157+
"hessian": self.model.workflow_model.hessian,
158+
}
159+
)
160+
# Setup metadata and resource parameters
148161
if self.model.resource_model.ncpus > 1:
149162
builder.metadata.options.withmpi = True
150163
else:
@@ -155,6 +168,7 @@ def _submit_core_calcjob(self) -> None:
155168
"num_machines": 1,
156169
"tot_num_mpiprocs": self.model.resource_model.ncpus,
157170
}
171+
# Submit and apply the label/description to the CalcJob
158172
self.node = submit(builder)
159173
self.node.label = self.model.resource_model.process_label
160174
self.node.description = self.model.resource_model.process_description

src/aiidalab_chemshell/wizards/structure.py

Lines changed: 11 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
"""Defines the model and view components for the structure setup stage."""
22

3-
import ase
43
import ipywidgets as ipw
5-
from aiida.orm import SinglefileData, StructureData
6-
from aiidalab_widgets_base import SmilesWidget, WizardAppWidgetStep
4+
from aiidalab_widgets_base import WizardAppWidgetStep
75

8-
from aiidalab_chemshell.common.database import AiiDADatabaseWidget
9-
from aiidalab_chemshell.common.file_handling import FileUploadWidget
10-
from aiidalab_chemshell.common.structure_viewer import StructureViewWidget
6+
from aiidalab_chemshell.common.structure_uploader import StructureSelectionWidget
117
from aiidalab_chemshell.models.structure import StructureInputModel
128

139

@@ -42,35 +38,15 @@ def __init__(self, model: StructureInputModel, **kwargs):
4238
"""
4339
)
4440

45-
self.tabs = ipw.Tab()
41+
self.structure_uploader = StructureSelectionWidget()
4642

47-
# upload file
48-
self.file_input_widget = ipw.VBox()
49-
self.file_uploader = FileUploadWidget(description="Structure file: ")
50-
self.file_input_widget.children = [
51-
self.file_uploader,
52-
]
53-
ipw.dlink((self.file_uploader, "file"), (self.model, "structure_file"))
54-
55-
# AiiDA database
56-
self.database_widget = AiiDADatabaseWidget(
57-
title="AiiDA Database",
58-
query=[SinglefileData, StructureData],
43+
ipw.dlink(
44+
(self.structure_uploader, "structure_file"),
45+
(self.model, "structure_file"),
46+
)
47+
ipw.dlink(
48+
(self.structure_uploader, "structure_data"), (self.model, "structure")
5949
)
60-
61-
self.smiles_widget = SmilesWidget(title="SMILES")
62-
63-
self.tabs.children = [
64-
self.file_input_widget,
65-
self.database_widget,
66-
self.smiles_widget,
67-
]
68-
for i, title in enumerate(["Upload File", "AiiDA Database", "SMILES String"]):
69-
self.tabs.set_title(i, title)
70-
71-
self.model.observe(self._on_file_upload, "structure_file")
72-
self.database_widget.observe(self._on_database_search, "data_object")
73-
self.smiles_widget.observe(self._on_smiles_generation, "structure")
7450

7551
def render(self):
7652
"""Render the wizard's contents if not already rendered."""
@@ -95,64 +71,17 @@ def render(self):
9571
def _update_children(self) -> None:
9672
self.children = [
9773
self.info,
98-
self.tabs,
99-
ipw.HTML("<h2>Viewer:</h2>"),
100-
self.viewer,
74+
self.structure_uploader,
10175
self.submit_btn,
10276
]
10377
return
10478

105-
def _on_file_upload(self, change: dict) -> None:
106-
"""When file upload button is pressed."""
107-
if self.model.has_file:
108-
self.viewer = StructureViewWidget()
109-
self.viewer.assign_structure_from_file(
110-
self.model.structure_file.filename, self.model.structure_file.content
111-
)
112-
self._update_children()
113-
return
114-
115-
def _on_smiles_generation(self, change: dict) -> None:
116-
"""When SMILES string is inputted."""
117-
if change["new"] != change["old"]:
118-
self._create_viewer(change["new"])
119-
self.model.structure = StructureData(ase=change["new"])
120-
return
121-
122-
def _on_database_search(self, change: dict) -> None:
123-
"""When data is loaded from AiiDA database."""
124-
if change["new"] == change["old"]:
125-
return
126-
if isinstance(change["new"], SinglefileData):
127-
self.model.structure_file = change["new"]
128-
self._on_file_upload(change)
129-
elif isinstance(change["new"], StructureData):
130-
self.model.structure = change["new"]
131-
self._create_viewer(change["new"]._get_object_ase())
132-
else:
133-
self._create_viewer(None)
134-
return
135-
136-
def _create_viewer(self, structure: ase.Atoms | None) -> None:
137-
"""Create a viewer widget with the loaded ase.Atoms structure object."""
138-
# if structure:
139-
# # self.viewer = awb.viewers.StructureDataViewer(structure=structure)
140-
# self.viewer = WeasWidget()
141-
# self.viewer.from_ase(structure)
142-
# else:
143-
# self.viewer = ipw.HTML("<p>Could not visualise structure ...</p>")
144-
self.viewer = StructureViewWidget()
145-
self.viewer.assign_structure_from_ase(structure)
146-
self._update_children()
147-
return
148-
14979
def submit_structure(self, _):
15080
"""Submit the structure step."""
15181
if self.model.has_file or self.model.has_structure:
152-
self.file_uploader.disable(True)
153-
self.database_widget.disable(True)
15482
self.submit_btn.disabled = True
15583
self.submit_btn.description = "Submitted"
84+
self.structure_uploader.disable(True)
15685
self.model.submitted = True
15786
else:
15887
self.model.submitted = False

0 commit comments

Comments
 (0)