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
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
@@ -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
+