aiida-chemshell 0.3.0__tar.gz → 0.3.1__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.0/src/aiida_chemshell.egg-info → aiida_chemshell-0.3.1}/PKG-INFO +1 -1
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/pyproject.toml +1 -1
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/calculations/base.py +114 -18
- aiida_chemshell-0.3.1/src/aiida_chemshell/calculations/utils.py +226 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/parsers/base.py +84 -24
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/units.py +2 -2
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/utils.py +58 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/workflows/batch_calculation.py +23 -148
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/workflows/isolated_atoms.py +19 -1
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/workflows/optimisation.py +28 -2
- aiida_chemshell-0.3.1/src/aiida_chemshell/workflows/utils.py +34 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1/src/aiida_chemshell.egg-info}/PKG-INFO +1 -1
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell.egg-info/SOURCES.txt +1 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/tests/test_batch_workcahin.py +28 -1
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/tests/test_calculations.py +18 -5
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/tests/test_inputs.py +63 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/tests/test_isolated_atoms_workchain.py +23 -0
- aiida_chemshell-0.3.1/tests/test_workflows.py +95 -0
- aiida_chemshell-0.3.0/src/aiida_chemshell/calculations/utils.py +0 -14
- aiida_chemshell-0.3.0/tests/test_workflows.py +0 -37
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/LICENSE +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/README.md +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/setup.cfg +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/__init__.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/calculations/__init__.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/calculations/file_conversion.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/calculations/solvant_removal.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/parsers/__init__.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/parsers/file_conversion.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/periodic_table.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell/workflows/__init__.py +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell.egg-info/dependency_links.txt +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell.egg-info/entry_points.txt +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell.egg-info/requires.txt +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/src/aiida_chemshell.egg-info/top_level.txt +0 -0
- {aiida_chemshell-0.3.0 → aiida_chemshell-0.3.1}/tests/test_error_codes.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.1
|
|
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
|
|
@@ -36,6 +36,47 @@ class ChemShellCalculation(CalcJob):
|
|
|
36
36
|
FILE_TRJPTH = "path.xyz"
|
|
37
37
|
FILE_TRJFRC = "path_force.xyz"
|
|
38
38
|
|
|
39
|
+
# Default labels and descriptions applied to input nodes that do not already
|
|
40
|
+
# have them set, keyed by the input port name.
|
|
41
|
+
DEFAULT_INPUT_TAGS = {
|
|
42
|
+
"structure": (
|
|
43
|
+
"Input Chemical Structure",
|
|
44
|
+
"The input structure for the ChemShell calculation.",
|
|
45
|
+
),
|
|
46
|
+
"structure_index": (
|
|
47
|
+
"Structure Input Index",
|
|
48
|
+
"Index used to select a structure from a multi-structure input.",
|
|
49
|
+
),
|
|
50
|
+
"structure2": (
|
|
51
|
+
"Secondary Input Structure",
|
|
52
|
+
"The additional (final) input structure for the ChemShell calculation.",
|
|
53
|
+
),
|
|
54
|
+
"calculation_parameters": (
|
|
55
|
+
"ChemShell Calculation Parameters",
|
|
56
|
+
"Base parameters for the ChemShell Task object.",
|
|
57
|
+
),
|
|
58
|
+
"optimisation_parameters": (
|
|
59
|
+
"ChemShell Optimisation Parameters",
|
|
60
|
+
"Parameters for the ChemShell geometry optimisation task.",
|
|
61
|
+
),
|
|
62
|
+
"qm_parameters": (
|
|
63
|
+
"ChemShell QM Parameters",
|
|
64
|
+
"Parameters for the ChemShell QM Theory object.",
|
|
65
|
+
),
|
|
66
|
+
"mm_parameters": (
|
|
67
|
+
"ChemShell MM Parameters",
|
|
68
|
+
"Parameters for the ChemShell MM interface.",
|
|
69
|
+
),
|
|
70
|
+
"force_field_file": (
|
|
71
|
+
"ChemShell Force Field File",
|
|
72
|
+
"Force field parameters for the ChemShell MM interface.",
|
|
73
|
+
),
|
|
74
|
+
"qmmm_parameters": (
|
|
75
|
+
"ChemShell QM/MM Parameters",
|
|
76
|
+
"Parameters for the ChemShell QM/MM interface.",
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
|
|
39
80
|
@classmethod
|
|
40
81
|
def define(cls, spec: CalcJobProcessSpec) -> None:
|
|
41
82
|
"""
|
|
@@ -136,6 +177,17 @@ class ChemShellCalculation(CalcJob):
|
|
|
136
177
|
required=False,
|
|
137
178
|
help="A dictionary of parameters for the ChemShell QM/MM interface.",
|
|
138
179
|
)
|
|
180
|
+
spec.input(
|
|
181
|
+
"output_structure_as_file",
|
|
182
|
+
valid_type=bool,
|
|
183
|
+
non_db=True,
|
|
184
|
+
default=False,
|
|
185
|
+
help=(
|
|
186
|
+
"If True, the optimised structure is returned as a SinglefileData "
|
|
187
|
+
"node containing a ChemShell '.cjson' file. By default (False) it is "
|
|
188
|
+
"returned as an AiiDA StructureData node."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
139
191
|
|
|
140
192
|
## Calculation outputs
|
|
141
193
|
spec.output(
|
|
@@ -156,12 +208,14 @@ class ChemShellCalculation(CalcJob):
|
|
|
156
208
|
)
|
|
157
209
|
spec.output(
|
|
158
210
|
"optimised_structure",
|
|
159
|
-
valid_type=SinglefileData,
|
|
211
|
+
valid_type=(StructureData, SinglefileData),
|
|
160
212
|
required=False,
|
|
161
213
|
help=(
|
|
162
214
|
"The optimised structure of the given system, if a geometry "
|
|
163
|
-
"optimisation task was configured and successfully completed.
|
|
164
|
-
"
|
|
215
|
+
"optimisation task was configured and successfully completed. By "
|
|
216
|
+
"default this is an AiiDA StructureData node; if the "
|
|
217
|
+
"'output_structure_as_file' input is True it is instead a "
|
|
218
|
+
"SinglefileData node containing a ChemShell '.cjson' file."
|
|
165
219
|
),
|
|
166
220
|
)
|
|
167
221
|
spec.output(
|
|
@@ -209,11 +263,11 @@ class ChemShellCalculation(CalcJob):
|
|
|
209
263
|
)
|
|
210
264
|
spec.output(
|
|
211
265
|
"trajectory_force",
|
|
212
|
-
valid_type=
|
|
266
|
+
valid_type=ArrayData,
|
|
213
267
|
required=False,
|
|
214
268
|
help=(
|
|
215
|
-
"
|
|
216
|
-
"
|
|
269
|
+
"Per-atom forces (natoms, 3) at each step of a geometry optimisation, "
|
|
270
|
+
"stored one array per frame labelled 'Frame {i}'."
|
|
217
271
|
),
|
|
218
272
|
)
|
|
219
273
|
|
|
@@ -324,13 +378,13 @@ class ChemShellCalculation(CalcJob):
|
|
|
324
378
|
return None
|
|
325
379
|
|
|
326
380
|
@classmethod
|
|
327
|
-
def get_valid_calculation_parameter_keys(cls) -> tuple[str]:
|
|
381
|
+
def get_valid_calculation_parameter_keys(cls) -> tuple[str, str]:
|
|
328
382
|
"""
|
|
329
383
|
Return the valid parameter keys for the ChemShell Single Point calculation.
|
|
330
384
|
|
|
331
385
|
Returns
|
|
332
386
|
-------
|
|
333
|
-
|
|
387
|
+
tuple[str, str]
|
|
334
388
|
A tuple of valid parameter keys for the ChemShell calculation.
|
|
335
389
|
"""
|
|
336
390
|
return ("gradients", "hessian")
|
|
@@ -379,7 +433,7 @@ class ChemShellCalculation(CalcJob):
|
|
|
379
433
|
|
|
380
434
|
Returns
|
|
381
435
|
-------
|
|
382
|
-
|
|
436
|
+
tuple[str]
|
|
383
437
|
A tuple of valid optimisation parameter keys for the ChemShell
|
|
384
438
|
calculation.
|
|
385
439
|
"""
|
|
@@ -437,12 +491,13 @@ class ChemShellCalculation(CalcJob):
|
|
|
437
491
|
@classmethod
|
|
438
492
|
def get_valid_qm_parameter_keys(cls) -> dict:
|
|
439
493
|
"""
|
|
440
|
-
Return a
|
|
494
|
+
Return a dictionary of valid parameter keys for the ChemShell calculation.
|
|
441
495
|
|
|
442
496
|
Returns
|
|
443
497
|
-------
|
|
444
|
-
|
|
445
|
-
A
|
|
498
|
+
dict[str, type]
|
|
499
|
+
A mapping of valid Theory parameter keys to their expected value
|
|
500
|
+
types for the ChemShell calculation.
|
|
446
501
|
"""
|
|
447
502
|
return {
|
|
448
503
|
"theory": str,
|
|
@@ -527,14 +582,21 @@ class ChemShellCalculation(CalcJob):
|
|
|
527
582
|
return None
|
|
528
583
|
|
|
529
584
|
@classmethod
|
|
530
|
-
def get_valid_mm_parameter_keys(cls, theory: str = "") -> dict[str
|
|
585
|
+
def get_valid_mm_parameter_keys(cls, theory: str = "") -> dict[str, type]:
|
|
531
586
|
"""
|
|
532
|
-
Return a
|
|
587
|
+
Return a dictionary of valid parameter keys for the ChemShell MM interface.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
theory : str
|
|
592
|
+
The MM theory whose valid parameter keys should be returned (e.g.
|
|
593
|
+
'DL_POLY'). Defaults to an empty string.
|
|
533
594
|
|
|
534
595
|
Returns
|
|
535
596
|
-------
|
|
536
|
-
|
|
537
|
-
A
|
|
597
|
+
dict[str, type]
|
|
598
|
+
A mapping of valid MM parameter keys to their expected value types
|
|
599
|
+
for the ChemShell calculation.
|
|
538
600
|
"""
|
|
539
601
|
if theory == "DL_POLY":
|
|
540
602
|
valid_keys = {
|
|
@@ -754,6 +816,12 @@ class ChemShellCalculation(CalcJob):
|
|
|
754
816
|
Defines the process label to be associated with the created ProcessNode
|
|
755
817
|
stored in the AiiDA database.
|
|
756
818
|
|
|
819
|
+
Parameters
|
|
820
|
+
----------
|
|
821
|
+
node : ProcessNode
|
|
822
|
+
The process (or its ProcessNode) whose inputs are inspected to build
|
|
823
|
+
the label.
|
|
824
|
+
|
|
757
825
|
Returns
|
|
758
826
|
-------
|
|
759
827
|
str
|
|
@@ -933,12 +1001,30 @@ class ChemShellCalculation(CalcJob):
|
|
|
933
1001
|
|
|
934
1002
|
return script
|
|
935
1003
|
|
|
1004
|
+
def apply_default_input_tags(self) -> None:
|
|
1005
|
+
"""
|
|
1006
|
+
Apply default labels and descriptions to the calculation input nodes.
|
|
1007
|
+
|
|
1008
|
+
Iterates over the provided input nodes and, for any node defined in
|
|
1009
|
+
:attr:`DEFAULT_INPUT_TAGS` that does not already have a label or
|
|
1010
|
+
description set, assigns the corresponding default value. Existing
|
|
1011
|
+
labels and descriptions are left untouched.
|
|
1012
|
+
"""
|
|
1013
|
+
for port_name, (label, description) in self.DEFAULT_INPUT_TAGS.items():
|
|
1014
|
+
if port_name not in self.inputs:
|
|
1015
|
+
continue
|
|
1016
|
+
node = self.inputs[port_name]
|
|
1017
|
+
if not node.label:
|
|
1018
|
+
node.label = label
|
|
1019
|
+
if not node.description:
|
|
1020
|
+
node.description = description
|
|
1021
|
+
|
|
936
1022
|
def prepare_for_submission(self, folder: Folder) -> CalcInfo:
|
|
937
1023
|
"""
|
|
938
1024
|
Prepare the ChemShell calculation for submission.
|
|
939
1025
|
|
|
940
|
-
|
|
941
|
-
|
|
1026
|
+
Parameters
|
|
1027
|
+
----------
|
|
942
1028
|
folder : Folder
|
|
943
1029
|
An `aiida.common.folders.Folder` specifying the temporary working
|
|
944
1030
|
directory for the calculation.
|
|
@@ -948,6 +1034,16 @@ class ChemShellCalculation(CalcJob):
|
|
|
948
1034
|
calcInfo : CalcInfo
|
|
949
1035
|
An `aiida.common.CalcInfo` instance.
|
|
950
1036
|
"""
|
|
1037
|
+
# Apply default labels/descriptions to input nodes lacking them
|
|
1038
|
+
self.apply_default_input_tags()
|
|
1039
|
+
|
|
1040
|
+
# Record the (non_db) output structure format choice as a node extra so
|
|
1041
|
+
# that the parser can determine whether to return the optimised structure
|
|
1042
|
+
# as a StructureData or SinglefileData node.
|
|
1043
|
+
self.node.base.extras.set(
|
|
1044
|
+
"output_structure_as_file", bool(self.inputs.output_structure_as_file)
|
|
1045
|
+
)
|
|
1046
|
+
|
|
951
1047
|
# Create the ChemShell input script
|
|
952
1048
|
input_script = self.chemsh_script_generator()
|
|
953
1049
|
with folder.open(ChemShellCalculation.FILE_SCRIPT, "w") as f:
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""A collection of smaller utility AiiDA CalcFunctions."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
from aiida.engine import calcfunction
|
|
7
|
+
from aiida.orm import (
|
|
8
|
+
ArrayData,
|
|
9
|
+
Dict,
|
|
10
|
+
Float,
|
|
11
|
+
SinglefileData,
|
|
12
|
+
StructureData,
|
|
13
|
+
TrajectoryData,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@calcfunction
|
|
18
|
+
def create_dictionary(atoms, energies) -> Dict:
|
|
19
|
+
"""Collate a series of isolated atom energies into a dictionary output."""
|
|
20
|
+
if len(atoms) != len(energies):
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"Mismatched lengths: Got {len(atoms)} atoms and {len(energies)} energies."
|
|
23
|
+
)
|
|
24
|
+
return Dict(dict(zip(atoms, energies, strict=False)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@calcfunction
|
|
28
|
+
def extract_structures_from_xyz(file: SinglefileData) -> dict[str, StructureData]:
|
|
29
|
+
"""
|
|
30
|
+
Split a multi-frame XYZ file into individual StructureData nodes.
|
|
31
|
+
|
|
32
|
+
Each frame in the (optionally extended) XYZ file is parsed into its own
|
|
33
|
+
StructureData node. Extended XYZ 'Lattice=' and 'pbc=' fields on the comment
|
|
34
|
+
line are honoured when present; frames without them are treated as
|
|
35
|
+
non-periodic.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
file : SinglefileData
|
|
40
|
+
A SinglefileData node wrapping an XYZ (or extended XYZ) file that may
|
|
41
|
+
contain one or more frames.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
dict[str, StructureData]
|
|
46
|
+
A mapping of output link labels to StructureData nodes, one per frame.
|
|
47
|
+
Keys are of the form '<filename>_frame_<index>' (zero-indexed, with
|
|
48
|
+
spaces in the filename replaced by underscores). As a calcfunction
|
|
49
|
+
return value, this dictionary is registered as a namespace of output
|
|
50
|
+
nodes.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
Exception
|
|
55
|
+
If the file is not valid XYZ, is truncated, or contains a malformed
|
|
56
|
+
atom line.
|
|
57
|
+
"""
|
|
58
|
+
with file.open(mode="r") as f:
|
|
59
|
+
lines = f.readlines()
|
|
60
|
+
|
|
61
|
+
structures = {}
|
|
62
|
+
line_count = len(lines)
|
|
63
|
+
i = 0
|
|
64
|
+
frame_idx = 0
|
|
65
|
+
|
|
66
|
+
while i < line_count:
|
|
67
|
+
line = lines[i].strip()
|
|
68
|
+
try:
|
|
69
|
+
natoms = int(line)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
raise Exception("Invalid XYZ format detected.") from e
|
|
72
|
+
if (i + 2 + natoms) > line_count:
|
|
73
|
+
raise Exception("XYZ file truncation detected.")
|
|
74
|
+
|
|
75
|
+
# Create the base StructureData object
|
|
76
|
+
structure = StructureData(pbc=(False, False, False))
|
|
77
|
+
|
|
78
|
+
# Read the comment line
|
|
79
|
+
i += 1
|
|
80
|
+
line = lines[i].strip()
|
|
81
|
+
cell = None
|
|
82
|
+
if "Lattice=" in line:
|
|
83
|
+
match = re.search(r'Lattice="([^"]+)"', line)
|
|
84
|
+
if match:
|
|
85
|
+
lat_vals = [float(x) for x in match.group(1).split()]
|
|
86
|
+
if len(lat_vals) == 9:
|
|
87
|
+
cell = [lat_vals[0:3], lat_vals[3:6], lat_vals[6:9]]
|
|
88
|
+
pbc = [True, True, True]
|
|
89
|
+
if "pbc=" in line:
|
|
90
|
+
match_pbc = re.search(r'pbc="([^"]+)"', line)
|
|
91
|
+
if match_pbc:
|
|
92
|
+
pbc_vals = match_pbc.group(1).split()
|
|
93
|
+
if len(pbc_vals) == 3:
|
|
94
|
+
# Robust check: converts 'T', 'True', or '1' to True
|
|
95
|
+
pbc = [val.upper() in ["T", "TRUE", "1"] for val in pbc_vals]
|
|
96
|
+
|
|
97
|
+
# Assign the parse cell parameters to the StructureData object
|
|
98
|
+
structure.cell = cell
|
|
99
|
+
structure.pbc = pbc
|
|
100
|
+
|
|
101
|
+
i += 1
|
|
102
|
+
for atmi in range(natoms):
|
|
103
|
+
atom_line = lines[i + atmi].strip().split()
|
|
104
|
+
if len(atom_line) < 4:
|
|
105
|
+
raise Exception(
|
|
106
|
+
f"Invalid atom entry in xyz file: {line[i + atmi].strip()}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
structure.append_atom(
|
|
110
|
+
position=[
|
|
111
|
+
float(atom_line[1]),
|
|
112
|
+
float(atom_line[2]),
|
|
113
|
+
float(atom_line[3]),
|
|
114
|
+
],
|
|
115
|
+
symbols=atom_line[0],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
structures[
|
|
119
|
+
f"{file.filename.replace(' ', '_').strip('.xyz')}_frame_{frame_idx}"
|
|
120
|
+
] = structure
|
|
121
|
+
frame_idx += 1
|
|
122
|
+
i += natoms
|
|
123
|
+
|
|
124
|
+
return structures
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@calcfunction
|
|
128
|
+
def combine_into_extended_xyz(
|
|
129
|
+
**kwargs: StructureData | TrajectoryData | Float | ArrayData,
|
|
130
|
+
) -> SinglefileData:
|
|
131
|
+
"""
|
|
132
|
+
Collate a batch of single-point results into one extended XYZ file.
|
|
133
|
+
|
|
134
|
+
The keyword nodes are sorted by type into three parallel lists: structures
|
|
135
|
+
(from StructureData, or each frame of a TrajectoryData), energies (Float)
|
|
136
|
+
and, optionally, force arrays (ArrayData). They are then written out as an
|
|
137
|
+
extended XYZ file with an 'Energy=' label per frame and, when forces are
|
|
138
|
+
supplied, a 'force' property column. The lists are paired positionally, so
|
|
139
|
+
the caller is responsible for passing matched sets of nodes (equal numbers
|
|
140
|
+
of structures, energies and, if used, arrays).
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
**kwargs : StructureData | TrajectoryData | Float | ArrayData
|
|
145
|
+
Arbitrary keyword arguments whose values are the result nodes to
|
|
146
|
+
combine. Only the values are used; the keys are ignored. Forces are read
|
|
147
|
+
from the 'gradients' array of any ArrayData node.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
SinglefileData
|
|
152
|
+
A SinglefileData node wrapping the combined
|
|
153
|
+
'chemshell_batch_workchain.extxyz' file.
|
|
154
|
+
|
|
155
|
+
Raises
|
|
156
|
+
------
|
|
157
|
+
ValueError
|
|
158
|
+
If an unsupported node type is passed, if no structures are provided, or
|
|
159
|
+
if the numbers of structures, energies and (when given) arrays do not
|
|
160
|
+
match.
|
|
161
|
+
"""
|
|
162
|
+
structures: list[StructureData] = []
|
|
163
|
+
energies: list[Float] = []
|
|
164
|
+
arrays: list[ArrayData] = []
|
|
165
|
+
|
|
166
|
+
# Extract individual structures and SP calculation results.
|
|
167
|
+
for _key, node in kwargs.items():
|
|
168
|
+
if isinstance(node, TrajectoryData):
|
|
169
|
+
for tindex in range(node.numsteps):
|
|
170
|
+
structures.append(node.get_step_structure(tindex))
|
|
171
|
+
elif isinstance(node, StructureData):
|
|
172
|
+
structures.append(node)
|
|
173
|
+
elif isinstance(node, Float):
|
|
174
|
+
energies.append(node)
|
|
175
|
+
elif isinstance(node, ArrayData): # Ensure after TrajectoryData !!
|
|
176
|
+
arrays.append(node)
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Invalid input of type {node} to 'combine_into_extended_xyz' "
|
|
180
|
+
"calcfunction."
|
|
181
|
+
)
|
|
182
|
+
if not structures:
|
|
183
|
+
raise ValueError("No StructureData nodes were provided.")
|
|
184
|
+
if len(structures) != len(energies):
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Mismatched input lengths: received {len(structures)} structures and "
|
|
187
|
+
f"{len(energies)} energies."
|
|
188
|
+
)
|
|
189
|
+
if arrays:
|
|
190
|
+
if len(arrays) != len(energies):
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"Mismatched input lengths: received {len(arrays)} arrays and "
|
|
193
|
+
f"{len(energies)} energies."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
xyz_lines = []
|
|
197
|
+
|
|
198
|
+
for i in range(len(structures)):
|
|
199
|
+
structure = structures[i]
|
|
200
|
+
energy = energies[i]
|
|
201
|
+
|
|
202
|
+
xyz_lines.append(str(len(structure.sites)))
|
|
203
|
+
|
|
204
|
+
header_line = f"Energy={energy.value}"
|
|
205
|
+
if arrays:
|
|
206
|
+
forces = arrays[i].get_array("gradients")
|
|
207
|
+
header_line += " Properties=species:S:1:pos:R:3:force:R:3"
|
|
208
|
+
else:
|
|
209
|
+
forces = None
|
|
210
|
+
header_line += " Properties=species:S:1:pos:R:3"
|
|
211
|
+
xyz_lines.append(header_line)
|
|
212
|
+
|
|
213
|
+
for j, site in enumerate(structure.sites):
|
|
214
|
+
kind = structure.get_kind(site.kind_name)
|
|
215
|
+
symbol = kind.symbols[0]
|
|
216
|
+
x, y, z = site.position
|
|
217
|
+
site_line = f"{symbol:<4} {x:15.8f} {y:15.8f} {z:15.8f}"
|
|
218
|
+
if forces is not None:
|
|
219
|
+
fx, fy, fz = forces[j]
|
|
220
|
+
site_line += f" {fx:15.8f} {fy:15.8f} {fz:15.8f}"
|
|
221
|
+
xyz_lines.append(site_line)
|
|
222
|
+
|
|
223
|
+
content = "\n".join(xyz_lines)
|
|
224
|
+
stream = BytesIO(content.encode("utf-8"))
|
|
225
|
+
|
|
226
|
+
return SinglefileData(file=stream, filename="chemshell_batch_workchain.extxyz")
|
|
@@ -10,6 +10,7 @@ from aiida.orm import ArrayData, Dict, Float, SinglefileData, TrajectoryData
|
|
|
10
10
|
from aiida.parsers.parser import Parser
|
|
11
11
|
|
|
12
12
|
from aiida_chemshell.calculations.base import ChemShellCalculation
|
|
13
|
+
from aiida_chemshell.utils import chemsh_cjson_to_structure_data
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class ChemShellParser(Parser):
|
|
@@ -31,7 +32,17 @@ class ChemShellParser(Parser):
|
|
|
31
32
|
|
|
32
33
|
# Extract the final energy
|
|
33
34
|
try:
|
|
34
|
-
self.out(
|
|
35
|
+
self.out(
|
|
36
|
+
"energy",
|
|
37
|
+
Float(
|
|
38
|
+
results["energy"][0],
|
|
39
|
+
label="Final SCF Energy",
|
|
40
|
+
description=(
|
|
41
|
+
"The total (final SCF) energy of the system calculated by "
|
|
42
|
+
"ChemShell."
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
35
46
|
except (KeyError, ValueError):
|
|
36
47
|
return self.exit_codes.ERROR_MISSING_FINAL_ENERGY
|
|
37
48
|
except ModificationNotAllowed as e:
|
|
@@ -91,17 +102,25 @@ class ChemShellParser(Parser):
|
|
|
91
102
|
if isinstance(self.node.inputs.structure, SinglefileData):
|
|
92
103
|
input_fname = self.node.inputs.structure.filename
|
|
93
104
|
descrip += f" ({input_fname})"
|
|
94
|
-
# Store the optimised structure
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
# Store the optimised structure either as a SinglefileData '.cjson'
|
|
106
|
+
# file or (by default) as an AiiDA StructureData node.
|
|
107
|
+
if self.node.base.extras.get("output_structure_as_file", False):
|
|
108
|
+
with open(dl_find_path, "rb") as f:
|
|
109
|
+
self.out(
|
|
110
|
+
"optimised_structure",
|
|
111
|
+
SinglefileData(
|
|
112
|
+
file=f,
|
|
113
|
+
filename=ChemShellCalculation.FILE_DLFIND,
|
|
114
|
+
label="CJSON Structure File",
|
|
115
|
+
description=descrip,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
with open(dl_find_path, "rb") as f:
|
|
120
|
+
structure = chemsh_cjson_to_structure_data(f.read())
|
|
121
|
+
structure.label = "Optimised Structure"
|
|
122
|
+
structure.description = descrip
|
|
123
|
+
self.out("optimised_structure", structure)
|
|
105
124
|
self.parse_optimisation_path(
|
|
106
125
|
self.retrieved.get_object_content(
|
|
107
126
|
ChemShellCalculation.FILE_STDOUT, "r"
|
|
@@ -115,17 +134,7 @@ class ChemShellParser(Parser):
|
|
|
115
134
|
trj_frc_path = retrieved_tmp_folder / ChemShellCalculation.FILE_TRJFRC
|
|
116
135
|
if trj_path.exists():
|
|
117
136
|
self.parse_xyz_path(trj_path, "trajectory_path")
|
|
118
|
-
|
|
119
|
-
self.out(
|
|
120
|
-
"trajectory_force",
|
|
121
|
-
SinglefileData(
|
|
122
|
-
file=f,
|
|
123
|
-
filename=ChemShellCalculation.FILE_TRJFRC.replace(
|
|
124
|
-
"/", "_"
|
|
125
|
-
),
|
|
126
|
-
label="ChemShell optimisation trajectory.",
|
|
127
|
-
),
|
|
128
|
-
)
|
|
137
|
+
self.parse_xyz_forces(trj_frc_path, "trajectory_force")
|
|
129
138
|
else:
|
|
130
139
|
return self.exit_codes.ERROR_MISSING_OPTIMISED_STRUCTURE_FILE
|
|
131
140
|
|
|
@@ -161,7 +170,17 @@ class ChemShellParser(Parser):
|
|
|
161
170
|
read = True
|
|
162
171
|
elif "total S vib" in line:
|
|
163
172
|
read = False
|
|
164
|
-
self.out(
|
|
173
|
+
self.out(
|
|
174
|
+
"vibrational_energies",
|
|
175
|
+
Dict(
|
|
176
|
+
energies,
|
|
177
|
+
label="Thermochemical Properties",
|
|
178
|
+
description=(
|
|
179
|
+
"Thermochemical properties (ZPE, enthalpy, entropy, etc.) "
|
|
180
|
+
"calculated by ChemShell."
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
)
|
|
165
184
|
modes = numpy.asarray(modes)
|
|
166
185
|
modes_data_node = ArrayData(
|
|
167
186
|
label="Vibrational Modes",
|
|
@@ -214,6 +233,47 @@ class ChemShellParser(Parser):
|
|
|
214
233
|
self.out(output_link, path)
|
|
215
234
|
return
|
|
216
235
|
|
|
236
|
+
def parse_xyz_forces(self, file_path: Path, output_link: str) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Parse an XYZ style forces trajectory into an AiiDA ArrayData node.
|
|
239
|
+
|
|
240
|
+
Each frame is stored as a separate '(natoms, 3)' array labelled
|
|
241
|
+
'Frame_{i}' (zero-indexed). An underscore is used in place of a space as
|
|
242
|
+
AiiDA array names may only contain digits, letters and underscores.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
file_path : Path
|
|
247
|
+
Path to the XYZ style forces trajectory file to parse.
|
|
248
|
+
output_link : str
|
|
249
|
+
The output link label under which the resulting ArrayData node is
|
|
250
|
+
attached to the calculation.
|
|
251
|
+
"""
|
|
252
|
+
with open(file_path) as f:
|
|
253
|
+
lines = f.readlines()
|
|
254
|
+
natoms = int(lines[0])
|
|
255
|
+
forces = ArrayData(
|
|
256
|
+
label="Optimisation Path Forces",
|
|
257
|
+
description=(
|
|
258
|
+
"Per-atom forces at each step of a ChemShell geometry optimisation, "
|
|
259
|
+
"stored as one (natoms, 3) array per frame."
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
frame = 0
|
|
263
|
+
i = 0
|
|
264
|
+
while i < len(lines):
|
|
265
|
+
frame_forces = numpy.zeros((natoms, 3), dtype=float)
|
|
266
|
+
for atm_index, atm_line in enumerate(lines[i + 2 : i + 2 + natoms]):
|
|
267
|
+
line = atm_line.split()
|
|
268
|
+
frame_forces[atm_index][0] = float(line[1])
|
|
269
|
+
frame_forces[atm_index][1] = float(line[2])
|
|
270
|
+
frame_forces[atm_index][2] = float(line[3])
|
|
271
|
+
forces.set_array(f"Frame_{frame}", frame_forces)
|
|
272
|
+
frame += 1
|
|
273
|
+
i += natoms + 2
|
|
274
|
+
self.out(output_link, forces)
|
|
275
|
+
return
|
|
276
|
+
|
|
217
277
|
def parse_neb_info(self, file_path: Path) -> None:
|
|
218
278
|
"""Parse the NEB info file into an AiiDA ArrayData noe."""
|
|
219
279
|
output = ArrayData(
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
class UnitsConverter:
|
|
5
5
|
"""Unit conversion utility."""
|
|
6
6
|
|
|
7
|
-
ANGSTROM = 0.
|
|
7
|
+
ANGSTROM = 0.529177210903
|
|
8
8
|
EV = 27.2114
|
|
9
9
|
|
|
10
10
|
@classmethod
|
|
11
11
|
def angstrom_to_bohr(cls, val: float) -> float:
|
|
12
12
|
"""Convert Angstrom to Bohr (a.u.)."""
|
|
13
|
-
return val
|
|
13
|
+
return val / UnitsConverter.ANGSTROM
|
|
14
14
|
|
|
15
15
|
@classmethod
|
|
16
16
|
def bohr_to_angstrom(cls, val: float) -> float:
|