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.
Files changed (111) hide show
  1. digichem/__init__.py +75 -0
  2. digichem/basis.py +116 -0
  3. digichem/config/README +3 -0
  4. digichem/config/__init__.py +5 -0
  5. digichem/config/base.py +321 -0
  6. digichem/config/locations.py +14 -0
  7. digichem/config/parse.py +90 -0
  8. digichem/config/util.py +117 -0
  9. digichem/data/README +4 -0
  10. digichem/data/batoms/COPYING +18 -0
  11. digichem/data/batoms/LICENSE +674 -0
  12. digichem/data/batoms/README +2 -0
  13. digichem/data/batoms/__init__.py +0 -0
  14. digichem/data/batoms/batoms-renderer.py +351 -0
  15. digichem/data/config/digichem.yaml +714 -0
  16. digichem/data/functionals.csv +15 -0
  17. digichem/data/solvents.csv +185 -0
  18. digichem/data/tachyon/COPYING.md +5 -0
  19. digichem/data/tachyon/LICENSE +30 -0
  20. digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
  21. digichem/data/vmd/common.tcl +468 -0
  22. digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
  23. digichem/data/vmd/generate_density_images.tcl +45 -0
  24. digichem/data/vmd/generate_dipole_images.tcl +68 -0
  25. digichem/data/vmd/generate_orbital_images.tcl +57 -0
  26. digichem/data/vmd/generate_spin_images.tcl +66 -0
  27. digichem/data/vmd/generate_structure_images.tcl +40 -0
  28. digichem/datas.py +14 -0
  29. digichem/exception/__init__.py +7 -0
  30. digichem/exception/base.py +133 -0
  31. digichem/exception/uncatchable.py +63 -0
  32. digichem/file/__init__.py +1 -0
  33. digichem/file/base.py +364 -0
  34. digichem/file/cube.py +284 -0
  35. digichem/file/fchk.py +94 -0
  36. digichem/file/prattle.py +277 -0
  37. digichem/file/types.py +97 -0
  38. digichem/image/__init__.py +6 -0
  39. digichem/image/base.py +113 -0
  40. digichem/image/excited_states.py +335 -0
  41. digichem/image/graph.py +293 -0
  42. digichem/image/orbitals.py +239 -0
  43. digichem/image/render.py +617 -0
  44. digichem/image/spectroscopy.py +797 -0
  45. digichem/image/structure.py +115 -0
  46. digichem/image/vmd.py +826 -0
  47. digichem/input/__init__.py +3 -0
  48. digichem/input/base.py +78 -0
  49. digichem/input/digichem_input.py +500 -0
  50. digichem/input/gaussian.py +140 -0
  51. digichem/log.py +179 -0
  52. digichem/memory.py +166 -0
  53. digichem/misc/__init__.py +4 -0
  54. digichem/misc/argparse.py +44 -0
  55. digichem/misc/base.py +61 -0
  56. digichem/misc/io.py +239 -0
  57. digichem/misc/layered_dict.py +285 -0
  58. digichem/misc/text.py +139 -0
  59. digichem/misc/time.py +73 -0
  60. digichem/parse/__init__.py +13 -0
  61. digichem/parse/base.py +220 -0
  62. digichem/parse/cclib.py +138 -0
  63. digichem/parse/dump.py +253 -0
  64. digichem/parse/gaussian.py +130 -0
  65. digichem/parse/orca.py +96 -0
  66. digichem/parse/turbomole.py +201 -0
  67. digichem/parse/util.py +523 -0
  68. digichem/result/__init__.py +6 -0
  69. digichem/result/alignment/AA.py +114 -0
  70. digichem/result/alignment/AAA.py +61 -0
  71. digichem/result/alignment/FAP.py +148 -0
  72. digichem/result/alignment/__init__.py +3 -0
  73. digichem/result/alignment/base.py +310 -0
  74. digichem/result/angle.py +153 -0
  75. digichem/result/atom.py +742 -0
  76. digichem/result/base.py +258 -0
  77. digichem/result/dipole_moment.py +332 -0
  78. digichem/result/emission.py +402 -0
  79. digichem/result/energy.py +323 -0
  80. digichem/result/excited_state.py +821 -0
  81. digichem/result/ground_state.py +94 -0
  82. digichem/result/metadata.py +644 -0
  83. digichem/result/multi.py +98 -0
  84. digichem/result/nmr.py +1086 -0
  85. digichem/result/orbital.py +647 -0
  86. digichem/result/result.py +244 -0
  87. digichem/result/soc.py +272 -0
  88. digichem/result/spectroscopy.py +514 -0
  89. digichem/result/tdm.py +267 -0
  90. digichem/result/vibration.py +167 -0
  91. digichem/test/__init__.py +6 -0
  92. digichem/test/conftest.py +4 -0
  93. digichem/test/test_basis.py +71 -0
  94. digichem/test/test_calculate.py +30 -0
  95. digichem/test/test_config.py +78 -0
  96. digichem/test/test_cube.py +369 -0
  97. digichem/test/test_exception.py +16 -0
  98. digichem/test/test_file.py +104 -0
  99. digichem/test/test_image.py +337 -0
  100. digichem/test/test_input.py +64 -0
  101. digichem/test/test_parsing.py +79 -0
  102. digichem/test/test_prattle.py +36 -0
  103. digichem/test/test_result.py +489 -0
  104. digichem/test/test_translate.py +112 -0
  105. digichem/test/util.py +207 -0
  106. digichem/translate.py +591 -0
  107. digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
  108. digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
  109. digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
  110. digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
  111. 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
+