digichem-core 6.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digichem/__init__.py +75 -0
- digichem/basis.py +116 -0
- digichem/config/README +3 -0
- digichem/config/__init__.py +5 -0
- digichem/config/base.py +321 -0
- digichem/config/locations.py +14 -0
- digichem/config/parse.py +90 -0
- digichem/config/util.py +117 -0
- digichem/data/README +4 -0
- digichem/data/batoms/COPYING +18 -0
- digichem/data/batoms/LICENSE +674 -0
- digichem/data/batoms/README +2 -0
- digichem/data/batoms/__init__.py +0 -0
- digichem/data/batoms/batoms-renderer.py +351 -0
- digichem/data/config/digichem.yaml +714 -0
- digichem/data/functionals.csv +15 -0
- digichem/data/solvents.csv +185 -0
- digichem/data/tachyon/COPYING.md +5 -0
- digichem/data/tachyon/LICENSE +30 -0
- digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
- digichem/data/vmd/common.tcl +468 -0
- digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
- digichem/data/vmd/generate_density_images.tcl +45 -0
- digichem/data/vmd/generate_dipole_images.tcl +68 -0
- digichem/data/vmd/generate_orbital_images.tcl +57 -0
- digichem/data/vmd/generate_spin_images.tcl +66 -0
- digichem/data/vmd/generate_structure_images.tcl +40 -0
- digichem/datas.py +14 -0
- digichem/exception/__init__.py +7 -0
- digichem/exception/base.py +133 -0
- digichem/exception/uncatchable.py +63 -0
- digichem/file/__init__.py +1 -0
- digichem/file/base.py +364 -0
- digichem/file/cube.py +284 -0
- digichem/file/fchk.py +94 -0
- digichem/file/prattle.py +277 -0
- digichem/file/types.py +97 -0
- digichem/image/__init__.py +6 -0
- digichem/image/base.py +113 -0
- digichem/image/excited_states.py +335 -0
- digichem/image/graph.py +293 -0
- digichem/image/orbitals.py +239 -0
- digichem/image/render.py +617 -0
- digichem/image/spectroscopy.py +797 -0
- digichem/image/structure.py +115 -0
- digichem/image/vmd.py +826 -0
- digichem/input/__init__.py +3 -0
- digichem/input/base.py +78 -0
- digichem/input/digichem_input.py +500 -0
- digichem/input/gaussian.py +140 -0
- digichem/log.py +179 -0
- digichem/memory.py +166 -0
- digichem/misc/__init__.py +4 -0
- digichem/misc/argparse.py +44 -0
- digichem/misc/base.py +61 -0
- digichem/misc/io.py +239 -0
- digichem/misc/layered_dict.py +285 -0
- digichem/misc/text.py +139 -0
- digichem/misc/time.py +73 -0
- digichem/parse/__init__.py +13 -0
- digichem/parse/base.py +220 -0
- digichem/parse/cclib.py +138 -0
- digichem/parse/dump.py +253 -0
- digichem/parse/gaussian.py +130 -0
- digichem/parse/orca.py +96 -0
- digichem/parse/turbomole.py +201 -0
- digichem/parse/util.py +523 -0
- digichem/result/__init__.py +6 -0
- digichem/result/alignment/AA.py +114 -0
- digichem/result/alignment/AAA.py +61 -0
- digichem/result/alignment/FAP.py +148 -0
- digichem/result/alignment/__init__.py +3 -0
- digichem/result/alignment/base.py +310 -0
- digichem/result/angle.py +153 -0
- digichem/result/atom.py +742 -0
- digichem/result/base.py +258 -0
- digichem/result/dipole_moment.py +332 -0
- digichem/result/emission.py +402 -0
- digichem/result/energy.py +323 -0
- digichem/result/excited_state.py +821 -0
- digichem/result/ground_state.py +94 -0
- digichem/result/metadata.py +644 -0
- digichem/result/multi.py +98 -0
- digichem/result/nmr.py +1086 -0
- digichem/result/orbital.py +647 -0
- digichem/result/result.py +244 -0
- digichem/result/soc.py +272 -0
- digichem/result/spectroscopy.py +514 -0
- digichem/result/tdm.py +267 -0
- digichem/result/vibration.py +167 -0
- digichem/test/__init__.py +6 -0
- digichem/test/conftest.py +4 -0
- digichem/test/test_basis.py +71 -0
- digichem/test/test_calculate.py +30 -0
- digichem/test/test_config.py +78 -0
- digichem/test/test_cube.py +369 -0
- digichem/test/test_exception.py +16 -0
- digichem/test/test_file.py +104 -0
- digichem/test/test_image.py +337 -0
- digichem/test/test_input.py +64 -0
- digichem/test/test_parsing.py +79 -0
- digichem/test/test_prattle.py +36 -0
- digichem/test/test_result.py +489 -0
- digichem/test/test_translate.py +112 -0
- digichem/test/util.py +207 -0
- digichem/translate.py +591 -0
- digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
- digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
- digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
- digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
- digichem_core-6.0.0rc1.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"""Code for metadata regarding finished calculation results."""
|
|
2
|
+
|
|
3
|
+
# General imports.
|
|
4
|
+
from datetime import timedelta, datetime
|
|
5
|
+
import math
|
|
6
|
+
import itertools
|
|
7
|
+
from deepmerge import conservative_merger
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import copy
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
from digichem.misc.time import latest_datetime, total_timedelta, date_to_string,\
|
|
13
|
+
timedelta_to_string
|
|
14
|
+
from digichem.misc.text import andjoin
|
|
15
|
+
from digichem.exception import Result_unavailable_error
|
|
16
|
+
from digichem.result import Result_object
|
|
17
|
+
import digichem
|
|
18
|
+
from digichem import translate
|
|
19
|
+
from digichem.memory import Memory
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Solvent(Result_object):
|
|
23
|
+
"""
|
|
24
|
+
Class for storing solvent metadata.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, model = None, name = None, params = None):
|
|
28
|
+
self.model = model
|
|
29
|
+
self.name = name
|
|
30
|
+
# A raw dictionary of solvent related parameters.
|
|
31
|
+
self.params = params if params is not None else {}
|
|
32
|
+
|
|
33
|
+
# If we are missing only one of name and params['epsilon'], look up the missing value.
|
|
34
|
+
if self.name is None and "epsilon" in self.params:
|
|
35
|
+
try:
|
|
36
|
+
self.name = translate.Solvent.epsilon_to_name(self.params['epsilon'])
|
|
37
|
+
|
|
38
|
+
except ValueError:
|
|
39
|
+
# Nothing close.
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
if self.name is not None:
|
|
43
|
+
self.name = self.name.capitalize()
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_parser(self, parser):
|
|
47
|
+
"""
|
|
48
|
+
Construct a Metadata object from an output file parser.
|
|
49
|
+
|
|
50
|
+
:param parser: Output data parser.
|
|
51
|
+
:return: A populated Metadata object.
|
|
52
|
+
"""
|
|
53
|
+
return self(
|
|
54
|
+
name = parser.data.metadata.get('solvent_name', None),
|
|
55
|
+
model = parser.data.metadata.get('solvent_model', None),
|
|
56
|
+
params = parser.data.metadata.get('solvent_params', {}),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def description(self):
|
|
61
|
+
"""
|
|
62
|
+
A common-name description of this solvent.
|
|
63
|
+
"""
|
|
64
|
+
# If we have no solvent, say so.
|
|
65
|
+
if self.model is None:
|
|
66
|
+
return "Gas-phase"
|
|
67
|
+
|
|
68
|
+
# If we have a name, just use that.
|
|
69
|
+
elif self.name is not None:
|
|
70
|
+
return self.name
|
|
71
|
+
|
|
72
|
+
# If we only have epsilon, use that.
|
|
73
|
+
elif "epsilon" in self.params:
|
|
74
|
+
return "ε = {}".format(self.params['epsilon'])
|
|
75
|
+
|
|
76
|
+
# Give up.
|
|
77
|
+
else:
|
|
78
|
+
return "Unknown"
|
|
79
|
+
|
|
80
|
+
def dump(self, digichem_options):
|
|
81
|
+
return {
|
|
82
|
+
"model": self.model,
|
|
83
|
+
"name": self.name,
|
|
84
|
+
"params": self.params
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def merge(self, *multiple_objects):
|
|
89
|
+
"""
|
|
90
|
+
Merge multiple solvent implementations together.
|
|
91
|
+
"""
|
|
92
|
+
# Check all items are the same.
|
|
93
|
+
if not all(obj == multiple_objects[0] for obj in multiple_objects if obj is not None):
|
|
94
|
+
warnings.warn("Refusing to merge different solvent methods")
|
|
95
|
+
return self()
|
|
96
|
+
|
|
97
|
+
return multiple_objects[0]
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dump(self, data, result_set, options):
|
|
101
|
+
"""
|
|
102
|
+
Get an instance of this class from its dumped representation.
|
|
103
|
+
|
|
104
|
+
:param data: The data to parse.
|
|
105
|
+
:param result_set: The partially constructed result set which is being populated.
|
|
106
|
+
"""
|
|
107
|
+
return self(
|
|
108
|
+
model = data.get('model', None),
|
|
109
|
+
name = data.get('name', None),
|
|
110
|
+
params = data.get('params', {})
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def __eq__(self, other):
|
|
114
|
+
"""Is this solvent implementation the same as another one?"""
|
|
115
|
+
return (
|
|
116
|
+
# Might want to do this case-insensitive.
|
|
117
|
+
self.model == other.model and
|
|
118
|
+
abs(self.params.get("epsilon", math.inf) - self.params.get("epsilon", math.inf)) < 0.001
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Metadata(Result_object):
|
|
123
|
+
"""
|
|
124
|
+
Class for storing calculation metadata.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# A dictionary of known computational orders, where the key is name of the method (upper case) and the value is an integer describing the ordered 'level of theory'.
|
|
128
|
+
METHODS = {
|
|
129
|
+
"HF": 1,
|
|
130
|
+
"DFT": 2,
|
|
131
|
+
"LMP2": 3,
|
|
132
|
+
"DF-MP2": 4,
|
|
133
|
+
"MP2": 5,
|
|
134
|
+
"MP3": 6,
|
|
135
|
+
"MP4": 7,
|
|
136
|
+
"MP5": 8,
|
|
137
|
+
"CCSD": 9,
|
|
138
|
+
"CCSD(T)": 10,
|
|
139
|
+
"CCSD-T": 11
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
name = None,
|
|
145
|
+
user = None,
|
|
146
|
+
log_files = None,
|
|
147
|
+
auxiliary_files = None,
|
|
148
|
+
history = None,
|
|
149
|
+
date = None,
|
|
150
|
+
duration = None,
|
|
151
|
+
package = None,
|
|
152
|
+
package_version = None,
|
|
153
|
+
calculations = None,
|
|
154
|
+
success = None,
|
|
155
|
+
methods = None,
|
|
156
|
+
functional = None,
|
|
157
|
+
basis_set = None,
|
|
158
|
+
charge = None,
|
|
159
|
+
multiplicity = None,
|
|
160
|
+
optimisation_converged = None,
|
|
161
|
+
temperature = None,
|
|
162
|
+
pressure = None,
|
|
163
|
+
orbital_spin_type = None,
|
|
164
|
+
digichem_version = None,
|
|
165
|
+
solvent = None,
|
|
166
|
+
num_cpu = None,
|
|
167
|
+
memory_available = None,
|
|
168
|
+
memory_used = None,
|
|
169
|
+
|
|
170
|
+
# Deprecated.
|
|
171
|
+
solvent_model = None,
|
|
172
|
+
solvent_name = None,):
|
|
173
|
+
"""
|
|
174
|
+
Constructor for result Metadata objects.
|
|
175
|
+
|
|
176
|
+
:param name: Optional name of this calculation result.
|
|
177
|
+
:param user: The username of the user who parsed this result.
|
|
178
|
+
:param log_files: An optional list of text-based calculation log files from which this result was parsed.
|
|
179
|
+
:param auxiliary_files: An optional dict of auxiliary files associated with this calculation result.
|
|
180
|
+
:param history: Optional SHA of the calculation from which the coordinates of this calculation were generated.
|
|
181
|
+
:param num_calculations: Optional number of individual calculations this metadata represents.
|
|
182
|
+
:param date: Optional date (datetime object) of this calculation result.
|
|
183
|
+
:param duration: Optional duration (timedelta object) of this calculation.
|
|
184
|
+
:param package: Optional string identifying the computational chem program that performed the calculation.
|
|
185
|
+
:param package_version: Optional string identifying the version of the computational chem program that performed the calculation.
|
|
186
|
+
:param calculations: A list of strings of the different calculations carried out (Opt, Freq, TD, TDA, SP etc).
|
|
187
|
+
:param success: Whether the calculation completed successfully or not.
|
|
188
|
+
:param methods: List of methods (DFT, HF, MPn etc) used in the calculation.
|
|
189
|
+
:param functional: Functional used in the calculation.
|
|
190
|
+
:param basis_set: Basis set used in the calculation.
|
|
191
|
+
:param charge: Charge (positive or negative integer) of the system studied.
|
|
192
|
+
:param multiplicity: The multiplicity of the system.
|
|
193
|
+
:param optimisation_converged: Whether the optimisation converged or not.
|
|
194
|
+
:param temperature: The temperature used in the calculation (not always relevant).
|
|
195
|
+
:param pressure: The pressure used in the calculation (not always relevant).
|
|
196
|
+
:param orbital_spin_type: The types of orbitals that have been calculated, either 'restricted' or 'unrestricted'.
|
|
197
|
+
:param num_cpu: The number of CPUs used to perform this calculation.
|
|
198
|
+
:param memory_available: The maximum amount of memory available to this calculation (the amount requested by the user).
|
|
199
|
+
:param memory_used: The maximum amount of memory used by the calculation (the amount requested by the user).
|
|
200
|
+
"""
|
|
201
|
+
self.num_calculations = 1
|
|
202
|
+
self.name = name
|
|
203
|
+
self.user = user
|
|
204
|
+
self.log_files = log_files if log_files is not None else []
|
|
205
|
+
self.auxiliary_files = auxiliary_files if auxiliary_files is not None and len(auxiliary_files) != 0 else {}
|
|
206
|
+
self.history = history
|
|
207
|
+
self.date = date
|
|
208
|
+
self.duration = duration
|
|
209
|
+
self.package = package
|
|
210
|
+
self.package_version = package_version
|
|
211
|
+
self.calculations = calculations if calculations is not None else []
|
|
212
|
+
self.success = success
|
|
213
|
+
self.methods = methods if methods is not None else []
|
|
214
|
+
self.functional = functional
|
|
215
|
+
self.basis_set = basis_set
|
|
216
|
+
# TODO: charge and mult should be deprecated here, they are available in ground_state.
|
|
217
|
+
self.charge = charge
|
|
218
|
+
self.multiplicity = multiplicity
|
|
219
|
+
self.optimisation_converged = optimisation_converged
|
|
220
|
+
self.temperature = temperature
|
|
221
|
+
self.pressure = pressure
|
|
222
|
+
self.orbital_spin_type = orbital_spin_type
|
|
223
|
+
# TOOD: Ideally this would be parsed from the calculation output somehow, but this is fine for now.
|
|
224
|
+
self.digichem_version = digichem.__version__ if digichem_version is None else digichem_version
|
|
225
|
+
self.solvent = solvent
|
|
226
|
+
self.num_cpu = num_cpu
|
|
227
|
+
self.memory_available = memory_available
|
|
228
|
+
self.memory_used = memory_used
|
|
229
|
+
|
|
230
|
+
# Deprecated solvent system.
|
|
231
|
+
if solvent_model is not None:
|
|
232
|
+
self.solvent.model = solvent_model
|
|
233
|
+
|
|
234
|
+
if solvent_name is not None:
|
|
235
|
+
self.solvent.name = solvent_name
|
|
236
|
+
|
|
237
|
+
# TODO: This is more than a bit clumsy and in general the handling of names should be improved.
|
|
238
|
+
@property
|
|
239
|
+
def molecule_name(self):
|
|
240
|
+
return Path(self.name).name if self.name is not None else None
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def level_of_theory(self):
|
|
244
|
+
"""
|
|
245
|
+
A short-hand summary of the methods and basis sets used.
|
|
246
|
+
"""
|
|
247
|
+
theories = []
|
|
248
|
+
if len(self.converted_methods) > 0:
|
|
249
|
+
#theories.extend(self.converted_methods)
|
|
250
|
+
theories.append(self.converted_methods[-1])
|
|
251
|
+
|
|
252
|
+
if self.basis_set is not None:
|
|
253
|
+
theories.append(self.basis_set)
|
|
254
|
+
|
|
255
|
+
return("/".join(theories))
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def merge(self, *multiple_metadatas):
|
|
259
|
+
"""
|
|
260
|
+
Merge multiple metadata objects into a single metadata.
|
|
261
|
+
"""
|
|
262
|
+
return Merged_metadata.from_metadatas(*multiple_metadatas)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def converted_methods(self):
|
|
266
|
+
"""
|
|
267
|
+
Similar to the methods attribute but where DFT is replaced with the actual functional used.
|
|
268
|
+
"""
|
|
269
|
+
return [self.functional if method == "DFT" and self.functional is not None else method for method in self.methods if method is not None]
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def description(self):
|
|
273
|
+
desc = []
|
|
274
|
+
if self.name is not None:
|
|
275
|
+
desc.append(self.name)
|
|
276
|
+
|
|
277
|
+
desc.append(self.identity_string)
|
|
278
|
+
|
|
279
|
+
return ", ".join(desc)
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def identity(self):
|
|
283
|
+
"""
|
|
284
|
+
A dictionary of critical attributes that identify a calculation.
|
|
285
|
+
"""
|
|
286
|
+
# Get our list of methods, replacing 'DFT' with the functional used if available.
|
|
287
|
+
return {
|
|
288
|
+
"package": self.package,
|
|
289
|
+
"calculations": self.calculations_string,
|
|
290
|
+
"methods": ", ".join(self.converted_methods) if len(self.converted_methods) > 0 else None,
|
|
291
|
+
"basis": self.basis_set,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def identity_string(self):
|
|
296
|
+
"""
|
|
297
|
+
A string that identifies this calculation.
|
|
298
|
+
"""
|
|
299
|
+
return " ".join([value for value in self.identity.values() if value is not None])
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def sorted_methods(self, methods):
|
|
304
|
+
"""
|
|
305
|
+
Order a list of methods (HF, DFT, MP2 etc) in terms of 'level of theory', with the lowest level (HF or DFT probably) first and highest (MP4, CCSD etc) last.
|
|
306
|
+
"""
|
|
307
|
+
return sorted((method.upper() for method in methods), key = lambda method: self.METHODS[method] if method in self.METHODS else math.inf)
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def package_string(self):
|
|
311
|
+
"""
|
|
312
|
+
The comp chem package and version combined into one string.
|
|
313
|
+
"""
|
|
314
|
+
package_string = self.package if self.package is not None else ""
|
|
315
|
+
|
|
316
|
+
# Add version string if we have it.
|
|
317
|
+
package_string += " " if package_string != "" else ""
|
|
318
|
+
package_string += "({})".format(self.package_version)
|
|
319
|
+
|
|
320
|
+
# Done.
|
|
321
|
+
return package_string
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def calculations_string(self):
|
|
325
|
+
"""
|
|
326
|
+
Get the list of calculation types as a single string, or None if there are no calculations set.
|
|
327
|
+
"""
|
|
328
|
+
return ", ".join(self.calculations) if len(self.calculations) != 0 else None
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def methods_string(self):
|
|
332
|
+
"""
|
|
333
|
+
Get the list of calculation methods as a single string, or None if there are no methods set.
|
|
334
|
+
"""
|
|
335
|
+
return ", ".join(self.methods) if len(self.methods) != 0 else None
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def human_calculations_string(self):
|
|
339
|
+
"""
|
|
340
|
+
A version of calculations_string that is more pleasant to read.
|
|
341
|
+
"""
|
|
342
|
+
calculations = []
|
|
343
|
+
if "Single Point" in self.calculations:
|
|
344
|
+
calculations.append("single point energy")
|
|
345
|
+
|
|
346
|
+
if "Optimisation" in self.calculations:
|
|
347
|
+
calculations.append("optimised structure")
|
|
348
|
+
|
|
349
|
+
if "Frequencies" in self.calculations:
|
|
350
|
+
calculations.append("vibrational frequencies")
|
|
351
|
+
|
|
352
|
+
if "Excited States" in self.calculations:
|
|
353
|
+
calculations.append("excited states")
|
|
354
|
+
|
|
355
|
+
if "NMR" in self.calculations:
|
|
356
|
+
calculations.append("NMR properties")
|
|
357
|
+
|
|
358
|
+
if len(calculations) == 0:
|
|
359
|
+
# Use a generic term.
|
|
360
|
+
calculations.append("properties")
|
|
361
|
+
|
|
362
|
+
return andjoin(calculations)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def get_calculation_types_from_cclib(self, ccdata):
|
|
366
|
+
"""
|
|
367
|
+
Determine what types of calculation we did from the data provided by cclib.
|
|
368
|
+
|
|
369
|
+
:param ccdata: The data from cclib.
|
|
370
|
+
:return: A list of strings.
|
|
371
|
+
"""
|
|
372
|
+
calcs = []
|
|
373
|
+
# Need to think of a better way to determine what is an optimisation, as frequency calcs contain an optdone for some reason.
|
|
374
|
+
#if hasattr(ccdata, 'optdone'):
|
|
375
|
+
if len(getattr(ccdata, 'scfenergies', [])) > 1:
|
|
376
|
+
calcs.append('Optimisation')
|
|
377
|
+
if hasattr(ccdata, 'vibfreqs'):
|
|
378
|
+
calcs.append('Frequencies')
|
|
379
|
+
if hasattr(ccdata, 'etenergies'):
|
|
380
|
+
calcs.append('Excited States')
|
|
381
|
+
if hasattr(ccdata, 'nmrtensors'):
|
|
382
|
+
calcs.append('NMR')
|
|
383
|
+
# If our list is empty, assume we did an SP.
|
|
384
|
+
if len(calcs) == 0 and ( hasattr(ccdata, 'scfenergies') or hasattr(ccdata, 'mpenergies') or hasattr(ccdata, 'ccenergies') ):
|
|
385
|
+
calcs = ['Single Point']
|
|
386
|
+
# Return our list.
|
|
387
|
+
return calcs
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
def get_methods_from_cclib(self, ccdata):
|
|
391
|
+
"""
|
|
392
|
+
Get a list of method types (DFT, MP etc.) from the data provided by cclib.
|
|
393
|
+
|
|
394
|
+
:param ccdata: The data from cclib.
|
|
395
|
+
:return: A list of strings of the methods used (each method appears once only; the order has no special significance). The list may be empty.
|
|
396
|
+
"""
|
|
397
|
+
return self.sorted_methods((set(ccdata.metadata.get('methods', []))))
|
|
398
|
+
#return list(dict.fromkeys(ccdata.metadata.get('methods', [])))
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
def get_orbital_spin_type_from_cclib(self, ccdata):
|
|
402
|
+
"""
|
|
403
|
+
Determine the orbital type (restricted, unrestricted) from the data provided by cclib.
|
|
404
|
+
|
|
405
|
+
:return ccdata: The data from cclib.
|
|
406
|
+
:return: A string describing the orbital type ('restricted', 'unrestricted' or 'none').
|
|
407
|
+
"""
|
|
408
|
+
#mo_len = len(ccdata.get('moenergies', []))
|
|
409
|
+
mo_len = len(getattr(ccdata, 'moenergies', []))
|
|
410
|
+
if mo_len == 1:
|
|
411
|
+
return "restricted"
|
|
412
|
+
elif mo_len == 2:
|
|
413
|
+
return "unrestricted"
|
|
414
|
+
else:
|
|
415
|
+
return "none"
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def from_parser(self, parser):
|
|
419
|
+
"""
|
|
420
|
+
Construct a Metadata object from an output file parser.
|
|
421
|
+
|
|
422
|
+
:param parser: Output data parser.
|
|
423
|
+
:return: A populated Metadata object.
|
|
424
|
+
"""
|
|
425
|
+
try:
|
|
426
|
+
# Do some processing on time objects.
|
|
427
|
+
if parser.data.metadata.get('wall_time', None) is not None:
|
|
428
|
+
duration = sum(parser.data.metadata['wall_time'], timedelta())
|
|
429
|
+
|
|
430
|
+
elif parser.data.metadata.get('cpu_time', None) is not None and parser.data.metadata.get('num_cpus', None) is not None:
|
|
431
|
+
# We can estimate the wall time from the CPU time and number of cpus.
|
|
432
|
+
duration = sum(parser.data.metadata['cpu_time'], timedelta()) / parser.data.metadata['num_cpus']
|
|
433
|
+
|
|
434
|
+
else:
|
|
435
|
+
duration = None
|
|
436
|
+
|
|
437
|
+
if parser.data.metadata.get('date', None) is not None:
|
|
438
|
+
date = datetime.fromtimestamp(parser.data.metadata['date'])
|
|
439
|
+
else:
|
|
440
|
+
date = None
|
|
441
|
+
|
|
442
|
+
memory_used = Memory(parser.data.metadata['memory_used']) if "memory_used" in parser.data.metadata else None
|
|
443
|
+
memory_available = Memory(parser.data.metadata['memory_available']) if "memory_available" in parser.data.metadata else None
|
|
444
|
+
|
|
445
|
+
# TODO: This doesn't seem to make sense; the parser already contains a metadata object...
|
|
446
|
+
return self(
|
|
447
|
+
name = parser.data.metadata.get('name', None),
|
|
448
|
+
user = parser.data.metadata.get('user', None),
|
|
449
|
+
log_files = parser.data.metadata.get('log_files', None),
|
|
450
|
+
auxiliary_files = parser.data.metadata.get('auxiliary_files', None),
|
|
451
|
+
date = date,
|
|
452
|
+
duration = duration,
|
|
453
|
+
package = parser.data.metadata.get('package', None),
|
|
454
|
+
package_version = parser.data.metadata.get('package_version', None),
|
|
455
|
+
calculations = self.get_calculation_types_from_cclib(parser.data),
|
|
456
|
+
success = parser.data.metadata.get('success', None),
|
|
457
|
+
methods = self.get_methods_from_cclib(parser.data),
|
|
458
|
+
functional = parser.data.metadata.get('functional', None),
|
|
459
|
+
basis_set = parser.data.metadata.get('basis_set', None),
|
|
460
|
+
|
|
461
|
+
charge = getattr(parser.data, 'charge', None),
|
|
462
|
+
multiplicity = getattr(parser.data, 'mult', None),
|
|
463
|
+
optimisation_converged = getattr(parser.data, 'optdone', None),
|
|
464
|
+
temperature = getattr(parser.data, 'temperature', None),
|
|
465
|
+
pressure = getattr(parser.data, 'pressure', None),
|
|
466
|
+
orbital_spin_type = self.get_orbital_spin_type_from_cclib(parser.data),
|
|
467
|
+
|
|
468
|
+
solvent = Solvent.from_parser(parser),
|
|
469
|
+
|
|
470
|
+
num_cpu = parser.data.metadata.get('num_cpu', None),
|
|
471
|
+
memory_available = memory_available,
|
|
472
|
+
memory_used = memory_used,
|
|
473
|
+
)
|
|
474
|
+
except AttributeError:
|
|
475
|
+
# There is no metadata available, give up.
|
|
476
|
+
raise Result_unavailable_error("Metadata", "no metadata is available")
|
|
477
|
+
|
|
478
|
+
def dump(self, digichem_options):
|
|
479
|
+
"""
|
|
480
|
+
Get a representation of this result object in primitive format.
|
|
481
|
+
"""
|
|
482
|
+
# Most attributes we can just dump as is.
|
|
483
|
+
attr_dict = {
|
|
484
|
+
"name": self.name,
|
|
485
|
+
"log_files": [str(log_file) for log_file in self.log_files],
|
|
486
|
+
"auxiliary_files": {aux_file_name: str(aux_file) for aux_file_name, aux_file in self.auxiliary_files.items()}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
attrs = [
|
|
490
|
+
"history",
|
|
491
|
+
"charge",
|
|
492
|
+
"multiplicity",
|
|
493
|
+
"user",
|
|
494
|
+
"package",
|
|
495
|
+
"package_version",
|
|
496
|
+
"digichem_version",
|
|
497
|
+
"calculations",
|
|
498
|
+
"methods",
|
|
499
|
+
"functional",
|
|
500
|
+
"basis_set",
|
|
501
|
+
"orbital_spin_type",
|
|
502
|
+
"success",
|
|
503
|
+
"optimisation_converged",
|
|
504
|
+
]
|
|
505
|
+
attr_dict.update({attr: getattr(self, attr) for attr in attrs})
|
|
506
|
+
|
|
507
|
+
# Add some more complex stuff.
|
|
508
|
+
attr_dict['date'] = {
|
|
509
|
+
"value": self.date.timestamp() if self.date is not None else None,
|
|
510
|
+
"units": "s",
|
|
511
|
+
"string": date_to_string(self.date) if self.date is not None else None
|
|
512
|
+
}
|
|
513
|
+
attr_dict['duration'] = {
|
|
514
|
+
"value": self.duration.total_seconds() if self.duration is not None else None,
|
|
515
|
+
"units": "s",
|
|
516
|
+
"string": timedelta_to_string(self.duration) if self.duration is not None else None
|
|
517
|
+
}
|
|
518
|
+
attr_dict["temperature"] = {
|
|
519
|
+
"value": self.temperature,
|
|
520
|
+
"units": "K"
|
|
521
|
+
}
|
|
522
|
+
attr_dict["pressure"] = {
|
|
523
|
+
"value": self.pressure,
|
|
524
|
+
"units": "atm"
|
|
525
|
+
}
|
|
526
|
+
attr_dict["solvent"] = self.solvent.dump(digichem_options)
|
|
527
|
+
|
|
528
|
+
attr_dict['num_cpu'] = self.num_cpu
|
|
529
|
+
for attr_name in ("memory_used", "memory_available"):
|
|
530
|
+
if getattr(self, attr_name) is not None:
|
|
531
|
+
value, unit = getattr(self, attr_name).auto_units
|
|
532
|
+
attr_dict[attr_name] = {
|
|
533
|
+
"value": value,
|
|
534
|
+
"units": unit
|
|
535
|
+
}
|
|
536
|
+
else:
|
|
537
|
+
attr_dict[attr_name] = {
|
|
538
|
+
"value": None,
|
|
539
|
+
"units": None
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return attr_dict
|
|
543
|
+
|
|
544
|
+
@classmethod
|
|
545
|
+
def from_dump(self, data, result_set, options):
|
|
546
|
+
"""
|
|
547
|
+
Get an instance of this class from its dumped representation.
|
|
548
|
+
|
|
549
|
+
:param data: The data to parse.
|
|
550
|
+
:param result_set: The partially constructed result set which is being populated.
|
|
551
|
+
"""
|
|
552
|
+
# Assemble our args to pass to the constructor.
|
|
553
|
+
# Most of these can be used from the metadata dict as is.
|
|
554
|
+
kwargs = copy.deepcopy(data)
|
|
555
|
+
|
|
556
|
+
# For more complex fields, use the data item.
|
|
557
|
+
for attr in ['date', 'duration', 'temperature', "pressure"]:
|
|
558
|
+
kwargs[attr] = data[attr]['value']
|
|
559
|
+
|
|
560
|
+
kwargs['date'] = datetime.fromtimestamp(kwargs['date']) if kwargs['date'] is not None else None
|
|
561
|
+
kwargs['duration'] = timedelta(seconds = kwargs['duration']) if kwargs['duration'] is not None else None
|
|
562
|
+
|
|
563
|
+
kwargs['solvent'] = Solvent.from_dump(data.get('solvent', {}), result_set, options)
|
|
564
|
+
|
|
565
|
+
for attr_name in ("memory_used", "memory_available"):
|
|
566
|
+
if attr_name in data and data[attr_name]['value'] is not None:
|
|
567
|
+
kwargs[attr_name] = Memory.from_units(data[attr_name]["value"], data[attr_name]["units"])
|
|
568
|
+
|
|
569
|
+
else:
|
|
570
|
+
kwargs[attr_name] = None
|
|
571
|
+
|
|
572
|
+
return self(**kwargs)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class Merged_metadata(Metadata):
|
|
576
|
+
"""
|
|
577
|
+
A modified metadata class for merged calculation results.
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def __init__(self, num_calculations, *args, **kwargs):
|
|
582
|
+
"""
|
|
583
|
+
:param num_calculations: The number of merged calculations this metadata represents.
|
|
584
|
+
"""
|
|
585
|
+
super().__init__(*args, log_files = None, auxiliary_files = None, **kwargs)
|
|
586
|
+
self.num_calculations = num_calculations
|
|
587
|
+
|
|
588
|
+
@classmethod
|
|
589
|
+
def from_metadatas(self, *multiple_metadatas):
|
|
590
|
+
"""
|
|
591
|
+
Create a merged metadata object from multiple metadata objects.
|
|
592
|
+
|
|
593
|
+
:param multiple_metadatas: A list of metadata objects to merge.
|
|
594
|
+
:returns: A new Merged_metadata object.
|
|
595
|
+
"""
|
|
596
|
+
# Our merged metadata.
|
|
597
|
+
merged_metadata = self(num_calculations = len(multiple_metadatas))
|
|
598
|
+
for attr in ("name", "user", "package", "package_version", "functional", "basis_set"):
|
|
599
|
+
setattr(merged_metadata, attr, self.merged_attr(attr, multiple_metadatas))
|
|
600
|
+
|
|
601
|
+
# We take the latest of the two dates.
|
|
602
|
+
merged_metadata.date = latest_datetime(*[metadata.date for metadata in multiple_metadatas if metadata.date is not None])
|
|
603
|
+
# And the total duration.
|
|
604
|
+
merged_metadata.duration = total_timedelta(*[metadata.duration for metadata in multiple_metadatas if metadata.duration is not None])
|
|
605
|
+
|
|
606
|
+
# Merge methods and calculations (but keep unique only).
|
|
607
|
+
merged_metadata.calculations = list(dict.fromkeys(itertools.chain(*(metadata.calculations for metadata in multiple_metadatas))))
|
|
608
|
+
merged_metadata.methods = self.sorted_methods(set(itertools.chain(*(metadata.methods for metadata in multiple_metadatas))))
|
|
609
|
+
|
|
610
|
+
# Keep the solvent if it's the same for all, otherwise discard.
|
|
611
|
+
merged_metadata.solvent = multiple_metadatas[0].solvent.merge(*[other.solvent for other in multiple_metadatas[1:]])
|
|
612
|
+
|
|
613
|
+
# Combine performance data.
|
|
614
|
+
merged_metadata.num_cpu = sum([meta.num_cpu for meta in multiple_metadatas if meta.num_cpu is not None])
|
|
615
|
+
merged_metadata.memory_available = Memory(sum([int(meta.memory_available) for meta in multiple_metadatas if meta.memory_available is not None]))
|
|
616
|
+
merged_metadata.memory_used = Memory(sum([int(meta.memory_used) for meta in multiple_metadatas if meta.memory_used is not None]))
|
|
617
|
+
|
|
618
|
+
# We are only successful if all calcs are successful.
|
|
619
|
+
merged_metadata.success = all((metadata.success for metadata in multiple_metadatas))
|
|
620
|
+
converged = [metadata.optimisation_converged for metadata in multiple_metadatas]
|
|
621
|
+
merged_metadata.optimisation_converged = False if False in converged else True if True in converged else None
|
|
622
|
+
|
|
623
|
+
# Only keep these if all the same.
|
|
624
|
+
for attr in ("temperature", "pressure"):
|
|
625
|
+
# Get a unique list of the attrs.
|
|
626
|
+
attr_set = set(getattr(metadata, attr) for metadata in multiple_metadatas)
|
|
627
|
+
|
|
628
|
+
# Only keep if we have exactly one entry.
|
|
629
|
+
if len(attr_set) == 1:
|
|
630
|
+
setattr(merged_metadata, attr, tuple(attr_set)[0])
|
|
631
|
+
|
|
632
|
+
# We take the first orbital spin type charge and mult, as this is what our orbital list will actually be.
|
|
633
|
+
merged_metadata.orbital_spin_type = multiple_metadatas[0].orbital_spin_type
|
|
634
|
+
merged_metadata.charge = multiple_metadatas[0].charge
|
|
635
|
+
merged_metadata.multiplicity = multiple_metadatas[0].multiplicity
|
|
636
|
+
|
|
637
|
+
# CAREFULLY merge aux files, so that later files do not overwrite earlier ones.
|
|
638
|
+
# This is useful behaviour because it matches how other results are merged, so certain aux files
|
|
639
|
+
# (turbomole density files) will still match their respective results.
|
|
640
|
+
for metadata in multiple_metadatas:
|
|
641
|
+
merged_metadata.auxiliary_files = conservative_merger.merge(merged_metadata.auxiliary_files, metadata.auxiliary_files)
|
|
642
|
+
|
|
643
|
+
return merged_metadata
|
|
644
|
+
|