aiida-chemshell 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {aiida_chemshell-0.3.1/src/aiida_chemshell.egg-info → aiida_chemshell-0.3.2}/PKG-INFO +2 -1
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/pyproject.toml +2 -2
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/base.py +10 -5
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/utils.py +82 -1
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/parsers/base.py +11 -2
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/batch_calculation.py +1 -10
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/isolated_atoms.py +19 -70
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/optimisation.py +9 -131
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2/src/aiida_chemshell.egg-info}/PKG-INFO +2 -1
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/SOURCES.txt +0 -3
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/requires.txt +1 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_inputs.py +46 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_isolated_atoms_workchain.py +42 -6
- aiida_chemshell-0.3.1/src/aiida_chemshell/calculations/file_conversion.py +0 -206
- aiida_chemshell-0.3.1/src/aiida_chemshell/calculations/solvant_removal.py +0 -115
- aiida_chemshell-0.3.1/src/aiida_chemshell/parsers/file_conversion.py +0 -54
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/LICENSE +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/README.md +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/setup.cfg +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/__init__.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/__init__.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/parsers/__init__.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/periodic_table.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/units.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/utils.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/__init__.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/utils.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/dependency_links.txt +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/entry_points.txt +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/top_level.txt +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_batch_workcahin.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_calculations.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_error_codes.py +0 -0
- {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_workflows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiida-chemshell
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: AiiDA workflow plugin for the ChemShell chemical modelling software package
|
|
5
5
|
Author-email: "Dr. Benjamin T. Speake" <benjamin.speake@stfc.ac.uk>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -42,6 +42,7 @@ Requires-Python: >=3.10
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: aiida-core>=2.5
|
|
45
|
+
Requires-Dist: ase>=3.29
|
|
45
46
|
Provides-Extra: dev
|
|
46
47
|
Requires-Dist: aiida-core>=2.8; extra == "dev"
|
|
47
48
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -3,13 +3,13 @@ requires = ["setuptools"]
|
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
|
-
version = "0.3.
|
|
6
|
+
version = "0.3.2"
|
|
7
7
|
name = "aiida-chemshell"
|
|
8
8
|
description = "AiiDA workflow plugin for the ChemShell chemical modelling software package"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Dr. Benjamin T. Speake", email = "benjamin.speake@stfc.ac.uk" },
|
|
11
11
|
]
|
|
12
|
-
dependencies = ["aiida-core >= 2.5"]
|
|
12
|
+
dependencies = ["aiida-core >= 2.5", "ase>=3.29"]
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
license = { file = "LICENSE" }
|
|
15
15
|
keywords = ["aiida", "workflows", "chemshell"]
|
|
@@ -236,7 +236,10 @@ class ChemShellCalculation(CalcJob):
|
|
|
236
236
|
"vibrational_modes",
|
|
237
237
|
valid_type=ArrayData,
|
|
238
238
|
required=False,
|
|
239
|
-
help=
|
|
239
|
+
help=(
|
|
240
|
+
"The calculated vibrational modes of the system. A negative "
|
|
241
|
+
"frequency indicates an imaginary frequency."
|
|
242
|
+
),
|
|
240
243
|
)
|
|
241
244
|
|
|
242
245
|
# Validate inputs namespace
|
|
@@ -839,6 +842,8 @@ class ChemShellCalculation(CalcJob):
|
|
|
839
842
|
if "optimisation_parameters" in node.inputs:
|
|
840
843
|
if node.inputs.optimisation_parameters.get("thermal", False):
|
|
841
844
|
return "ChemShell_Vibrational_Frequencies" + theory_key
|
|
845
|
+
if node.inputs.optimisation_parameters.get("neb", "no") != "no":
|
|
846
|
+
return "ChemShell_NEB" + theory_key
|
|
842
847
|
return "ChemShell_Geometry_Optimisation" + theory_key
|
|
843
848
|
|
|
844
849
|
return "ChemShell_Single_Point_Calculation" + theory_key
|
|
@@ -888,7 +893,6 @@ class ChemShellCalculation(CalcJob):
|
|
|
888
893
|
fname = self.inputs.structure2.filename
|
|
889
894
|
else:
|
|
890
895
|
fname = ChemShellCalculation.FILE_TMP_STRUCTURE2
|
|
891
|
-
print(fname)
|
|
892
896
|
script += f"frg2 = Fragment(coords='{fname:s}')\n"
|
|
893
897
|
|
|
894
898
|
## Setup Theory objects
|
|
@@ -1016,8 +1020,9 @@ class ChemShellCalculation(CalcJob):
|
|
|
1016
1020
|
node = self.inputs[port_name]
|
|
1017
1021
|
if not node.label:
|
|
1018
1022
|
node.label = label
|
|
1019
|
-
|
|
1020
|
-
|
|
1023
|
+
if not node.description:
|
|
1024
|
+
node.description = description
|
|
1025
|
+
return
|
|
1021
1026
|
|
|
1022
1027
|
def prepare_for_submission(self, folder: Folder) -> CalcInfo:
|
|
1023
1028
|
"""
|
|
@@ -1101,7 +1106,7 @@ class ChemShellCalculation(CalcJob):
|
|
|
1101
1106
|
# Copy data for second input fragment if required
|
|
1102
1107
|
if "structure2" in self.inputs:
|
|
1103
1108
|
if isinstance(self.inputs.structure2, StructureData):
|
|
1104
|
-
with folder.open(ChemShellCalculation.
|
|
1109
|
+
with folder.open(ChemShellCalculation.FILE_TMP_STRUCTURE2, "wb") as f:
|
|
1105
1110
|
f.write(self.inputs.structure2._prepare_xyz()[0])
|
|
1106
1111
|
else:
|
|
1107
1112
|
calc_info.local_copy_list.append(
|
|
@@ -15,7 +15,88 @@ from aiida.orm import (
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@calcfunction
|
|
18
|
-
def
|
|
18
|
+
def create_isolated_atom_structures(structure) -> dict[str, StructureData]:
|
|
19
|
+
"""
|
|
20
|
+
Create an isolated single-atom StructureData for each unique atom type.
|
|
21
|
+
|
|
22
|
+
The unique atom types present in ``structure`` are determined and, for each,
|
|
23
|
+
a non-periodic StructureData node containing a single atom at the origin is
|
|
24
|
+
created.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
structure : StructureData | SinglefileData
|
|
29
|
+
The input structure to extract isolated atomic species from. Either a
|
|
30
|
+
StructureData node or a SinglefileData node wrapping a structure file in
|
|
31
|
+
any format readable by ``ase.io.read``.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
dict[str, StructureData]
|
|
36
|
+
A mapping of atom type to its isolated single-atom StructureData node.
|
|
37
|
+
As a calcfunction return value, this is registered as a namespace of
|
|
38
|
+
output nodes, with the atom type used as the output link label.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
------
|
|
42
|
+
Exception
|
|
43
|
+
If the structure file format is not supported by the ASE reader.
|
|
44
|
+
"""
|
|
45
|
+
unique_atoms = _determine_unique_atoms(structure)
|
|
46
|
+
structures = {}
|
|
47
|
+
for atom_symbol in unique_atoms:
|
|
48
|
+
atom = StructureData()
|
|
49
|
+
atom.append_atom(position=(0.0, 0.0, 0.0), symbols=atom_symbol)
|
|
50
|
+
atom.set_pbc((False, False, False))
|
|
51
|
+
atom.label = f"{atom_symbol} atom"
|
|
52
|
+
atom.description = (
|
|
53
|
+
f"Isolated {atom_symbol} atom extracted from Node: {structure.pk}"
|
|
54
|
+
)
|
|
55
|
+
structures[atom_symbol] = atom
|
|
56
|
+
return structures
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _determine_unique_atoms(structure: SinglefileData | StructureData) -> list[str]:
|
|
60
|
+
"""Determine all unique atom types within the given structure."""
|
|
61
|
+
if isinstance(structure, StructureData):
|
|
62
|
+
return _atom_types_from_structuredata(structure)
|
|
63
|
+
return _atom_types_from_file(structure)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _atom_types_from_structuredata(structure: StructureData) -> list[str]:
|
|
67
|
+
"""Determine the unique atom types from a StructureData object."""
|
|
68
|
+
unique_atoms: list[str] = []
|
|
69
|
+
for site in structure.sites:
|
|
70
|
+
if site.kind_name not in unique_atoms:
|
|
71
|
+
unique_atoms.append(site.kind_name)
|
|
72
|
+
return unique_atoms
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _atom_types_from_file(structure: SinglefileData) -> list[str]:
|
|
76
|
+
"""Determine the unique atom types from a SinglefileData object using ASE."""
|
|
77
|
+
import os
|
|
78
|
+
import tempfile
|
|
79
|
+
|
|
80
|
+
from ase.io import read
|
|
81
|
+
from ase.io.formats import UnknownFileTypeError
|
|
82
|
+
|
|
83
|
+
suffix = os.path.splitext(structure.filename)[1]
|
|
84
|
+
with tempfile.NamedTemporaryFile(suffix=suffix) as tmp:
|
|
85
|
+
with structure.open(mode="rb") as f:
|
|
86
|
+
tmp.write(f.read())
|
|
87
|
+
tmp.flush()
|
|
88
|
+
try:
|
|
89
|
+
atoms = read(tmp.name)
|
|
90
|
+
except UnknownFileTypeError as e:
|
|
91
|
+
raise Exception(
|
|
92
|
+
f"Unsupported structure file format for isolated atom extraction: "
|
|
93
|
+
f"{structure.filename}"
|
|
94
|
+
) from e
|
|
95
|
+
return list(dict.fromkeys(atoms.get_chemical_symbols()))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@calcfunction
|
|
99
|
+
def create_atomic_energy_dictionary(atoms, energies) -> Dict:
|
|
19
100
|
"""Collate a series of isolated atom energies into a dictionary output."""
|
|
20
101
|
if len(atoms) != len(energies):
|
|
21
102
|
raise ValueError(
|
|
@@ -163,8 +163,17 @@ class ChemShellParser(Parser):
|
|
|
163
163
|
elif "Mode" in line or "total" in line:
|
|
164
164
|
pass
|
|
165
165
|
else:
|
|
166
|
-
# All remaining lines should be part of the modes table
|
|
167
|
-
|
|
166
|
+
# All remaining lines should be part of the modes table.
|
|
167
|
+
# Imaginary frequencies are printed with a trailing "i"
|
|
168
|
+
# (e.g. "100.0i") and are stored as the negative value.
|
|
169
|
+
modes.append(
|
|
170
|
+
numpy.array(
|
|
171
|
+
[
|
|
172
|
+
-float(x[:-1]) if x.endswith("i") else float(x)
|
|
173
|
+
for x in line_vals[2:]
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
)
|
|
168
177
|
|
|
169
178
|
if "Thermochemical analysis" in line:
|
|
170
179
|
read = True
|
{aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/batch_calculation.py
RENAMED
|
@@ -113,16 +113,7 @@ class BatchProcessWorkChain(WorkChain):
|
|
|
113
113
|
def apply_default_input_tags(self) -> None:
|
|
114
114
|
"""Apply default labels/descriptions to WorkChain specific input nodes."""
|
|
115
115
|
apply_default_input_node_tags(self.inputs, self.DEFAULT_INPUT_TAGS)
|
|
116
|
-
|
|
117
|
-
# @classmethod
|
|
118
|
-
# def validate_combination_input_key(cls, key: str | None, _) -> str | None:
|
|
119
|
-
# """Validate the combine_results input key."""
|
|
120
|
-
# if key in ["xyz", "trajectory"]:
|
|
121
|
-
# return None
|
|
122
|
-
# return (
|
|
123
|
-
# f"Invalid input for 'combine_results'. {key} must be either 'xyz' or "
|
|
124
|
-
# "'trajectory'"
|
|
125
|
-
# )
|
|
116
|
+
return
|
|
126
117
|
|
|
127
118
|
def validate_inputs(self):
|
|
128
119
|
"""Validate the inputs provided to the WorkChain."""
|
{aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/isolated_atoms.py
RENAMED
|
@@ -4,8 +4,10 @@ from aiida.engine import ToContext, WorkChain
|
|
|
4
4
|
from aiida.orm import Dict, StructureData
|
|
5
5
|
from aiida.plugins.factories import CalculationFactory
|
|
6
6
|
|
|
7
|
-
from aiida_chemshell.calculations.utils import
|
|
8
|
-
|
|
7
|
+
from aiida_chemshell.calculations.utils import (
|
|
8
|
+
create_atomic_energy_dictionary,
|
|
9
|
+
create_isolated_atom_structures,
|
|
10
|
+
)
|
|
9
11
|
from aiida_chemshell.workflows.utils import apply_default_input_node_tags
|
|
10
12
|
|
|
11
13
|
ChemShellCalculation = CalculationFactory("chemshell")
|
|
@@ -46,7 +48,7 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
|
|
|
46
48
|
|
|
47
49
|
spec.outline(
|
|
48
50
|
cls.apply_default_input_tags,
|
|
49
|
-
cls.
|
|
51
|
+
cls.create_atom_structures,
|
|
50
52
|
cls.atom_energies,
|
|
51
53
|
cls.result,
|
|
52
54
|
)
|
|
@@ -57,31 +59,21 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
|
|
|
57
59
|
"""Apply default labels/descriptions to WorkChain specific input nodes."""
|
|
58
60
|
apply_default_input_node_tags(self.inputs, self.DEFAULT_INPUT_TAGS)
|
|
59
61
|
|
|
60
|
-
def
|
|
61
|
-
"""
|
|
62
|
-
self.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
def create_atom_structures(self) -> None:
|
|
63
|
+
"""Create provenance-tracked isolated atom structures for each atom type."""
|
|
64
|
+
if self.inputs.qm_parameters["theory"] == "PySCF":
|
|
65
|
+
raise Exception(
|
|
66
|
+
"Isolated atom calculations not supported by PySCF QM backend."
|
|
67
|
+
)
|
|
68
|
+
self.ctx.atom_structures = create_isolated_atom_structures(
|
|
69
|
+
self.inputs.structure
|
|
70
|
+
)
|
|
67
71
|
return
|
|
68
72
|
|
|
69
73
|
def atom_energies(self):
|
|
70
74
|
"""Run ChemShell single point calculations for each atom type."""
|
|
71
75
|
calculations = {}
|
|
72
|
-
|
|
73
|
-
raise Exception(
|
|
74
|
-
"Isolated atom calculations not supported by PySCF QM backend."
|
|
75
|
-
)
|
|
76
|
-
for atom_symbol in self.unique_atoms:
|
|
77
|
-
structure = StructureData()
|
|
78
|
-
structure.append_atom(position=(0.0, 0.0, 0.0), symbols=atom_symbol)
|
|
79
|
-
structure.set_pbc((False, False, False))
|
|
80
|
-
structure.label = f"{atom_symbol} atom"
|
|
81
|
-
structure.description = (
|
|
82
|
-
f"Isolated {atom_symbol} atom extracted from Node: "
|
|
83
|
-
f"{self.inputs.structure.pk}"
|
|
84
|
-
)
|
|
76
|
+
for atom_symbol, structure in self.ctx.atom_structures.items():
|
|
85
77
|
inputs = {
|
|
86
78
|
"structure": structure,
|
|
87
79
|
"qm_parameters": self.inputs.qm_parameters,
|
|
@@ -95,9 +87,10 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
|
|
|
95
87
|
|
|
96
88
|
def result(self) -> None:
|
|
97
89
|
"""Collect the results into a dictionary."""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
unique_atoms = list(self.ctx.atom_structures.keys())
|
|
91
|
+
results_dict = create_atomic_energy_dictionary(
|
|
92
|
+
unique_atoms,
|
|
93
|
+
[self.ctx.get(atom).outputs.energy for atom in unique_atoms],
|
|
101
94
|
)
|
|
102
95
|
if isinstance(self.inputs.structure, StructureData):
|
|
103
96
|
results_dict.label = (
|
|
@@ -111,47 +104,3 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
|
|
|
111
104
|
)
|
|
112
105
|
self.out("atom_energies", results_dict)
|
|
113
106
|
return
|
|
114
|
-
|
|
115
|
-
def _atom_types_from_structuredata(self, structure: StructureData) -> None:
|
|
116
|
-
"""Determine the unique atom types from a StructureData object."""
|
|
117
|
-
self.unique_atoms = []
|
|
118
|
-
for site in structure.sites:
|
|
119
|
-
if site.kind_name not in self.unique_atoms:
|
|
120
|
-
self.unique_atoms.append(site.kind_name)
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
def _atom_types_from_file(self) -> None:
|
|
124
|
-
"""Determine the unique atom types from a SinglefileData object."""
|
|
125
|
-
if self.inputs.structure.filename[-4:] == ".xyz":
|
|
126
|
-
self._atom_types_from_xyz()
|
|
127
|
-
elif self.inputs.structure.filename[-6:] == ".cjson":
|
|
128
|
-
self._atom_types_from_cjson()
|
|
129
|
-
elif self.inputs.structure.filename[-4:] == ".pun":
|
|
130
|
-
self._atom_types_from_pun()
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
def _atom_types_from_xyz(self) -> None:
|
|
134
|
-
"""Determine the unique atom types from an xyz structure file."""
|
|
135
|
-
structure = StructureData()
|
|
136
|
-
structure._parse_xyz(self.inputs.structure.content.decode("utf-8"))
|
|
137
|
-
self._atom_types_from_structuredata(structure)
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
def _atom_types_from_cjson(self) -> None:
|
|
141
|
-
"""Determine the unique atom types from a cjson structure file."""
|
|
142
|
-
import json
|
|
143
|
-
|
|
144
|
-
data = json.loads(self.inputs.structure.content).get("atoms").get("elements")
|
|
145
|
-
atom_symbols: list[str] = data.get("symbol", [])
|
|
146
|
-
atom_numbers: list[int] = data.get("number", [])
|
|
147
|
-
if len(atom_symbols) == 0:
|
|
148
|
-
self.unique_atoms = list(
|
|
149
|
-
{PeriodicTable.atom_z_to_symbol(number) for number in atom_numbers}
|
|
150
|
-
)
|
|
151
|
-
else:
|
|
152
|
-
self.unique_atoms = list(set(atom_symbols))
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
def _atom_types_from_pun(self) -> None:
|
|
156
|
-
"""Determine the unique atom types from a .pun structure file."""
|
|
157
|
-
return
|
{aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/optimisation.py
RENAMED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
"""Workflows for geometry optimisation based taks."""
|
|
2
2
|
|
|
3
|
-
from aiida.common.exceptions import MissingEntryPointError
|
|
4
3
|
from aiida.engine import ToContext, WorkChain
|
|
5
4
|
from aiida.orm import (
|
|
6
5
|
ArrayData,
|
|
7
6
|
Bool,
|
|
8
|
-
Code,
|
|
9
7
|
Dict,
|
|
10
8
|
Float,
|
|
11
9
|
SinglefileData,
|
|
12
10
|
StructureData,
|
|
13
11
|
)
|
|
14
|
-
from aiida.plugins.factories import CalculationFactory
|
|
15
12
|
|
|
16
13
|
from aiida_chemshell.calculations.base import ChemShellCalculation
|
|
17
14
|
from aiida_chemshell.workflows.isolated_atoms import IsolatedAtomicEnergiesWorkChain
|
|
@@ -27,10 +24,6 @@ class GeometryOptimisationWorkChain(WorkChain):
|
|
|
27
24
|
"Vibrational Analysis Flag",
|
|
28
25
|
"Whether to calculate the vibrational modes of the optimised structure.",
|
|
29
26
|
),
|
|
30
|
-
"mlip_model": (
|
|
31
|
-
"MLIP Foundation Model",
|
|
32
|
-
"The MLIP foundation model to apply fine-tuning to.",
|
|
33
|
-
),
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
@classmethod
|
|
@@ -71,48 +64,20 @@ class GeometryOptimisationWorkChain(WorkChain):
|
|
|
71
64
|
"vibrational_modes",
|
|
72
65
|
valid_type=ArrayData,
|
|
73
66
|
required=False,
|
|
74
|
-
help=
|
|
67
|
+
help=(
|
|
68
|
+
"The calculated vibrational modes for the optimised structure. A "
|
|
69
|
+
"negative frequency indicates an imaginary frequency."
|
|
70
|
+
),
|
|
75
71
|
)
|
|
76
72
|
|
|
77
|
-
# Optional inputs/outputs for using the results to fine tune a MLIP model
|
|
78
|
-
# using the janus-core project via the aiida-mlip plugin.
|
|
79
|
-
try:
|
|
80
|
-
CalculationFactory("mlip.train")
|
|
81
|
-
except MissingEntryPointError:
|
|
82
|
-
pass
|
|
83
|
-
else:
|
|
84
|
-
from aiida_mlip.data.model import ModelData
|
|
85
|
-
|
|
86
|
-
spec.input(
|
|
87
|
-
"mlip_model",
|
|
88
|
-
valid_type=ModelData,
|
|
89
|
-
required=False,
|
|
90
|
-
help="The MLIP foundation model to apply fine-tuning to.",
|
|
91
|
-
)
|
|
92
|
-
spec.input(
|
|
93
|
-
"mlip_code",
|
|
94
|
-
valid_type=Code,
|
|
95
|
-
required=False,
|
|
96
|
-
help="The Janus-core AiiDA code instance.",
|
|
97
|
-
)
|
|
98
|
-
spec.output(
|
|
99
|
-
"fine_tuned_model", valid_type=ModelData, required=False, help=""
|
|
100
|
-
)
|
|
101
|
-
spec.output(
|
|
102
|
-
"fine_tuned_model_compiled",
|
|
103
|
-
valid_type=SinglefileData,
|
|
104
|
-
required=False,
|
|
105
|
-
help="",
|
|
106
|
-
)
|
|
107
|
-
|
|
108
73
|
## Workflow ##
|
|
109
74
|
spec.outline(
|
|
110
75
|
cls.apply_default_input_tags,
|
|
111
76
|
cls.optimise,
|
|
112
77
|
cls.energy,
|
|
113
78
|
cls.isolated_atom_energies,
|
|
114
|
-
cls.generate_mlip_training_inputs,
|
|
115
|
-
cls.train_mlip,
|
|
79
|
+
# cls.generate_mlip_training_inputs,
|
|
80
|
+
# cls.train_mlip,
|
|
116
81
|
cls.result,
|
|
117
82
|
)
|
|
118
83
|
|
|
@@ -146,6 +111,9 @@ class GeometryOptimisationWorkChain(WorkChain):
|
|
|
146
111
|
if "mlip_model" in self.inputs:
|
|
147
112
|
inputs["optimisation_parameters"]["save_path"] = True
|
|
148
113
|
|
|
114
|
+
if "maxcycle" not in inputs["optimisation_parameters"]:
|
|
115
|
+
inputs["optimisation_parameters"]["maxcycle"] = 300
|
|
116
|
+
|
|
149
117
|
future = self.submit(ChemShellCalculation, **inputs)
|
|
150
118
|
future.label = ChemShellCalculation.default_process_label(future)
|
|
151
119
|
future.description = (
|
|
@@ -201,96 +169,6 @@ class GeometryOptimisationWorkChain(WorkChain):
|
|
|
201
169
|
return ToContext(isolated_atoms=future)
|
|
202
170
|
return None
|
|
203
171
|
|
|
204
|
-
def generate_mlip_training_inputs(self):
|
|
205
|
-
"""Convert the optimisation path files to Janus compatible inputs."""
|
|
206
|
-
if "mlip_model" in self.inputs:
|
|
207
|
-
inputs = {
|
|
208
|
-
"path": self.ctx.optimise.outputs.trajectory_path,
|
|
209
|
-
"force": self.ctx.optimise.outputs.trajectory_force,
|
|
210
|
-
"energies": self.ctx.optimise.outputs.optimisation_path,
|
|
211
|
-
"atom_energies": self.ctx.isolated_atoms.outputs.atom_energies,
|
|
212
|
-
"code": self.inputs.chemsh.code,
|
|
213
|
-
"metadata": {
|
|
214
|
-
"options": {
|
|
215
|
-
"resources": {"num_mpiprocs_per_machine": 2, "num_machines": 1},
|
|
216
|
-
# "withmpi": True,
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
}
|
|
220
|
-
from aiida_chemshell.calculations.file_conversion import (
|
|
221
|
-
CreateJanusTrainingInputsCalcJob,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
future = self.submit(CreateJanusTrainingInputsCalcJob, **inputs)
|
|
225
|
-
future.label = "Generate MLIP training data set from geometry optimisation."
|
|
226
|
-
future.description = (
|
|
227
|
-
f"Data extraction step from WorkChainNode pk: {self.node.pk}"
|
|
228
|
-
)
|
|
229
|
-
return ToContext(create_mlip_inputs=future)
|
|
230
|
-
return None
|
|
231
|
-
|
|
232
|
-
def train_mlip(self):
|
|
233
|
-
"""Train a given MLIP model."""
|
|
234
|
-
try:
|
|
235
|
-
mlip_train_calc = CalculationFactory("mlip.train")
|
|
236
|
-
except MissingEntryPointError:
|
|
237
|
-
pass
|
|
238
|
-
else:
|
|
239
|
-
if "mlip_model" in self.inputs:
|
|
240
|
-
# This needs to be properly addressed within aiida-mlip
|
|
241
|
-
computer = self.inputs.get("mlip_code", None).computer
|
|
242
|
-
work_dir = computer.get_workdir()
|
|
243
|
-
with open("train.xyz", mode="wb") as f:
|
|
244
|
-
f.write(self.ctx.create_mlip_inputs.outputs.training_input.content)
|
|
245
|
-
with open("test.xyz", mode="wb") as f:
|
|
246
|
-
f.write(self.ctx.create_mlip_inputs.outputs.test_input.content)
|
|
247
|
-
with open("valid.xyz", mode="wb") as f:
|
|
248
|
-
f.write(
|
|
249
|
-
self.ctx.create_mlip_inputs.outputs.validation_input.content
|
|
250
|
-
)
|
|
251
|
-
with computer.get_transport() as transport:
|
|
252
|
-
from pathlib import Path
|
|
253
|
-
|
|
254
|
-
transport.putfile(
|
|
255
|
-
Path("train.xyz").absolute(), f"{work_dir}/train.xyz"
|
|
256
|
-
)
|
|
257
|
-
transport.putfile(
|
|
258
|
-
Path("test.xyz").absolute(), f"{work_dir}/test.xyz"
|
|
259
|
-
)
|
|
260
|
-
transport.putfile(
|
|
261
|
-
Path("valid.xyz").absolute(), f"{work_dir}/valid.xyz"
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
import yaml
|
|
265
|
-
from aiida_mlip.data.config import JanusConfigfile
|
|
266
|
-
|
|
267
|
-
from aiida_chemshell.utils import generate_default_mlip_fine_tune_config
|
|
268
|
-
|
|
269
|
-
config_dict = generate_default_mlip_fine_tune_config()
|
|
270
|
-
config_dict["train_file"] = f"{work_dir}/train.xyz"
|
|
271
|
-
config_dict["test_file"] = f"{work_dir}/test.xyz"
|
|
272
|
-
config_dict["valid_file"] = f"{work_dir}/valid.xyz"
|
|
273
|
-
config_dict["name"] = "ChemShell_Workflow_Test"
|
|
274
|
-
|
|
275
|
-
with open("mlip_config.yml", "w+") as f:
|
|
276
|
-
yaml.dump(config_dict, f, default_flow_style=False)
|
|
277
|
-
|
|
278
|
-
mlip_inputs = {
|
|
279
|
-
"mlip_config": JanusConfigfile(Path("mlip_config.yml").absolute()),
|
|
280
|
-
"code": self.inputs.get("mlip_code", None),
|
|
281
|
-
"fine_tune": True,
|
|
282
|
-
"foundation_model": self.inputs.get("mlip_model", None),
|
|
283
|
-
"metadata": {"options": {"resources": {"num_machines": 1}}},
|
|
284
|
-
}
|
|
285
|
-
# Submit the mlip training job
|
|
286
|
-
future = self.submit(mlip_train_calc, **mlip_inputs)
|
|
287
|
-
future.label = "MLIP Fine-Tuning."
|
|
288
|
-
future.description = (
|
|
289
|
-
f"MLIP fine-tuning step from WorkChainNode pk: {self.node.pk}"
|
|
290
|
-
)
|
|
291
|
-
return ToContext(mlip_training=future)
|
|
292
|
-
return None
|
|
293
|
-
|
|
294
172
|
def result(self):
|
|
295
173
|
"""Extract the final workflow results."""
|
|
296
174
|
if "mlip_model" in self.inputs:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiida-chemshell
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: AiiDA workflow plugin for the ChemShell chemical modelling software package
|
|
5
5
|
Author-email: "Dr. Benjamin T. Speake" <benjamin.speake@stfc.ac.uk>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -42,6 +42,7 @@ Requires-Python: >=3.10
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: aiida-core>=2.5
|
|
45
|
+
Requires-Dist: ase>=3.29
|
|
45
46
|
Provides-Extra: dev
|
|
46
47
|
Requires-Dist: aiida-core>=2.8; extra == "dev"
|
|
47
48
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -13,12 +13,9 @@ src/aiida_chemshell.egg-info/requires.txt
|
|
|
13
13
|
src/aiida_chemshell.egg-info/top_level.txt
|
|
14
14
|
src/aiida_chemshell/calculations/__init__.py
|
|
15
15
|
src/aiida_chemshell/calculations/base.py
|
|
16
|
-
src/aiida_chemshell/calculations/file_conversion.py
|
|
17
|
-
src/aiida_chemshell/calculations/solvant_removal.py
|
|
18
16
|
src/aiida_chemshell/calculations/utils.py
|
|
19
17
|
src/aiida_chemshell/parsers/__init__.py
|
|
20
18
|
src/aiida_chemshell/parsers/base.py
|
|
21
|
-
src/aiida_chemshell/parsers/file_conversion.py
|
|
22
19
|
src/aiida_chemshell/workflows/__init__.py
|
|
23
20
|
src/aiida_chemshell/workflows/batch_calculation.py
|
|
24
21
|
src/aiida_chemshell/workflows/isolated_atoms.py
|
|
@@ -154,6 +154,52 @@ H 0.7546064020 0.5900323550 0.0000000000"""
|
|
|
154
154
|
assert ChemShellCalculation.FILE_TMP_STRUCTURE in script_txt
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
def test_structure2_as_structuredata_object(
|
|
158
|
+
generate_calcjob, generate_inputs, water_structure_object
|
|
159
|
+
):
|
|
160
|
+
"""Test that a StructureData 'structure2' input is written to its own file.
|
|
161
|
+
|
|
162
|
+
Regression test: 'structure2' was previously written to the primary
|
|
163
|
+
structure's temporary file (FILE_TMP_STRUCTURE), overwriting it, instead
|
|
164
|
+
of its own file (FILE_TMP_STRUCTURE2).
|
|
165
|
+
"""
|
|
166
|
+
structure2 = StructureData()
|
|
167
|
+
structure2.pbc = [False, False, False]
|
|
168
|
+
structure2.append_atom(symbols="O", position=[0.0, 0.0, 1.0])
|
|
169
|
+
|
|
170
|
+
inputs = generate_inputs(structure_fname=water_structure_object)
|
|
171
|
+
inputs["structure2"] = structure2
|
|
172
|
+
|
|
173
|
+
tmp_pth, calc_info = generate_calcjob(ChemShellCalculation, inputs)
|
|
174
|
+
|
|
175
|
+
structure_file = tmp_pth / ChemShellCalculation.FILE_TMP_STRUCTURE
|
|
176
|
+
structure2_file = tmp_pth / ChemShellCalculation.FILE_TMP_STRUCTURE2
|
|
177
|
+
assert structure_file.exists()
|
|
178
|
+
assert structure2_file.exists()
|
|
179
|
+
|
|
180
|
+
# The primary structure file must not have been overwritten by structure2.
|
|
181
|
+
chk_str = """3
|
|
182
|
+
Lattice="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" pbc="False False False"
|
|
183
|
+
O 0.0000000000 0.0000000000 0.0000000000
|
|
184
|
+
H -0.7546064020 0.5900323550 0.0000000000
|
|
185
|
+
H 0.7546064020 0.5900323550 0.0000000000"""
|
|
186
|
+
assert structure_file.read_text() == chk_str
|
|
187
|
+
|
|
188
|
+
chk_str2 = """1
|
|
189
|
+
Lattice="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" pbc="False False False"
|
|
190
|
+
O 0.0000000000 0.0000000000 1.0000000000"""
|
|
191
|
+
assert structure2_file.read_text() == chk_str2
|
|
192
|
+
|
|
193
|
+
script_file = tmp_pth / ChemShellCalculation.FILE_SCRIPT
|
|
194
|
+
assert script_file.exists()
|
|
195
|
+
|
|
196
|
+
script_txt = script_file.read_text()
|
|
197
|
+
assert (
|
|
198
|
+
f"frg2 = Fragment(coords='{ChemShellCalculation.FILE_TMP_STRUCTURE2:s}')\n"
|
|
199
|
+
in script_txt
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
157
203
|
def test_cjson_to_structure_data(get_test_data_file):
|
|
158
204
|
"""Test parsing a ChemShell '.cjson' file into a StructureData node."""
|
|
159
205
|
from aiida_chemshell.units import UnitsConverter
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from unittest.mock import MagicMock
|
|
4
4
|
|
|
5
|
-
import pytest
|
|
6
5
|
from aiida.engine import run_get_node
|
|
7
6
|
from aiida.engine.utils import instantiate_process
|
|
8
7
|
from aiida.manage.manager import get_manager
|
|
@@ -59,7 +58,7 @@ def test_metadata_options_forwarded_to_subcalculations(
|
|
|
59
58
|
# Skip the actual (slow) ChemShell calculations; only capture the submissions.
|
|
60
59
|
process.submit = MagicMock(return_value=MagicMock())
|
|
61
60
|
|
|
62
|
-
process.
|
|
61
|
+
process.create_atom_structures()
|
|
63
62
|
process.atom_energies()
|
|
64
63
|
|
|
65
64
|
# Water contains two unique atom types (O and H).
|
|
@@ -71,9 +70,47 @@ def test_metadata_options_forwarded_to_subcalculations(
|
|
|
71
70
|
)
|
|
72
71
|
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
def test_isolated_atom_structures_are_provenance_tracked(
|
|
74
|
+
chemsh_code, water_structure_object
|
|
75
|
+
):
|
|
76
|
+
"""Isolated atom structures are created and correctly linked in provenance."""
|
|
77
|
+
from aiida.common.links import LinkType
|
|
78
|
+
|
|
79
|
+
inputs = {
|
|
80
|
+
"structure": water_structure_object,
|
|
81
|
+
"code": chemsh_code,
|
|
82
|
+
"qm_parameters": Dict({"theory": "NWChem"}),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
runner = get_manager().get_runner()
|
|
86
|
+
process = instantiate_process(runner, IsolatedAtomicEnergiesWorkChain, **inputs)
|
|
87
|
+
|
|
88
|
+
process.create_atom_structures()
|
|
89
|
+
|
|
90
|
+
atom_structures = process.ctx.atom_structures
|
|
91
|
+
# Water contains two unique atom types (O and H).
|
|
92
|
+
assert set(atom_structures.keys()) == {"O", "H"}
|
|
93
|
+
|
|
94
|
+
for atom_structure in atom_structures.values():
|
|
95
|
+
assert atom_structure.is_stored, "Isolated atom structure was not stored."
|
|
96
|
+
creator = (
|
|
97
|
+
atom_structure.base.links.get_incoming(link_type=LinkType.CREATE).one().node
|
|
98
|
+
)
|
|
99
|
+
# The creator must be the calcfunction, which itself took the input
|
|
100
|
+
# structure as an input node.
|
|
101
|
+
input_pks = [
|
|
102
|
+
link.node.pk
|
|
103
|
+
for link in creator.base.links.get_incoming(
|
|
104
|
+
link_type=LinkType.INPUT_CALC
|
|
105
|
+
).all()
|
|
106
|
+
]
|
|
107
|
+
assert water_structure_object.pk in input_pks, (
|
|
108
|
+
"Isolated atom structure is not linked back to the input structure."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_full_workflow(chemsh_code, get_test_data_file):
|
|
113
|
+
"""End-to-end test for the isolated atomic energies workflow."""
|
|
77
114
|
inputs = {
|
|
78
115
|
"structure": get_test_data_file(),
|
|
79
116
|
"code": chemsh_code,
|
|
@@ -81,7 +118,6 @@ def test_geometry_optimisation_workflow(chemsh_code, get_test_data_file):
|
|
|
81
118
|
}
|
|
82
119
|
results, node = run_get_node(IsolatedAtomicEnergiesWorkChain, **inputs)
|
|
83
120
|
|
|
84
|
-
# print(results)
|
|
85
121
|
assert node.is_finished_ok, (
|
|
86
122
|
"WorkChain node failed for IsolatedAtomicEnergiesWorkChain"
|
|
87
123
|
)
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
"""CalcJob to create individual extended XYZ files for steps in an optimisation."""
|
|
2
|
-
|
|
3
|
-
from aiida.common import CalcInfo, CodeInfo
|
|
4
|
-
from aiida.common.folders import Folder
|
|
5
|
-
from aiida.engine import CalcJob, CalcJobProcessSpec, PortNamespace
|
|
6
|
-
from aiida.orm import ArrayData, Dict, SinglefileData, Str
|
|
7
|
-
|
|
8
|
-
from aiida_chemshell.units import UnitsConverter
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CreateJanusTrainingInputsCalcJob(CalcJob):
|
|
12
|
-
"""CalcJob to split an XYZ trajectory into individual ext XYZ files."""
|
|
13
|
-
|
|
14
|
-
@classmethod
|
|
15
|
-
def define(cls, spec: CalcJobProcessSpec) -> None:
|
|
16
|
-
"""Define the inputs, outputs and metadata for the CalcJob."""
|
|
17
|
-
super().define(spec)
|
|
18
|
-
spec.input(
|
|
19
|
-
"path",
|
|
20
|
-
valid_type=SinglefileData,
|
|
21
|
-
required=True,
|
|
22
|
-
help="An XYZ trajectory file containing the positions of each step.",
|
|
23
|
-
)
|
|
24
|
-
spec.input(
|
|
25
|
-
"force",
|
|
26
|
-
valid_type=SinglefileData,
|
|
27
|
-
required=True,
|
|
28
|
-
help="An XYZ trajectory file containing the forces at each step.",
|
|
29
|
-
)
|
|
30
|
-
spec.input(
|
|
31
|
-
"energies",
|
|
32
|
-
valid_type=ArrayData,
|
|
33
|
-
required=True,
|
|
34
|
-
help="The calculated dft energy of each step in the data series in a.u.",
|
|
35
|
-
)
|
|
36
|
-
spec.input(
|
|
37
|
-
"atom_energies",
|
|
38
|
-
valid_type=Dict,
|
|
39
|
-
required=True,
|
|
40
|
-
help="The isolated atomic energies for the training set.",
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
spec.input(
|
|
44
|
-
"filename",
|
|
45
|
-
valid_type=Str,
|
|
46
|
-
required=False,
|
|
47
|
-
help="The name to give the directory of output files.",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
spec.output(
|
|
51
|
-
"training_input",
|
|
52
|
-
valid_type=SinglefileData,
|
|
53
|
-
required=True,
|
|
54
|
-
help="The main training data set in extended XYZ format.",
|
|
55
|
-
)
|
|
56
|
-
spec.output(
|
|
57
|
-
"validation_input",
|
|
58
|
-
valid_type=SinglefileData,
|
|
59
|
-
required=True,
|
|
60
|
-
help="The validation data set in extended XYZ format.",
|
|
61
|
-
)
|
|
62
|
-
spec.output(
|
|
63
|
-
"test_input",
|
|
64
|
-
valid_type=SinglefileData,
|
|
65
|
-
required=True,
|
|
66
|
-
help="The testing data set in extended XYZ format.",
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
## Metadata
|
|
70
|
-
spec.inputs["metadata"]["options"]["resources"].default = {
|
|
71
|
-
"num_machines": 1,
|
|
72
|
-
"num_mpiprocs_per_machine": 2,
|
|
73
|
-
}
|
|
74
|
-
spec.inputs["metadata"]["options"][
|
|
75
|
-
"parser_name"
|
|
76
|
-
].default = "chemshell.file_conversion.mlip_training"
|
|
77
|
-
|
|
78
|
-
# Exit Codes
|
|
79
|
-
spec.exit_code(
|
|
80
|
-
300,
|
|
81
|
-
"ERROR_NO_TRAJECTORY_FILES",
|
|
82
|
-
message="No trajectory files have been produced.",
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
@classmethod
|
|
88
|
-
def validate_path_file(
|
|
89
|
-
cls, value: SinglefileData, namepsace: PortNamespace
|
|
90
|
-
) -> str | None:
|
|
91
|
-
"""Perform validation checks on the input path xyz files."""
|
|
92
|
-
if not isinstance(value, SinglefileData):
|
|
93
|
-
return "Input trajectory needs to be a SinglefileData node."
|
|
94
|
-
content = value.get_content("r")
|
|
95
|
-
lines = content.split("\n")
|
|
96
|
-
natoms = int(lines[0])
|
|
97
|
-
if len(lines) % (natoms + 2) != 0:
|
|
98
|
-
print(lines)
|
|
99
|
-
return "Invalid XYZ trajectory structure detected."
|
|
100
|
-
if len(lines) // (natoms + 2) < 5:
|
|
101
|
-
return "Not enough individual configurations within input trajectory."
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
def create_isolated_atom_energy_xyz(self) -> str:
|
|
105
|
-
"""Create the isolated atomistic energies in the required XYZ format."""
|
|
106
|
-
xyz_str = ""
|
|
107
|
-
zero = 0.0
|
|
108
|
-
for atom in self.inputs.atom_energies.keys():
|
|
109
|
-
xyz_str += "1\nProperties=species:S:1:pos:R:3:dft_forces:R:3 "
|
|
110
|
-
energy = UnitsConverter.hartree_to_ev(self.inputs.atom_energies[atom])
|
|
111
|
-
xyz_str += f"dft_energy={energy} "
|
|
112
|
-
xyz_str += 'config_type=IsolatedAtom pbc="F F F"\n'
|
|
113
|
-
xyz_str += f"{atom:8s} {zero:.4f} {zero:.4f} {zero:.4f} "
|
|
114
|
-
xyz_str += f"{zero:.4f} {zero:.4f} {zero:.4f}\n"
|
|
115
|
-
return xyz_str
|
|
116
|
-
|
|
117
|
-
def prepare_for_submission(self, folder: Folder) -> CalcInfo:
|
|
118
|
-
"""Perform the python task to split the input trajectories."""
|
|
119
|
-
script = self.generate_script()
|
|
120
|
-
with folder.open("input.py", "w") as f:
|
|
121
|
-
f.write(script)
|
|
122
|
-
|
|
123
|
-
with folder.open("energies.txt", "w") as f:
|
|
124
|
-
for val in self.inputs.energies.get_array("energies"):
|
|
125
|
-
f.write(f"{UnitsConverter.hartree_to_ev(val):.10f}\n")
|
|
126
|
-
|
|
127
|
-
with folder.open("isolated_atoms.xyz", "w") as f:
|
|
128
|
-
f.write(self.create_isolated_atom_energy_xyz())
|
|
129
|
-
|
|
130
|
-
code_info = CodeInfo()
|
|
131
|
-
code_info.code_uuid = self.inputs.code.uuid
|
|
132
|
-
if "chemsh.x" in str(self.inputs.code.filepath_executable):
|
|
133
|
-
code_info.cmdline_params = [
|
|
134
|
-
"input.py",
|
|
135
|
-
]
|
|
136
|
-
else:
|
|
137
|
-
n_machines = self.inputs.metadata.options.resources.get("num_machines")
|
|
138
|
-
n_mpi_pm = self.inputs.metadata.options.resources.get(
|
|
139
|
-
"num_mpiprocs_per_machine"
|
|
140
|
-
)
|
|
141
|
-
tot_mpi = n_machines * n_mpi_pm
|
|
142
|
-
code_info.cmdline_params = [
|
|
143
|
-
"-np",
|
|
144
|
-
self.inputs.metadata.options.resources.get("tot_num_mpiprocs", tot_mpi),
|
|
145
|
-
"input.py",
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
calc_info = CalcInfo()
|
|
149
|
-
calc_info.codes_info = [code_info]
|
|
150
|
-
calc_info.provenance_exclude_list = []
|
|
151
|
-
calc_info.retrieve_temporary_list = ["train.xyz", "valid.xyz", "test.xyz"]
|
|
152
|
-
calc_info.local_copy_list = [
|
|
153
|
-
(
|
|
154
|
-
self.inputs.path.uuid,
|
|
155
|
-
self.inputs.path.filename,
|
|
156
|
-
self.inputs.path.filename,
|
|
157
|
-
),
|
|
158
|
-
(
|
|
159
|
-
self.inputs.force.uuid,
|
|
160
|
-
self.inputs.force.filename,
|
|
161
|
-
self.inputs.force.filename,
|
|
162
|
-
),
|
|
163
|
-
]
|
|
164
|
-
|
|
165
|
-
return calc_info
|
|
166
|
-
|
|
167
|
-
def generate_script(self) -> str:
|
|
168
|
-
"""Generate the python script for splitting the trajectory files."""
|
|
169
|
-
return """
|
|
170
|
-
with open("path.xyz", "r") as f:
|
|
171
|
-
path = f.readlines()
|
|
172
|
-
with open("path_force.xyz", "r") as f:
|
|
173
|
-
force = f.readlines()
|
|
174
|
-
energies = []
|
|
175
|
-
with open("energies.txt", 'r') as f:
|
|
176
|
-
for line in f:
|
|
177
|
-
energies.append(float(line))
|
|
178
|
-
natms = int(path[0])
|
|
179
|
-
nsteps = len(path) // (natms + 2)
|
|
180
|
-
valid_interval = 5
|
|
181
|
-
test_interval = 10
|
|
182
|
-
if nsteps < test_interval:
|
|
183
|
-
test_interval = nsteps
|
|
184
|
-
valid_interval = (nsteps // 2) + 1
|
|
185
|
-
for step in range(nsteps):
|
|
186
|
-
if (step + 1) % test_interval == 0:
|
|
187
|
-
fname = "test.xyz"
|
|
188
|
-
elif (step + 1) % valid_interval == 0:
|
|
189
|
-
fname = "valid.xyz"
|
|
190
|
-
else:
|
|
191
|
-
fname = "train.xyz"
|
|
192
|
-
with open(fname, "a") as f:
|
|
193
|
-
f.write(f"{natms}\\n")
|
|
194
|
-
f.write(f'Step={step} ')
|
|
195
|
-
f.write(f'Properties=species:S:1:pos:R:3:dft_forces:R:3 pbc="F F F" ')
|
|
196
|
-
f.write(f'energy={energies[step]}\\n')
|
|
197
|
-
for i in range(2, natms + 2):
|
|
198
|
-
index = (step * (natms + 2)) + i
|
|
199
|
-
f.write(path[index].strip("\\n") + " ")
|
|
200
|
-
f.write(" ".join(force[index].split()[1:]))
|
|
201
|
-
f.write("\\n")
|
|
202
|
-
with open("isolated_atoms.xyz", "r") as f:
|
|
203
|
-
atoms = f.read()
|
|
204
|
-
with open("train.xyz", "a") as f:
|
|
205
|
-
f.write(atoms)
|
|
206
|
-
"""
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"""Module for performing a solvant removal CalcJob."""
|
|
2
|
-
|
|
3
|
-
from aiida.common import CalcInfo, CodeInfo
|
|
4
|
-
from aiida.common.folders import Folder
|
|
5
|
-
from aiida.engine import CalcJob, CalcJobProcessSpec
|
|
6
|
-
from aiida.orm import Int, SinglefileData
|
|
7
|
-
|
|
8
|
-
from aiida_chemshell.utils import xyz_file_validator
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class SolvantRemovalCalcJob(CalcJob):
|
|
12
|
-
"""CalcJob to extract a solvant from a complex for NEB calculations."""
|
|
13
|
-
|
|
14
|
-
_SCRIPT_NAME = "separate_solvant.py"
|
|
15
|
-
|
|
16
|
-
@classmethod
|
|
17
|
-
def define(cls, spec: CalcJobProcessSpec) -> None:
|
|
18
|
-
"""Define the CalcJob spec for solvant removal."""
|
|
19
|
-
super().define(spec)
|
|
20
|
-
spec.input(
|
|
21
|
-
"structure",
|
|
22
|
-
valid_type=SinglefileData,
|
|
23
|
-
required=True,
|
|
24
|
-
validator=xyz_file_validator,
|
|
25
|
-
help=(
|
|
26
|
-
"Input structure file containing ligand+solvant gas phase complex "
|
|
27
|
-
"in XYZ format."
|
|
28
|
-
),
|
|
29
|
-
)
|
|
30
|
-
spec.input(
|
|
31
|
-
"num_ligand_atoms",
|
|
32
|
-
valid_type=Int,
|
|
33
|
-
required=True,
|
|
34
|
-
help="Number of atoms within the core ligand molecule.",
|
|
35
|
-
)
|
|
36
|
-
spec.output(
|
|
37
|
-
"unbound_structure",
|
|
38
|
-
valid_type=SinglefileData,
|
|
39
|
-
required=True,
|
|
40
|
-
help="Gas phase structure for the unboun ligand+solvant complex.",
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
spec.inputs["metadata"]["options"]["resources"].default = {
|
|
44
|
-
"num_machines": 1,
|
|
45
|
-
"num_mpiprocs_per_machine": 1,
|
|
46
|
-
}
|
|
47
|
-
spec.inputs["metadata"]["options"]["parser_name"].default = "chemshell.unbind"
|
|
48
|
-
|
|
49
|
-
def prepare_for_submission(self, folder: Folder) -> CalcInfo:
|
|
50
|
-
"""Prepare the CalcJob for submission."""
|
|
51
|
-
py_scipt = self._generate_python_script()
|
|
52
|
-
with folder.open(SolvantRemovalCalcJob._SCRIPT_NAME, "w") as handle:
|
|
53
|
-
handle.write(py_scipt)
|
|
54
|
-
|
|
55
|
-
code_info = CodeInfo()
|
|
56
|
-
code_info.code_uuid = self.inputs.code.uuid
|
|
57
|
-
code_info.cmdline_params = [SolvantRemovalCalcJob._SCRIPT_NAME]
|
|
58
|
-
|
|
59
|
-
calc_info = CalcInfo()
|
|
60
|
-
calc_info.codes_info = [code_info]
|
|
61
|
-
calc_info.retrieve_temporary_list = []
|
|
62
|
-
calc_info.provenance_exclude_list = []
|
|
63
|
-
calc_info.retrieve_list = [self._generate_output_filename()]
|
|
64
|
-
calc_info.local_copy_list = [
|
|
65
|
-
(
|
|
66
|
-
self.inputs.structure.uuid,
|
|
67
|
-
self.inputs.structure.filename,
|
|
68
|
-
self.inputs.structure.filename,
|
|
69
|
-
),
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
return calc_info
|
|
73
|
-
|
|
74
|
-
@classmethod
|
|
75
|
-
def generate_output_filename(cls, input_filename) -> str:
|
|
76
|
-
"""Generate a filename based on the name of the initial structure file."""
|
|
77
|
-
return input_filename.replace(".xyz", "_unbound.xyz")
|
|
78
|
-
|
|
79
|
-
def _generate_output_filename(self) -> str:
|
|
80
|
-
"""Generate the output filename."""
|
|
81
|
-
return self.generate_output_filename(self.inputs.structure.filename)
|
|
82
|
-
|
|
83
|
-
def _generate_python_script(self) -> str:
|
|
84
|
-
"""Generate the Python script to be executed."""
|
|
85
|
-
return f"""
|
|
86
|
-
import numpy
|
|
87
|
-
with open("{self.inputs.structure.filename}", "r") as f:
|
|
88
|
-
lines = f.readlines()
|
|
89
|
-
natoms = int(lines[0].strip())
|
|
90
|
-
ligand = []
|
|
91
|
-
for i in range(2, {int(self.inputs.num_ligand_atoms)} + 2):
|
|
92
|
-
line = lines[i].split()
|
|
93
|
-
ligand.append(numpy.array([float(line[1]), float(line[2]), float(line[3])]))
|
|
94
|
-
solvant = []
|
|
95
|
-
for i in range({int(self.inputs.num_ligand_atoms)} + 2, natoms + 2):
|
|
96
|
-
line = lines[i].split()
|
|
97
|
-
solvant.append(numpy.array([float(line[1]), float(line[2]), float(line[3])]))
|
|
98
|
-
ligand_center = numpy.mean(ligand, axis=0)
|
|
99
|
-
solvant_center = numpy.mean(solvant, axis=0)
|
|
100
|
-
vector = solvant_center - ligand_center
|
|
101
|
-
distance = numpy.linalg.norm(vector)
|
|
102
|
-
vector /= distance
|
|
103
|
-
displacement = vector * 10.0 # Move solvant 10 Angstroms away
|
|
104
|
-
with open("{self._generate_output_filename()}", "w") as f:
|
|
105
|
-
f.write(f"{{natoms}}\\n")
|
|
106
|
-
f.write("Unbound ligand+solvant complex\\n")
|
|
107
|
-
for i in range(2, {int(self.inputs.num_ligand_atoms)} + 2):
|
|
108
|
-
f.write(lines[i])
|
|
109
|
-
for i in range({int(self.inputs.num_ligand_atoms)} + 2, natoms + 2):
|
|
110
|
-
line = lines[i].split()
|
|
111
|
-
x = float(line[1]) + displacement[0]
|
|
112
|
-
y = float(line[2]) + displacement[1]
|
|
113
|
-
z = float(line[3]) + displacement[2]
|
|
114
|
-
f.write(f"{{line[0]}} {{x:.8f}} {{y:.8f}} {{z:.8f}}\\n")
|
|
115
|
-
"""
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""Defines the parser for the split trajectory CalcJob."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
from aiida.engine import ExitCode
|
|
6
|
-
from aiida.orm import SinglefileData
|
|
7
|
-
from aiida.parsers.parser import Parser
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CreateJanusTrainingInputsParser(Parser):
|
|
11
|
-
"""AiiDA parser plugin for SplitTrajectory CalcJob."""
|
|
12
|
-
|
|
13
|
-
def parse(self, **kwargs):
|
|
14
|
-
"""AiiDA parser plugin for SplitTrajectory CalcJob."""
|
|
15
|
-
description_str = "MLIP {} data set extracted from ChemShell calculation."
|
|
16
|
-
retrieved_temporary_folder = kwargs.get("retrieved_temporary_folder", None)
|
|
17
|
-
|
|
18
|
-
# with self.retrieved.open("train.xyz", "r") as f:
|
|
19
|
-
with open(os.path.join(retrieved_temporary_folder, "train.xyz"), "rb") as f:
|
|
20
|
-
self.out(
|
|
21
|
-
"training_input",
|
|
22
|
-
SinglefileData(
|
|
23
|
-
file=f,
|
|
24
|
-
filename="train.xyz",
|
|
25
|
-
label="MLIP training data.",
|
|
26
|
-
description=description_str.format("training"),
|
|
27
|
-
),
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
# with self.retrieved.open("test.xyz", "r") as f:
|
|
31
|
-
with open(os.path.join(retrieved_temporary_folder, "test.xyz"), "rb") as f:
|
|
32
|
-
self.out(
|
|
33
|
-
"test_input",
|
|
34
|
-
SinglefileData(
|
|
35
|
-
file=f,
|
|
36
|
-
filename="test.xyz",
|
|
37
|
-
label="MLIP test data.",
|
|
38
|
-
description=description_str.format("testing"),
|
|
39
|
-
),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# with self.retrieved.open("valid.xyz", "r") as f:
|
|
43
|
-
with open(os.path.join(retrieved_temporary_folder, "valid.xyz"), "rb") as f:
|
|
44
|
-
self.out(
|
|
45
|
-
"validation_input",
|
|
46
|
-
SinglefileData(
|
|
47
|
-
file=f,
|
|
48
|
-
filename="valid.xyz",
|
|
49
|
-
label="MLIP validation data.",
|
|
50
|
-
description=description_str.format("validation"),
|
|
51
|
-
),
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
return ExitCode(0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|