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
digichem/misc/time.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Miscellaneous functions for handling date and time.
|
|
2
|
+
|
|
3
|
+
# General imports.
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
def latest_datetime(*datetimes):
|
|
7
|
+
"""
|
|
8
|
+
Return the latest of an iterable of datetimes.
|
|
9
|
+
|
|
10
|
+
:param datetimes: Datetimes, of which the latest will be returned.
|
|
11
|
+
:returns: The latest of the given datetimes, or None if no datetimes were given.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
datetimes = list(datetimes)
|
|
15
|
+
datetimes.sort()
|
|
16
|
+
return datetimes[-1]
|
|
17
|
+
except IndexError:
|
|
18
|
+
# No datetimes.
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def total_timedelta(*timedeltas):
|
|
22
|
+
"""
|
|
23
|
+
Return the sum of multiple timedeltas.
|
|
24
|
+
|
|
25
|
+
:param timedeltas: Timedelta objects to be summed.
|
|
26
|
+
:returns: A new timedelta object that is the sum of the objects given, or None if no timedeltas were given.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
return sum(timedeltas[1:], timedeltas[0])
|
|
30
|
+
except IndexError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def date_to_string(datetime_object):
|
|
35
|
+
"""
|
|
36
|
+
Convert a datetime object to a standard string representation.
|
|
37
|
+
|
|
38
|
+
:param datetime_object: The date and time to convert
|
|
39
|
+
:return: A string representation of datetime_object.
|
|
40
|
+
"""
|
|
41
|
+
return datetime_object.strftime("%d/%m/%Y %H:%M:%S")
|
|
42
|
+
|
|
43
|
+
def timedelta_to_string(timedelta_object):
|
|
44
|
+
"""
|
|
45
|
+
Convert a timedelta object to a standard string representation.
|
|
46
|
+
|
|
47
|
+
:param timedelta_object: The time difference to convert
|
|
48
|
+
:return: A string representation of timedelta_object.
|
|
49
|
+
"""
|
|
50
|
+
hours = math.floor(timedelta_object.seconds / 3600)
|
|
51
|
+
minutes = math.floor((timedelta_object.seconds - hours * 3600) / 60)
|
|
52
|
+
# Seconds lacks microsecond accuracy, but we can add it on.
|
|
53
|
+
seconds = round((timedelta_object.seconds + timedelta_object.microseconds /1000000) - (hours * 3600 + minutes * 60))
|
|
54
|
+
|
|
55
|
+
date_str = ""
|
|
56
|
+
add = False
|
|
57
|
+
|
|
58
|
+
# Only add each section if not zero.
|
|
59
|
+
if timedelta_object.days != 0:
|
|
60
|
+
date_str += "{} d, ".format(timedelta_object.days)
|
|
61
|
+
add = True
|
|
62
|
+
|
|
63
|
+
if hours != 0 or add:
|
|
64
|
+
date_str += "{} h, ".format(hours)
|
|
65
|
+
add = True
|
|
66
|
+
|
|
67
|
+
if minutes != 0 or add:
|
|
68
|
+
date_str += "{} m, ".format(minutes)
|
|
69
|
+
add = True
|
|
70
|
+
|
|
71
|
+
date_str += "{} s".format(seconds)
|
|
72
|
+
|
|
73
|
+
return date_str
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Classes for parsing calculation result data.
|
|
3
|
+
|
|
4
|
+
Most of the heavy lifting is done by cclib, we just extract additional data not currently handed by cclib.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# These alignment classes are needed to parse correctly.
|
|
8
|
+
from digichem.result.alignment.AAA import Adjusted_average_angle
|
|
9
|
+
from digichem.result.alignment.AA import Average_angle
|
|
10
|
+
from digichem.result.alignment.FAP import Furthest_atom_pair
|
|
11
|
+
from digichem.result.alignment import Minimal
|
|
12
|
+
|
|
13
|
+
from digichem.parse.util import from_log_files, parse_calculation, parse_and_merge_calculations, parse_multiple_calculations, parse_and_merge_multiple_calculations
|
digichem/parse/base.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# General imports.
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import pwd
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from digichem.exception.base import Digichem_exception
|
|
7
|
+
from digichem.result.orbital import Molecular_orbital_list,\
|
|
8
|
+
Beta_orbital
|
|
9
|
+
from digichem.result.metadata import Metadata
|
|
10
|
+
from digichem.result.result import Result_set
|
|
11
|
+
from digichem.result.atom import Atom_list
|
|
12
|
+
from digichem.result.tdm import Transition_dipole_moment
|
|
13
|
+
from digichem.result.excited_state import Excited_state_list
|
|
14
|
+
from digichem.result.energy import Energies
|
|
15
|
+
from digichem.result.ground_state import Ground_state
|
|
16
|
+
from digichem.result.soc import SOC_list
|
|
17
|
+
from digichem.result.dipole_moment import Dipole_moment
|
|
18
|
+
from digichem.result.vibration import Vibrations_list
|
|
19
|
+
from digichem.result.emission import Relaxed_excited_state
|
|
20
|
+
from digichem.result.nmr import NMR_shielding, NMR_spin_couplings_list, NMR_list
|
|
21
|
+
from digichem.result.alignment.base import Minimal, Alignment
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# NOTE: This is a repeat of the list in util to avoid circular import nonsense.
|
|
25
|
+
custom_parsing_formats = [
|
|
26
|
+
"sir",
|
|
27
|
+
"sij",
|
|
28
|
+
"yaml",
|
|
29
|
+
"json"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
class Parser_abc():
|
|
33
|
+
"""ABC for all parsers."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, *log_files, raw_data = None, **kwargs):
|
|
36
|
+
"""
|
|
37
|
+
Top level constructor for calculation parsers.
|
|
38
|
+
|
|
39
|
+
:param log_files: A list of output file to analyse/parse. The first log_file given will be used for naming purposes.
|
|
40
|
+
"""
|
|
41
|
+
# Set our name.
|
|
42
|
+
self.log_file_paths = self.sort_log_files([Path(log_file) for log_file in log_files if log_file is not None])
|
|
43
|
+
|
|
44
|
+
# Panic if we have no logs.
|
|
45
|
+
if len(self.log_file_paths) == 0:
|
|
46
|
+
raise Digichem_exception("Cannot parse calculation output; no available log files. Are you sure the given path is a log file or directory containing log files?")
|
|
47
|
+
|
|
48
|
+
# An object that we will populate with raw results.
|
|
49
|
+
self.data = raw_data
|
|
50
|
+
|
|
51
|
+
# A result set object that we'll populate with results.
|
|
52
|
+
self.results = None
|
|
53
|
+
|
|
54
|
+
# Parse (if we haven't already).
|
|
55
|
+
try:
|
|
56
|
+
if self.data is None:
|
|
57
|
+
self.parse()
|
|
58
|
+
except Exception:
|
|
59
|
+
raise Digichem_exception("Error parsing calculation result '{}'".format(self.description))
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_logs(self, *log_files, **kwargs):
|
|
63
|
+
"""
|
|
64
|
+
Intelligent constructor that will attempt to guess the location of aux files from a given log file(s).
|
|
65
|
+
|
|
66
|
+
:param log_files: Output file(s) to parse or a directory of output files to parse.
|
|
67
|
+
"""
|
|
68
|
+
# This default implementation does nothing smart.
|
|
69
|
+
return self(*log_files, **kwargs)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def sort_log_files(self, log_files):
|
|
73
|
+
"""
|
|
74
|
+
Sort a list of log files into a particular order, if required for this parser.
|
|
75
|
+
"""
|
|
76
|
+
return log_files
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def log_file_path(self):
|
|
80
|
+
"""
|
|
81
|
+
The main log file.
|
|
82
|
+
"""
|
|
83
|
+
for log_file in self.log_file_paths:
|
|
84
|
+
if log_file.suffix.lower() == ".log":
|
|
85
|
+
return log_file
|
|
86
|
+
|
|
87
|
+
return self.log_file_paths[0]
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def name(self):
|
|
91
|
+
"""
|
|
92
|
+
Short name to describe this calculation result.
|
|
93
|
+
"""
|
|
94
|
+
return self.log_file_path.with_suffix("").name
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def description(self):
|
|
98
|
+
"""
|
|
99
|
+
A name/path that describes the file(s) being parsed, used for error messages etc.
|
|
100
|
+
"""
|
|
101
|
+
return self.log_file_path
|
|
102
|
+
|
|
103
|
+
def parse(self):
|
|
104
|
+
"""
|
|
105
|
+
Extract results from our output files.
|
|
106
|
+
"""
|
|
107
|
+
self._parse()
|
|
108
|
+
|
|
109
|
+
# Make any final adjustments.
|
|
110
|
+
self.post_parse()
|
|
111
|
+
|
|
112
|
+
def _parse(self):
|
|
113
|
+
"""
|
|
114
|
+
Extract results from our output files.
|
|
115
|
+
"""
|
|
116
|
+
raise NotImplementedError("Implement in subclass")
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_current_username(self):
|
|
120
|
+
"""
|
|
121
|
+
Get the username of the currently logged in user.
|
|
122
|
+
|
|
123
|
+
:returns: The username as a string, or None if the username could not be determined.
|
|
124
|
+
"""
|
|
125
|
+
# Based on https://stackoverflow.com/questions/842059/is-there-a-portable-way-to-get-the-current-username-in-python
|
|
126
|
+
try:
|
|
127
|
+
return pwd.getpwuid(os.getuid()).pw_name
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def post_parse(self):
|
|
133
|
+
"""
|
|
134
|
+
Perform any required operations after line-by-line parsing.
|
|
135
|
+
"""
|
|
136
|
+
# Add our name.
|
|
137
|
+
if self.name is not None:
|
|
138
|
+
self.data.metadata['name'] = self.name
|
|
139
|
+
|
|
140
|
+
# Add current username.
|
|
141
|
+
# TODO: It would probably be better if we used the name of the user who owns the output file, rather than the current user...
|
|
142
|
+
self.data.metadata['user'] = self.get_current_username()
|
|
143
|
+
|
|
144
|
+
# Set our file paths.
|
|
145
|
+
self.data.metadata['log_files'] = self.log_file_paths
|
|
146
|
+
self.data.metadata['auxiliary_files'] = self.auxiliary_files
|
|
147
|
+
|
|
148
|
+
def process_all(self, options):
|
|
149
|
+
"""
|
|
150
|
+
Get all the Result set objects produced by this parser.
|
|
151
|
+
|
|
152
|
+
:param options: A Digichem options nested dictionary containing options to control parsing.
|
|
153
|
+
:return: A list of the populated result sets.
|
|
154
|
+
"""
|
|
155
|
+
self.process(options)
|
|
156
|
+
return [self.results]
|
|
157
|
+
|
|
158
|
+
def process(self, options):
|
|
159
|
+
"""
|
|
160
|
+
Get a Result set object from this parser.
|
|
161
|
+
|
|
162
|
+
:param options: A Digichem options nested dictionary containing options to control parsing.
|
|
163
|
+
:return: The populated result set.
|
|
164
|
+
"""
|
|
165
|
+
self.options = options
|
|
166
|
+
# Get our result set.
|
|
167
|
+
self.results = Result_set(
|
|
168
|
+
_id = self.data._id,
|
|
169
|
+
metadata = Metadata.from_parser(self)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
alignment_class = Alignment.from_class_handle(options['alignment']) if options['alignment'] is not None else Minimal
|
|
173
|
+
|
|
174
|
+
# First get our list of MOs (because we need them for excited states too.)
|
|
175
|
+
self.results.orbitals = Molecular_orbital_list.from_parser(self)
|
|
176
|
+
self.results.beta_orbitals = Molecular_orbital_list.from_parser(self, cls = Beta_orbital)
|
|
177
|
+
|
|
178
|
+
# Assign total levels.
|
|
179
|
+
self.results.orbitals.assign_total_level(self.results.beta_orbitals)
|
|
180
|
+
|
|
181
|
+
# Our alignment orientation data.
|
|
182
|
+
self.results.atoms = alignment_class.from_parser(self)
|
|
183
|
+
self.results.raw_atoms = Atom_list.from_parser(self)
|
|
184
|
+
|
|
185
|
+
# TEDM and TMDM.
|
|
186
|
+
self.results.transition_dipole_moments = Transition_dipole_moment.from_parser(self)
|
|
187
|
+
|
|
188
|
+
# Excited states.
|
|
189
|
+
self.results.excited_states = Excited_state_list.from_parser(self)
|
|
190
|
+
|
|
191
|
+
# Energies.
|
|
192
|
+
self.results.energies = Energies.from_parser(self)
|
|
193
|
+
|
|
194
|
+
# Our ground state.
|
|
195
|
+
self.results.ground_state = Ground_state.from_parser(self)
|
|
196
|
+
|
|
197
|
+
# And a similar list but also including the ground.
|
|
198
|
+
self.results.energy_states = Excited_state_list()
|
|
199
|
+
self.results.energy_states.append(self.results.ground_state)
|
|
200
|
+
self.results.energy_states.extend(self.results.excited_states)
|
|
201
|
+
|
|
202
|
+
# SOC.
|
|
203
|
+
self.results.soc = SOC_list.from_parser(self)
|
|
204
|
+
|
|
205
|
+
# PDM
|
|
206
|
+
self.results.pdm = Dipole_moment.from_parser(self)
|
|
207
|
+
|
|
208
|
+
# Frequencies.
|
|
209
|
+
self.results.vibrations = Vibrations_list.from_parser(self)
|
|
210
|
+
|
|
211
|
+
# NMR.
|
|
212
|
+
self.results.nmr_shieldings = NMR_shielding.dict_from_parser(self)
|
|
213
|
+
self.results.nmr_couplings = NMR_spin_couplings_list.from_parser(self)
|
|
214
|
+
self.results.nmr = NMR_list.from_parser(self)
|
|
215
|
+
|
|
216
|
+
# Finally, try and set emission.
|
|
217
|
+
self.results.emission.vertical, self.results.emission.adiabatic = Relaxed_excited_state.guess_from_results(self.results)
|
|
218
|
+
|
|
219
|
+
# Return the populated result set for convenience.
|
|
220
|
+
return self.results
|
digichem/parse/cclib.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
import digichem.log
|
|
5
|
+
from digichem.parse.base import Parser_abc
|
|
6
|
+
|
|
7
|
+
# Hidden imports.
|
|
8
|
+
#import cclib.io
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Cclib_parser(Parser_abc):
|
|
12
|
+
"""
|
|
13
|
+
ABC for parsers that use cclib to do most of their work for them.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# A dictionary of recognised auxiliary file types.
|
|
17
|
+
INPUT_FILE_TYPES = {}
|
|
18
|
+
|
|
19
|
+
def __init__(self, *log_files, **auxiliary_files):
|
|
20
|
+
"""
|
|
21
|
+
Top level constructor for calculation parsers.
|
|
22
|
+
|
|
23
|
+
:param log_files: A list of output file to analyse/parse. The first log_file given will be used for naming purposes.
|
|
24
|
+
:param auxiliary_files: A dictionary of auxiliary input files related to the calculation.
|
|
25
|
+
"""
|
|
26
|
+
# Also save our aux files, stripping None.
|
|
27
|
+
self.auxiliary_files = {name: aux_file for name,aux_file in auxiliary_files.items() if aux_file is not None}
|
|
28
|
+
|
|
29
|
+
super().__init__(*log_files)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_logs(self, *log_files, **kwargs):
|
|
33
|
+
"""
|
|
34
|
+
Intelligent constructor that will attempt to guess the location of files from a given log file(s).
|
|
35
|
+
|
|
36
|
+
:param given_log_files: Output file(s) to parse or a directory of output files to parse.
|
|
37
|
+
"""
|
|
38
|
+
# Have a look for aux. files.
|
|
39
|
+
auxiliary_files = {}
|
|
40
|
+
|
|
41
|
+
for found_log_file in log_files:
|
|
42
|
+
auxiliary_files.update(self.find_auxiliary_files(found_log_file))
|
|
43
|
+
|
|
44
|
+
# Finally, update our auxiliary_files with kwargs, so any user specified aux files take precedence.
|
|
45
|
+
auxiliary_files.update(kwargs)
|
|
46
|
+
|
|
47
|
+
return self(*log_files, **auxiliary_files)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def find_auxiliary_files(self, hint):
|
|
51
|
+
"""
|
|
52
|
+
Find auxiliary files from a given hint.
|
|
53
|
+
|
|
54
|
+
:param hint: A path to a file to use as a hint to find additional files.
|
|
55
|
+
:returns: A dictionary of found aux files.
|
|
56
|
+
"""
|
|
57
|
+
hint = Path(hint)
|
|
58
|
+
|
|
59
|
+
# Now have a look for aux. input files, which are defined by each parser's INPUT_FILE_TYPES
|
|
60
|
+
auxiliary_files = {}
|
|
61
|
+
for file_type in self.INPUT_FILE_TYPES:
|
|
62
|
+
for extension in file_type.extensions:
|
|
63
|
+
if hint.with_suffix(extension).exists():
|
|
64
|
+
auxiliary_files[self.INPUT_FILE_TYPES[file_type]] = hint.with_suffix(extension)
|
|
65
|
+
|
|
66
|
+
return auxiliary_files
|
|
67
|
+
|
|
68
|
+
def _parse(self):
|
|
69
|
+
"""
|
|
70
|
+
Extract results from our output files.
|
|
71
|
+
|
|
72
|
+
This default implementation will parse the log file with cclib and save the result to self.data.
|
|
73
|
+
"""
|
|
74
|
+
import cclib.io
|
|
75
|
+
|
|
76
|
+
# We start by using cclib to get most of the data we need.
|
|
77
|
+
|
|
78
|
+
# Output a message (because this is slow).
|
|
79
|
+
digichem.log.get_logger().info("Parsing calculation result '{}'".format(self.description))
|
|
80
|
+
|
|
81
|
+
# Use cclib to open our log files.
|
|
82
|
+
# ccread will accept a list of log files to read, but will sometimes choke if the list contains only one entry,
|
|
83
|
+
# in which case we give it only one file name.
|
|
84
|
+
# ccread will also choke if we give it pathlib Paths.
|
|
85
|
+
file_paths = [str(log_file) for log_file in self.log_file_paths]
|
|
86
|
+
|
|
87
|
+
# Get data from cclib.
|
|
88
|
+
self.data = cclib.io.ccread(file_paths if len(file_paths) > 1 else file_paths[0])
|
|
89
|
+
|
|
90
|
+
# Get a unique ID (checksum) from the given log files.
|
|
91
|
+
# First, order the list of filenames so we also process in the same order.
|
|
92
|
+
# We do this because not all parsers define a custom sort.
|
|
93
|
+
|
|
94
|
+
file_paths.sort()
|
|
95
|
+
hasher = hashlib.sha1()
|
|
96
|
+
|
|
97
|
+
for file_path in file_paths:
|
|
98
|
+
hasher.update(Path(file_path).read_bytes())
|
|
99
|
+
|
|
100
|
+
self.data._id = hasher.hexdigest()
|
|
101
|
+
|
|
102
|
+
# Do some setup.
|
|
103
|
+
self.pre_parse()
|
|
104
|
+
|
|
105
|
+
# Do our own parsing (if any).
|
|
106
|
+
for log_file_path in self.log_file_paths:
|
|
107
|
+
with open(log_file_path, "rt") as log_file:
|
|
108
|
+
for line in log_file:
|
|
109
|
+
self.parse_output_line(log_file, line)
|
|
110
|
+
|
|
111
|
+
def parse_output_line(self, log_file, line):
|
|
112
|
+
"""
|
|
113
|
+
Perform custom line-by-line parsing of an output file.
|
|
114
|
+
|
|
115
|
+
This method will be called for each line of each log-file given to the parser (although be aware that some implementations may skip some lines during parsing),
|
|
116
|
+
and it allows for data not supported by cclib to be extracted. It is program specific.
|
|
117
|
+
"""
|
|
118
|
+
# Do nothing.
|
|
119
|
+
|
|
120
|
+
def pre_parse(self):
|
|
121
|
+
"""
|
|
122
|
+
Perform any setup before line-by-line parsing.
|
|
123
|
+
"""
|
|
124
|
+
# Do nothing.
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def au_to_debye(self, au):
|
|
128
|
+
"""
|
|
129
|
+
Convert a dipole moment in au to debye.
|
|
130
|
+
"""
|
|
131
|
+
return au * 2.541746473
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def bohr_to_angstrom(self, bohr_distance):
|
|
135
|
+
"""
|
|
136
|
+
Convert a length in bohr to angstrom.
|
|
137
|
+
"""
|
|
138
|
+
return bohr_distance * 0.529177210903
|
digichem/parse/dump.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Parser(s) for reading from dumped digichem results sets"""
|
|
2
|
+
import yaml
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from digichem.parse.base import Parser_abc
|
|
6
|
+
from digichem.result.tdm import Transition_dipole_moment
|
|
7
|
+
from digichem.result.result import Result_set
|
|
8
|
+
from digichem.result.metadata import Metadata
|
|
9
|
+
from digichem.result.orbital import Molecular_orbital_list
|
|
10
|
+
from digichem.result.atom import Atom_list
|
|
11
|
+
from digichem.result.excited_state import Excited_state_list
|
|
12
|
+
from digichem.result.energy import Energies
|
|
13
|
+
from digichem.result.ground_state import Ground_state
|
|
14
|
+
from digichem.result.soc import SOC_list
|
|
15
|
+
from digichem.result.dipole_moment import Dipole_moment
|
|
16
|
+
from digichem.result.vibration import Vibrations_list
|
|
17
|
+
from digichem.result.emission import Relaxed_excited_state
|
|
18
|
+
import digichem.log
|
|
19
|
+
from digichem.result.alignment.base import Alignment, Minimal
|
|
20
|
+
from digichem.result.nmr import NMR_list
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Dump_multi_parser_abc(Parser_abc):
|
|
24
|
+
"""
|
|
25
|
+
ABC for classes that can read multiple result sets from dumped data.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, input_file, *other_log_files, raw_data = None, **auxiliary_files):
|
|
29
|
+
"""
|
|
30
|
+
Top level constructor for calculation parsers.
|
|
31
|
+
|
|
32
|
+
:param input_file: The path to read from.
|
|
33
|
+
"""
|
|
34
|
+
# Neither other_log_files nor auxiliary_files are currently used...
|
|
35
|
+
super().__init__(input_file, raw_data = raw_data)
|
|
36
|
+
self.all_results = []
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_data(self, input_file, data):
|
|
40
|
+
return self(input_file, raw_data = data)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def results(self):
|
|
44
|
+
return self.all_results[0]
|
|
45
|
+
|
|
46
|
+
@results.setter
|
|
47
|
+
def results(self, value):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def post_parse(self):
|
|
51
|
+
"""
|
|
52
|
+
Perform any required operations after line-by-line parsing.
|
|
53
|
+
"""
|
|
54
|
+
for result in self.data:
|
|
55
|
+
# Add current username.
|
|
56
|
+
# TODO: It would probably be better if we used the name of the user who owns the output file, rather than the current user...
|
|
57
|
+
result['metadata']['user'] = self.get_current_username()
|
|
58
|
+
|
|
59
|
+
# Set our file paths.
|
|
60
|
+
result['metadata']['log_files'] = self.log_file_paths
|
|
61
|
+
result['metadata']['auxiliary_files'] = {}
|
|
62
|
+
|
|
63
|
+
def get_sub_parser(self):
|
|
64
|
+
raise NotImplementedError("Implement in subclass")
|
|
65
|
+
|
|
66
|
+
def process_all(self, options):
|
|
67
|
+
"""
|
|
68
|
+
Get all the Result set objects produced by this parser.
|
|
69
|
+
|
|
70
|
+
:param options: A Digichem options nested dictionary containing options to control parsing.
|
|
71
|
+
:return: A list of the populated result sets.
|
|
72
|
+
"""
|
|
73
|
+
# Unlike most other parsers, our data can actually contain lots of results.
|
|
74
|
+
self.all_results = [self.get_sub_parser()(self.log_file_path, raw_data = data).process(options) for data in self.data]
|
|
75
|
+
|
|
76
|
+
return self.all_results
|
|
77
|
+
|
|
78
|
+
def process(self, options):
|
|
79
|
+
"""
|
|
80
|
+
Get a Result set object from this parser.
|
|
81
|
+
|
|
82
|
+
:param options: A Digichem options nested dictionary containing options to control parsing.
|
|
83
|
+
:return: The populated result set.
|
|
84
|
+
"""
|
|
85
|
+
self.process_all(options)
|
|
86
|
+
return self.results
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Yaml_multi_parser(Dump_multi_parser_abc):
|
|
90
|
+
"""
|
|
91
|
+
Parser for reading from dumped result sets in yaml format.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def get_sub_parser(self):
|
|
95
|
+
return Yaml_parser
|
|
96
|
+
|
|
97
|
+
def _parse(self):
|
|
98
|
+
"""
|
|
99
|
+
Extract results from our output files.
|
|
100
|
+
"""
|
|
101
|
+
# Read that file.
|
|
102
|
+
digichem.log.get_logger().info("Parsing calculation result '{}'".format(self.description))
|
|
103
|
+
with open(self.log_file_path, "rt") as yaml_file:
|
|
104
|
+
self.data = list(yaml.safe_load_all(yaml_file))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Json_multi_parser(Dump_multi_parser_abc):
|
|
108
|
+
"""
|
|
109
|
+
Parser for reading from dumped result sets in JSON format.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def get_sub_parser(self):
|
|
113
|
+
return Json_parser
|
|
114
|
+
|
|
115
|
+
def _parse(self):
|
|
116
|
+
"""
|
|
117
|
+
Extract results from our output files.
|
|
118
|
+
"""
|
|
119
|
+
# Read that file.
|
|
120
|
+
digichem.log.get_logger().info("Parsing calculation result '{}'".format(self.description))
|
|
121
|
+
with open(self.log_file_path, "rt") as json_file:
|
|
122
|
+
self.data = json.load(json_file)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Dump_parser_abc(Parser_abc):
|
|
126
|
+
"""
|
|
127
|
+
ABC for parsers that read dumped data.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, input_file, *other_log_files, raw_data = None, **auxiliary_files):
|
|
131
|
+
"""
|
|
132
|
+
Top level constructor for calculation parsers.
|
|
133
|
+
|
|
134
|
+
:param input_file: The path to the input file to read.
|
|
135
|
+
"""
|
|
136
|
+
# Neither other_log_files nor auxiliary_files are currently used...
|
|
137
|
+
super().__init__(input_file, raw_data = raw_data)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_data(self, input_file, data):
|
|
141
|
+
return self(input_file, raw_data = data)
|
|
142
|
+
|
|
143
|
+
def process_all(self, options):
|
|
144
|
+
"""
|
|
145
|
+
Get all the Result set objects produced by this parser.
|
|
146
|
+
|
|
147
|
+
:param options: A Digichem options nested dictionary containing options to control parsing.
|
|
148
|
+
:return: A list of the populated result sets.
|
|
149
|
+
"""
|
|
150
|
+
self.process(options)
|
|
151
|
+
return [self.results]
|
|
152
|
+
|
|
153
|
+
def process(self, options):
|
|
154
|
+
"""
|
|
155
|
+
Get a Result set object from this parser.
|
|
156
|
+
|
|
157
|
+
:param options: A Digichem options nested dictionary containing options to control parsing.
|
|
158
|
+
:return: The populated result set.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# Get our result set.
|
|
162
|
+
self.results = Result_set(
|
|
163
|
+
_id = self.data.get("_id"),
|
|
164
|
+
metadata = Metadata.from_dump(self.data['metadata'], self.results, options)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# First get our list of MOs (because we need them for excited states too.)
|
|
168
|
+
self.results.orbitals = Molecular_orbital_list.from_dump(self.data['orbitals'], self.results, options)
|
|
169
|
+
self.results.beta_orbitals = Molecular_orbital_list.from_dump(self.data['beta_orbitals'], self.results, options)
|
|
170
|
+
|
|
171
|
+
alignment_class = Alignment.from_class_handle(options['alignment']) if options['alignment'] is not None else Minimal
|
|
172
|
+
|
|
173
|
+
# Our alignment orientation data.
|
|
174
|
+
# The constructor for each alignment class automatically performs realignment.
|
|
175
|
+
self.results.atoms = alignment_class.from_dump(self.data['atoms'], self.results, options)
|
|
176
|
+
self.results.raw_atoms = Atom_list.from_dump(self.data['raw_atoms'], self.results, options)
|
|
177
|
+
|
|
178
|
+
# TEDM and TMDM.
|
|
179
|
+
self.results.transition_dipole_moments = Transition_dipole_moment.list_from_dump(self.data['excited_states']['values'], self.results, options)
|
|
180
|
+
|
|
181
|
+
# Excited states.
|
|
182
|
+
self.results.excited_states = Excited_state_list.from_dump(self.data['excited_states'], self.results, options)
|
|
183
|
+
|
|
184
|
+
# Energies.
|
|
185
|
+
self.results.energies = Energies.from_dump(self.data['energies'], self.results, options)
|
|
186
|
+
|
|
187
|
+
# Our ground state.
|
|
188
|
+
self.results.ground_state = Ground_state.from_dump(self.data['ground_state'], self.results, options)
|
|
189
|
+
|
|
190
|
+
# And a similar list but also including the ground.
|
|
191
|
+
self.results.energy_states = Excited_state_list()
|
|
192
|
+
self.results.energy_states.append(self.results.ground_state)
|
|
193
|
+
self.results.energy_states.extend(self.results.excited_states)
|
|
194
|
+
|
|
195
|
+
# SOC.
|
|
196
|
+
self.results.soc = SOC_list.from_dump(self.data['soc'], self.results, options)
|
|
197
|
+
|
|
198
|
+
# PDM
|
|
199
|
+
self.results.pdm = Dipole_moment.from_dump(self.data['pdm'], self.results, options)
|
|
200
|
+
|
|
201
|
+
# Frequencies.
|
|
202
|
+
self.results.vibrations = Vibrations_list.from_dump(self.data['vibrations'], self.results, options)
|
|
203
|
+
|
|
204
|
+
# NMR.
|
|
205
|
+
if "nmr" in self.data:
|
|
206
|
+
self.results.nmr = NMR_list.from_dump(self.data['nmr'], self.results, options)
|
|
207
|
+
|
|
208
|
+
else:
|
|
209
|
+
self.results.nmr = NMR_list(atoms = self.results.atoms, options = options)
|
|
210
|
+
|
|
211
|
+
# Finally, try and set emission.
|
|
212
|
+
try:
|
|
213
|
+
self.results.emission = self.results.emission.from_dump(self.data['emission'], self.results, options)
|
|
214
|
+
|
|
215
|
+
except Exception:
|
|
216
|
+
digichem.log.get_logger().warning("Failed to parse emission data", exc_info = True)
|
|
217
|
+
|
|
218
|
+
# If we don't have explicit emission set, try and guess.
|
|
219
|
+
#if len(self.results.emission.vertical) == 0 and len(self.results.emission.adiabatic) == 0:
|
|
220
|
+
# self.results.emission.vertical, self.results.emission.adiabatic = Relaxed_excited_state.guess_from_results(self.results)
|
|
221
|
+
|
|
222
|
+
# Return the populated result set for convenience.
|
|
223
|
+
return self.results
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class Yaml_parser(Dump_parser_abc):
|
|
227
|
+
"""
|
|
228
|
+
Parser for reading from dumped result sets in yaml format.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def _parse(self):
|
|
232
|
+
"""
|
|
233
|
+
Extract results from our output files.
|
|
234
|
+
"""
|
|
235
|
+
# Read that file.
|
|
236
|
+
digichem.log.get_logger().info("Parsing calculation result '{}'".format(self.description))
|
|
237
|
+
with open(self.log_file_paths[0], "rt") as yaml_file:
|
|
238
|
+
self.data = yaml.safe_load(yaml_file)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Json_parser(Dump_parser_abc):
|
|
242
|
+
"""
|
|
243
|
+
Parser for reading from TinyDB json files.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def _parse(self):
|
|
247
|
+
"""
|
|
248
|
+
Extract results from our output files.
|
|
249
|
+
"""
|
|
250
|
+
raise NotImplementedError("Databases contain many results so it does not make sense to return only one. Try Tinydb_multi_parser instead.")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|