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/file/base.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# General imports.
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from timeit import default_timer as timer
|
|
4
|
+
import datetime
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from digichem import misc
|
|
8
|
+
from digichem.exception.base import File_maker_exception, Digichem_exception
|
|
9
|
+
|
|
10
|
+
# Digichem imports.
|
|
11
|
+
import digichem.log
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class File_maker_ABC():
|
|
15
|
+
"""
|
|
16
|
+
ABC for file maker classes.
|
|
17
|
+
|
|
18
|
+
These classes make file(s) on the fly, they are mostly used for image rendering (and making the prerequisite files for image rendering).
|
|
19
|
+
|
|
20
|
+
Do not try to init an instance of this class yourself; use a real one below (or elsewhere).
|
|
21
|
+
"""
|
|
22
|
+
#####################################
|
|
23
|
+
# To be implemented in sub classes. #
|
|
24
|
+
#####################################
|
|
25
|
+
|
|
26
|
+
def __init__(self, output):
|
|
27
|
+
"""
|
|
28
|
+
:param output: The filename/path to the new file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
29
|
+
"""
|
|
30
|
+
self.output = output
|
|
31
|
+
self.file_path = {'file': self.output}
|
|
32
|
+
|
|
33
|
+
def get_file(self, name = 'file'):
|
|
34
|
+
"""
|
|
35
|
+
Get the path to one of the files that this class represents, converting from our input file and writing to file first if necessary.
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError("get_file() is not implemented in this ABC")
|
|
38
|
+
|
|
39
|
+
def delete(self, lazy = False):
|
|
40
|
+
"""
|
|
41
|
+
Delete the file(s) described by this object.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError("delete() is not implemented in this ABC")
|
|
44
|
+
|
|
45
|
+
#################################
|
|
46
|
+
# Implemented in this sub-class #
|
|
47
|
+
#################################
|
|
48
|
+
|
|
49
|
+
def safe_get_file(self, name = 'file'):
|
|
50
|
+
"""
|
|
51
|
+
Get the path to one of the files that this class represents, converting from our input file and writing to file first if necessary.
|
|
52
|
+
|
|
53
|
+
The functioning of this method is controlled by the dont_modify & use_existing flags.
|
|
54
|
+
|
|
55
|
+
You can also use the normal python attribute mechanism (either through getattr() or dot notation) to get these paths.
|
|
56
|
+
|
|
57
|
+
:raises KeyError: If name is not the name of one of the files this class represents.
|
|
58
|
+
:param name: The name of a file to get. Depends on the paths in self.file_path.
|
|
59
|
+
:return: A pathlib Path object pointing to the file represented by name, or None if no file could be created.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
return self.get_file(name)
|
|
63
|
+
|
|
64
|
+
except Exception:
|
|
65
|
+
digichem.log.get_logger().error("Unable to get file '{}'".format(self.file_path[name]), exc_info = True)
|
|
66
|
+
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def relative_path(self, file_name = 'file', *, output_base):
|
|
70
|
+
"""
|
|
71
|
+
Get the relative path to the file (or one of the files) represented by this object.
|
|
72
|
+
|
|
73
|
+
The path is relative to the directory where we write our file, called the output_base.
|
|
74
|
+
|
|
75
|
+
:param file_name: The name of one of the files that we represent to get the path of, this can be excluded if we only represent one file.
|
|
76
|
+
:param output_base: Pathlib Path to the base directory to get a relative path to.
|
|
77
|
+
:return: A relative Path object to the file.
|
|
78
|
+
"""
|
|
79
|
+
# First get the file's real path (this could trigger rendering).
|
|
80
|
+
file_path = self.safe_get_file(file_name)
|
|
81
|
+
|
|
82
|
+
# Return None if that doesn't work (eg, because dont_modify = True and use_existing = False) to match the rest of the class.
|
|
83
|
+
if file_path is None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# And return a relative path (this can still raise exceptions).
|
|
87
|
+
return file_path.relative_to(output_base)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return str(self.safe_get_file())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Dummy_file_maker(File_maker_ABC):
|
|
95
|
+
"""
|
|
96
|
+
A dummy file maker that will always fail to make any files.
|
|
97
|
+
|
|
98
|
+
This class is useful where it is not possible to construct a real file maker object, where instead an instance of this class can be seamlessly used instead.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, output, message):
|
|
102
|
+
"""
|
|
103
|
+
:param output: The path to the file this class is not creating.
|
|
104
|
+
:param message: The reason why this dummy file maker is being used.
|
|
105
|
+
"""
|
|
106
|
+
super().__init__(output)
|
|
107
|
+
self.message = message
|
|
108
|
+
|
|
109
|
+
def get_file(self, name = 'file'):
|
|
110
|
+
"""
|
|
111
|
+
Get the path to one of the files that this class represents, converting from our input file and writing to file first if necessary.
|
|
112
|
+
"""
|
|
113
|
+
raise File_maker_exception(self, self.message)
|
|
114
|
+
|
|
115
|
+
def delete(self, lazy = False):
|
|
116
|
+
"""
|
|
117
|
+
Delete the file(s) described by this object.
|
|
118
|
+
"""
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class File_maker(File_maker_ABC):
|
|
123
|
+
"""
|
|
124
|
+
Superclass for classes that automatically create files when needed.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# Text description of our output file type, used for error messages etc. This can be changed by inheriting classes.
|
|
128
|
+
output_file_type = "output"
|
|
129
|
+
|
|
130
|
+
def __init__(self, output, existing_file = None, dont_modify = False, use_existing = False, full_path_names = False):
|
|
131
|
+
"""
|
|
132
|
+
Constructor for File_converter objects.
|
|
133
|
+
|
|
134
|
+
:param output: The filename/path to the new file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
135
|
+
:param existing_file: An optional existing file of the type we're converting to. If this is given, then no conversion is done. This option exists so the user can specify files they have already converted themselves.
|
|
136
|
+
:param dont_modify: Flag that prevents modifying the file on disk. If True, no new file will be written even if one does not already exist. dont_modify is automatically set to True if existing_file is not None (to prevent over-writing whatever file the user gave us).
|
|
137
|
+
:param use_existing: Flag that modifies how file conversion works. If True, existing files will be preferentially used if available (set to False to force overwriting existing files).
|
|
138
|
+
:param full_path_names: Whether to print full path names in log messages.
|
|
139
|
+
"""
|
|
140
|
+
# If we've been given an existing_file explicitly, check it exists.
|
|
141
|
+
if existing_file is not None:
|
|
142
|
+
# Convert to path.
|
|
143
|
+
existing_file = Path(existing_file)
|
|
144
|
+
|
|
145
|
+
# Force set dont_modify = True so we don't overwrite the input that the user has given us.
|
|
146
|
+
dont_modify = True
|
|
147
|
+
digichem.log.get_logger().debug("Setting dont_modify == True to prevent overwrite of user specified {} file '{}'".format(self.output_file_type, existing_file))
|
|
148
|
+
|
|
149
|
+
# Check
|
|
150
|
+
if not existing_file.exists():
|
|
151
|
+
warnings.warn("The given {} file '{}' does not appear to exist".format(self.output_file_type, existing_file))
|
|
152
|
+
# Continue anyway.
|
|
153
|
+
|
|
154
|
+
# Set our output appropriately, and continue as normal.
|
|
155
|
+
output = existing_file
|
|
156
|
+
else:
|
|
157
|
+
# Convert to path.
|
|
158
|
+
output = Path(output)
|
|
159
|
+
|
|
160
|
+
self.dont_modify = dont_modify
|
|
161
|
+
self.use_existing = use_existing
|
|
162
|
+
|
|
163
|
+
# A flag of whether we have already created our output file.
|
|
164
|
+
self.done_file_creation = False
|
|
165
|
+
|
|
166
|
+
self.failed_file_creation = False
|
|
167
|
+
|
|
168
|
+
self.full_path_names = full_path_names
|
|
169
|
+
|
|
170
|
+
super().__init__(output)
|
|
171
|
+
|
|
172
|
+
def get_file(self, name = 'file'):
|
|
173
|
+
"""
|
|
174
|
+
Get the path to one of the files that this class represents, converting from our input file and writing to file first if necessary.
|
|
175
|
+
|
|
176
|
+
The functioning of this method is controlled by the dont_modify & use_existing flags.
|
|
177
|
+
|
|
178
|
+
:raises KeyError: If name is not the name of one of the files this class represents.
|
|
179
|
+
:raises File_maker_exception: If the file could not be created for some reason.
|
|
180
|
+
:param name: The name of a file to get. Depends on the paths in self.file_path.
|
|
181
|
+
:return: A pathlib Path object pointing to the file represented by name.
|
|
182
|
+
"""
|
|
183
|
+
# We first need to see if we've already made our files or not.
|
|
184
|
+
# Although we can actually represent multiple files, they are all created at the same time.
|
|
185
|
+
if self.done_file_creation:
|
|
186
|
+
return self.file_path[name]
|
|
187
|
+
else:
|
|
188
|
+
# Our files haven't been created. What happens next depends on the options given to us at __init__().
|
|
189
|
+
if self.use_existing:
|
|
190
|
+
# We've been asked to use existing files if they exist, see if they do.
|
|
191
|
+
# There is obviously a race condition here, but we're not making any guarantee to the calling function that the file really exists (or even what it is), we're only checking to see if we need to create a new file or not.
|
|
192
|
+
if self.file_path[name].exists():
|
|
193
|
+
# File exists, return its path.
|
|
194
|
+
#digichem.log.get_logger().debug("Using existing {} file '{}'".format(self.output_file_type, self.file_path[name]))
|
|
195
|
+
return self.file_path[name]
|
|
196
|
+
|
|
197
|
+
# There are no files we can use. If we're allowed, write new files.
|
|
198
|
+
if not self.dont_modify:
|
|
199
|
+
# Try and make.
|
|
200
|
+
try:
|
|
201
|
+
self.make()
|
|
202
|
+
return self.file_path[name]
|
|
203
|
+
|
|
204
|
+
except File_maker_exception as e:
|
|
205
|
+
# Don't raise a new exception if we've already got one as this just pollutes the log.
|
|
206
|
+
self.failed_file_creation = True
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
# Know that a KeyError could be thrown here if name is not valid, but I think we'd want that to propagate anyway.
|
|
211
|
+
self.failed_file_creation = True
|
|
212
|
+
raise File_maker_exception(self, "Unable to create file") from e
|
|
213
|
+
else:
|
|
214
|
+
raise File_maker_exception(self, "Not creating file because dont_modify is True")
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def creation_message(self):
|
|
218
|
+
"""
|
|
219
|
+
A short message that may (depending on log-level) be printed to the user before make_files() is called.
|
|
220
|
+
"""
|
|
221
|
+
return "Generating {} file '{}'".format(self.output_file_type, self.output if self.full_path_names else self.output.name)
|
|
222
|
+
|
|
223
|
+
def check_can_make(self):
|
|
224
|
+
"""
|
|
225
|
+
Check whether it is feasible to try and create the files(s) that we represent.
|
|
226
|
+
|
|
227
|
+
Reasons for making not being possible are varied and are up to the inheriting class, but include eg, a required input (cube, fchk) file not being given.
|
|
228
|
+
|
|
229
|
+
This method returns nothing, but will raise an File_maker_exception exception if the rendering is not possible.
|
|
230
|
+
"""
|
|
231
|
+
if self.failed_file_creation:
|
|
232
|
+
# We've already tried to make this file once before and failed, don't try again.
|
|
233
|
+
raise File_maker_exception(self, "Not creating file due to previous errors")
|
|
234
|
+
|
|
235
|
+
def make(self):
|
|
236
|
+
"""
|
|
237
|
+
Make the file(s) described by this object.
|
|
238
|
+
|
|
239
|
+
This method is a wrapper, used to call other methods that should be written by the user.
|
|
240
|
+
|
|
241
|
+
Most inheriting classes won't need to modify this.
|
|
242
|
+
|
|
243
|
+
This method should not be called directly, and doing so can overwrite files that we've been asked not to overwrite.
|
|
244
|
+
"""
|
|
245
|
+
# First check if we can render. This method will throw an exception to stop us if we can't proceed.
|
|
246
|
+
self.check_can_make()
|
|
247
|
+
|
|
248
|
+
# Print a debug message (because lots can go wrong next and this step an be quite slow).
|
|
249
|
+
digichem.log.get_logger().info(self.creation_message)
|
|
250
|
+
|
|
251
|
+
start_timer = timer()
|
|
252
|
+
|
|
253
|
+
# Make our parent folder(s).
|
|
254
|
+
try:
|
|
255
|
+
self.output.parent.mkdir(parents = True)
|
|
256
|
+
except FileExistsError:
|
|
257
|
+
# This will happen when the dir already exists, which is fine.
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Call the real workhorse, which is implementation specific.
|
|
262
|
+
self.make_files()
|
|
263
|
+
|
|
264
|
+
# Set our flag.
|
|
265
|
+
self.done_file_creation = True
|
|
266
|
+
|
|
267
|
+
# Print timing info.
|
|
268
|
+
self.duration = datetime.timedelta(seconds = timer() - start_timer)
|
|
269
|
+
digichem.log.get_logger().info("File generation duration: {} ({} total seconds)".format(misc.timedelta_to_string(self.duration), self.duration.total_seconds()))
|
|
270
|
+
|
|
271
|
+
def delete(self, lazy = False):
|
|
272
|
+
"""
|
|
273
|
+
Delete the file(s) described by this object.
|
|
274
|
+
|
|
275
|
+
This method will try to delete all files represented by this object, even if exceptions are raised during deletion of an earlier file.
|
|
276
|
+
If lazy is not True, note that it is the last caught exception (from the last file deletion that went wrong) that is re-raised.
|
|
277
|
+
|
|
278
|
+
:param lazy: If true, no exceptions are raised if file deletion is not possible.
|
|
279
|
+
:return: True if all files were deleted successfully, False otherwise.
|
|
280
|
+
"""
|
|
281
|
+
# We'll keep hold of any exceptions for later.
|
|
282
|
+
exc = None
|
|
283
|
+
|
|
284
|
+
# Delete each of our files.
|
|
285
|
+
for file_name, file_path in self.file_path.items():
|
|
286
|
+
try:
|
|
287
|
+
# First check we're allowed to delete the file.
|
|
288
|
+
if self.dont_modify:
|
|
289
|
+
raise Digichem_exception("Unable to delete file '{}'; dont_modify is True".format(file_path))
|
|
290
|
+
|
|
291
|
+
# DELETE.
|
|
292
|
+
file_path.unlink()
|
|
293
|
+
|
|
294
|
+
# Unset the done_file_creation flag, so we'll re-write any files if they are called for again.
|
|
295
|
+
self.done_file_creation = False
|
|
296
|
+
except Exception as e:
|
|
297
|
+
# Hold onto our exception. This might discard a previous exception, but we can only raise one exception anyway (?).
|
|
298
|
+
exc = e
|
|
299
|
+
|
|
300
|
+
# If we've been asked to report exceptions, do so.
|
|
301
|
+
if not lazy and exc is not None:
|
|
302
|
+
raise exc
|
|
303
|
+
|
|
304
|
+
# Also return whether anything bad happened.
|
|
305
|
+
return exc is None
|
|
306
|
+
|
|
307
|
+
def make_files(self):
|
|
308
|
+
"""
|
|
309
|
+
Make the files(s) described by this object. Inheriting classes should write their own implementation.
|
|
310
|
+
"""
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class File_converter(File_maker):
|
|
315
|
+
"""
|
|
316
|
+
Superclass for classes that automatically convert between different file formats.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
# Text description of our input file type, used for error messages etc. This can be changed by inheriting classes.
|
|
320
|
+
input_file_type = "input"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def __init__(self, output, input_file = None, existing_file = None, dont_modify = False, use_existing = True):
|
|
324
|
+
"""
|
|
325
|
+
Constructor for File_converter objects.
|
|
326
|
+
|
|
327
|
+
:param output: The filename/path to the new file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
328
|
+
:param input_file: The input file that will be converted. Not that while this option is formally optional, conversion will normally fail if no input is available.
|
|
329
|
+
:param existing_file: An optional existing file of the type we're converting to. If this is given, then no conversion is done. This option exists so the user can specify files they have already converted themselves.
|
|
330
|
+
:param dont_modify: Flag that modifies how file conversion works. If True, no new file will be written.
|
|
331
|
+
:param use_existing: Flag that modifies how file conversion works. If True, no new file will be written. If True, existing files will be preferentially used if available (set to False to force overwriting existing files).
|
|
332
|
+
"""
|
|
333
|
+
# Call our parent.
|
|
334
|
+
super().__init__(output, existing_file, dont_modify, use_existing)
|
|
335
|
+
|
|
336
|
+
# And save our input file.
|
|
337
|
+
self.input_file = Path(input_file) if isinstance(input_file, str) else input_file
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def check_can_make(self):
|
|
341
|
+
"""
|
|
342
|
+
Check whether it is feasible to try and create the files(s) that we represent.
|
|
343
|
+
|
|
344
|
+
Reasons for making not being possible are varied and are up to the inheriting class, but include eg, a required input (cube, fchk) file not being given.
|
|
345
|
+
|
|
346
|
+
This method returns nothing, but will raise an File_maker_exception exception if the rendering is not possible.
|
|
347
|
+
"""
|
|
348
|
+
super().check_can_make()
|
|
349
|
+
|
|
350
|
+
if self.input_file is None:
|
|
351
|
+
raise File_maker_exception(self, "No {} file is available".format(self.input_file_type))
|
|
352
|
+
try:
|
|
353
|
+
if self.input_file.safe_get_file() is None:
|
|
354
|
+
raise File_maker_exception(self, "No {} file is available".format(self.input_file_type))
|
|
355
|
+
except AttributeError:
|
|
356
|
+
# Input file does not have a safe_get_file() method.
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def creation_message(self):
|
|
361
|
+
"""
|
|
362
|
+
A short message that may (depending on log-level) be printed to the user before make_files() is called.
|
|
363
|
+
"""
|
|
364
|
+
return "Converting {} file '{}' to {} file '{}'".format(self.input_file_type, self.input_file if self.full_path_names else Path(str(self.input_file)).name, self.output_file_type, self.output if self.full_path_names else Path(str(self.output)).name)
|
digichem/file/cube.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Objects for making cube files.
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from digichem.exception.base import File_maker_exception
|
|
10
|
+
|
|
11
|
+
from digichem.file import File_converter
|
|
12
|
+
import digichem.file.types as file_types
|
|
13
|
+
import digichem.log
|
|
14
|
+
from digichem.file.base import File_maker, Dummy_file_maker
|
|
15
|
+
from digichem.memory import Memory
|
|
16
|
+
from digichem.misc.io import expand_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sanitize_modern_cubes(cube_file_path):
|
|
20
|
+
"""
|
|
21
|
+
'Sanitize' or 'fix' modern cube files to be compatible with older parsers.
|
|
22
|
+
|
|
23
|
+
Modern cubes can store more than one density per coordinate, but many programs don't like this.
|
|
24
|
+
|
|
25
|
+
:param cube_file: Path to the cube file to modify.
|
|
26
|
+
"""
|
|
27
|
+
cube_file_path = Path(cube_file_path)
|
|
28
|
+
with open(cube_file_path, "rt") as old_cube_file, \
|
|
29
|
+
tempfile.NamedTemporaryFile("wt", dir = cube_file_path.parent, delete = False) as temp_cube:
|
|
30
|
+
for line_index, line in enumerate(old_cube_file):
|
|
31
|
+
# Line 3 (index 2) contains this header:
|
|
32
|
+
# -11 -10.884822 -13.392441 -10.884822 1
|
|
33
|
+
# The final '1' is unsupported on older software.
|
|
34
|
+
if line_index == 2:
|
|
35
|
+
line_split = line.split()
|
|
36
|
+
if len(line_split) == 5:
|
|
37
|
+
# We have a line that needs cleaning.
|
|
38
|
+
if line_split[-1] != "1":
|
|
39
|
+
digichem.log.get_logger().warning(
|
|
40
|
+
"Cube file {} contains more than 1 density ('{}' densities found), this file may no longer be fully compliant".format(
|
|
41
|
+
cube_file_path,
|
|
42
|
+
line_split[-1]
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
line = " " + " ".join(line_split[:-1]) + "\n"
|
|
47
|
+
|
|
48
|
+
temp_cube.write(line)
|
|
49
|
+
|
|
50
|
+
# Replace old cube with new cube.
|
|
51
|
+
os.rename(temp_cube.name, cube_file_path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Fchk_to_cube(File_converter):
|
|
55
|
+
"""
|
|
56
|
+
Class for handling file references to Gaussian cube files.
|
|
57
|
+
|
|
58
|
+
We currently rely on Gaussian's cubegen facility to make cube files. There are a number of non-trivial problems with this approach:
|
|
59
|
+
- Cubegen is part of the (very expensive) Gaussian package and so is not readily available on most machines used for post-processing.
|
|
60
|
+
- The cubegen interface is poor, which makes effective error reporting particularly challenging.
|
|
61
|
+
- Cubegen only takes .fchk files as input, while the rest of this analysis package reads the .log file.
|
|
62
|
+
- .fchk files are not even generated by Gaussian automatically, they must be created from .chk files using another buggy Gaussian program: formchk
|
|
63
|
+
- The requirement on these .fchk files is unnecessary as the data is also available in the .log file, but cubegen cannot read it.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Text description of our input file type, used for error messages etc.
|
|
67
|
+
#input_file_type = "fchk"
|
|
68
|
+
input_file_type = file_types.gaussian_fchk_file
|
|
69
|
+
# Text description of our output file type, used for error messages etc.
|
|
70
|
+
output_file_type = file_types.gaussian_cube_file
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
*args,
|
|
75
|
+
fchk_file = None,
|
|
76
|
+
cubegen_type = "MO",
|
|
77
|
+
orbital = "HOMO",
|
|
78
|
+
npts = 0,
|
|
79
|
+
cube_file = None,
|
|
80
|
+
num_cpu = None,
|
|
81
|
+
memory = None,
|
|
82
|
+
cubegen_executable = "cubegen",
|
|
83
|
+
sanitize = False,
|
|
84
|
+
**kwargs):
|
|
85
|
+
"""
|
|
86
|
+
Constructor for Fchk_to_cube objects.
|
|
87
|
+
|
|
88
|
+
See Image_maker for a full signature.
|
|
89
|
+
|
|
90
|
+
:param output: The filename/path to the cube file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
91
|
+
:param fchk_file: Optional fchk_file to use to generate this cube file.
|
|
92
|
+
:param cubegen_type: The type of orbital that will be included in the cube file when we make it. Possible values are MO, AMO and BMO (for MO, alpha MO and beta MO respectively).
|
|
93
|
+
:param orbital: The orbital to be included in the cube file when we make it. Possible values are 'HOMO', 'LUMO' or the integer level of the desired orbital.
|
|
94
|
+
:param npts: The 'npts' option of cubegen, controls how detailed the resulting file is. Common options are 0 (default), -2 ('low' quality), -3 (medium quality), -4 (very high quality).
|
|
95
|
+
:param cube_file: An optional file path to an existing cube file to use. If this is given (and points to an actual file), then a new cube will not be made and this file will be used instead.
|
|
96
|
+
:param num_cpu: The number of CPUs/processes for cubegen to use. If not given, the number of CPUs available will be used.
|
|
97
|
+
:param memory: The amount of memory for cubegen to use.
|
|
98
|
+
:param sanitize: Whether to modify the cube file to make it compatible with older software.
|
|
99
|
+
"""
|
|
100
|
+
super().__init__(*args, input_file = fchk_file, existing_file = cube_file, **kwargs)
|
|
101
|
+
self.cubegen_type = cubegen_type
|
|
102
|
+
self.orbital = orbital
|
|
103
|
+
self.npts = npts
|
|
104
|
+
memory = memory if memory is not None else "3 GB"
|
|
105
|
+
self.memory = Memory(memory)
|
|
106
|
+
# Default to number of CPUs of the system.
|
|
107
|
+
self.num_cpu = int(num_cpu) if num_cpu is not None else os.cpu_count()
|
|
108
|
+
self.cubegen_executable = expand_path(cubegen_executable)
|
|
109
|
+
self.sanitize = sanitize
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_options(self, output, *, fchk_file = None, cubegen_type = "MO", orbital = "HOMO", options, **kwargs):
|
|
113
|
+
"""
|
|
114
|
+
Constructor that takes a dictionary of config like options.
|
|
115
|
+
"""
|
|
116
|
+
return self(
|
|
117
|
+
output,
|
|
118
|
+
fchk_file = fchk_file,
|
|
119
|
+
cubegen_type = cubegen_type,
|
|
120
|
+
orbital = orbital,
|
|
121
|
+
npts = options['render']['orbital']['cube_grid_size'].to_gaussian(),
|
|
122
|
+
dont_modify = not options['render']['enable_rendering'],
|
|
123
|
+
cubegen_executable = options['external']['cubegen'],
|
|
124
|
+
sanitize = options['render']['safe_cubes'],
|
|
125
|
+
**kwargs
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def make_files(self):
|
|
129
|
+
"""
|
|
130
|
+
Make the files referenced by this object.
|
|
131
|
+
"""
|
|
132
|
+
# The signature we'll use to call cubegen.
|
|
133
|
+
signature = [
|
|
134
|
+
"{}".format(self.cubegen_executable),
|
|
135
|
+
#"{}".format(self.num_cpu),
|
|
136
|
+
"0", # Disable CPUs for now, cubegen does not respond well to >1.
|
|
137
|
+
"{}={}".format(self.cubegen_type, self.orbital),
|
|
138
|
+
str(self.input_file),
|
|
139
|
+
str(self.output),
|
|
140
|
+
str(self.npts)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
cubegen_proc = subprocess.run(
|
|
145
|
+
signature,
|
|
146
|
+
# Capture both stdout and stderr.
|
|
147
|
+
# It appears that cubegen writes everything (including error messages) to stdout, but it does return meaningful exit codes.
|
|
148
|
+
stdout = subprocess.PIPE,
|
|
149
|
+
stderr = subprocess.STDOUT,
|
|
150
|
+
universal_newlines = True,
|
|
151
|
+
env = dict(os.environ, GAUSS_MEMDEF = str(self.memory))
|
|
152
|
+
)
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
raise File_maker_exception(self, "Could not locate cubegen executable '{}'".format(self.cubegen_executable))
|
|
155
|
+
|
|
156
|
+
# If something went wrong, dump output.
|
|
157
|
+
if cubegen_proc.returncode != 0:
|
|
158
|
+
# An error occured.
|
|
159
|
+
# Check if the input file exists, if not this is probably what went wrong.
|
|
160
|
+
message = "Cubegen did not exit successfully"
|
|
161
|
+
if not Path(str(self.input_file)).exists():
|
|
162
|
+
message += " (probably because the input file '{}' could not be found)".format(self.input_file)
|
|
163
|
+
message += ":\n{}".format(cubegen_proc.stdout)
|
|
164
|
+
raise File_maker_exception(self, message)
|
|
165
|
+
else:
|
|
166
|
+
# Everything appeared to go ok.
|
|
167
|
+
# Dump cubegen output if we're in debug.
|
|
168
|
+
digichem.log.get_logger().debug(cubegen_proc.stdout)
|
|
169
|
+
|
|
170
|
+
if self.sanitize:
|
|
171
|
+
sanitize_modern_cubes(self.output)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Fchk_to_spin_cube(Fchk_to_cube):
|
|
175
|
+
"""
|
|
176
|
+
A variation of the cube maker designed for making spin density cubes.
|
|
177
|
+
|
|
178
|
+
Note that this class exists mainly for convenience; either class can be used to generate cubes of any supported type.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(self, *args, spin_density = "SCF", **kwargs):
|
|
182
|
+
"""
|
|
183
|
+
Constructor for Fchk_to_cube objects.
|
|
184
|
+
|
|
185
|
+
See Fchk_to_cube for a full signature.
|
|
186
|
+
|
|
187
|
+
:param output: The filename/path to the cube file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
188
|
+
:param fchk_file: Optional fchk_file to use to generate this cube file.
|
|
189
|
+
:param spin_density: The density to use; you almost certainly want SCF unless perhaps you are using MP2...
|
|
190
|
+
:param npts: The 'npts' option of cubegen, controls how detailed the resulting file is. Common options are 0 (default), -2 ('low' quality), -3 (medium quality), -4 (very high quality).
|
|
191
|
+
:param cube_file: An optional file path to an existing cube file to use. If this is given (and points to an actual file), then a new cube will not be made and this file will be used instead.
|
|
192
|
+
"""
|
|
193
|
+
return super().__init__(*args, cubegen_type = "Spin", orbital = spin_density, **kwargs)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_options(self, output, *, fchk_file = None, spin_density = "SCF", options, **kwargs):
|
|
197
|
+
"""
|
|
198
|
+
Constructor that takes a dictionary of config like options.
|
|
199
|
+
"""
|
|
200
|
+
return self(
|
|
201
|
+
output,
|
|
202
|
+
fchk_file = fchk_file,
|
|
203
|
+
spin_density = spin_density,
|
|
204
|
+
npts = options['render']['spin']['cube_grid_size'].to_gaussian(),
|
|
205
|
+
dont_modify = not options['render']['enable_rendering'],
|
|
206
|
+
cubegen_executable = options['external']['cubegen'],
|
|
207
|
+
sanitize = options['render']['safe_cubes'],
|
|
208
|
+
**kwargs
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
class Fchk_to_density_cube(Fchk_to_cube):
|
|
212
|
+
"""
|
|
213
|
+
A variation of the cube maker designed for making density cubes.
|
|
214
|
+
|
|
215
|
+
Note that this class exists mainly for convenience; either class can be used to generate cubes of any supported type.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(self, *args, density_type = "SCF", **kwargs):
|
|
219
|
+
"""
|
|
220
|
+
Constructor for Fchk_to_cube objects.
|
|
221
|
+
|
|
222
|
+
See Fchk_to_cube for a full signature.
|
|
223
|
+
|
|
224
|
+
:param output: The filename/path to the cube file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
225
|
+
:param fchk_file: Optional fchk_file to use to generate this cube file.
|
|
226
|
+
:param density_type: The density to use.
|
|
227
|
+
:param npts: The 'npts' option of cubegen, controls how detailed the resulting file is. Common options are 0 (default), -2 ('low' quality), -3 (medium quality), -4 (very high quality).
|
|
228
|
+
:param cube_file: An optional file path to an existing cube file to use. If this is given (and points to an actual file), then a new cube will not be made and this file will be used instead.
|
|
229
|
+
"""
|
|
230
|
+
super().__init__(*args, cubegen_type = "Density", orbital = density_type, **kwargs)
|
|
231
|
+
self.type = density_type
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def from_options(self, output, *, fchk_file = None, density_type = "SCF", options, **kwargs):
|
|
235
|
+
"""
|
|
236
|
+
Constructor that takes a dictionary of config like options.
|
|
237
|
+
"""
|
|
238
|
+
return self(
|
|
239
|
+
output,
|
|
240
|
+
fchk_file = fchk_file,
|
|
241
|
+
density_type = density_type,
|
|
242
|
+
npts = options['render']['density']['cube_grid_size'].to_gaussian(),
|
|
243
|
+
dont_modify = not options['render']['enable_rendering'],
|
|
244
|
+
cubegen_executable = options['external']['cubegen'],
|
|
245
|
+
sanitize = options['render']['safe_cubes'],
|
|
246
|
+
**kwargs
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
class Fchk_to_nto_cube(Fchk_to_cube):
|
|
250
|
+
"""
|
|
251
|
+
A variation of the cube maker designed for making NTO cubes.
|
|
252
|
+
|
|
253
|
+
Note that this class exists mainly for convenience; either class can be used to generate cubes of any supported type.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(self, *args, orbital = "HOMO", **kwargs):
|
|
257
|
+
"""
|
|
258
|
+
Constructor for Fchk_to_cube objects.
|
|
259
|
+
|
|
260
|
+
See Fchk_to_cube for a full signature.
|
|
261
|
+
|
|
262
|
+
:param output: The filename/path to the cube file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
263
|
+
:param fchk_file: Optional fchk_file to use to generate this cube file.
|
|
264
|
+
:param orbital: The NTO to plot.
|
|
265
|
+
:param npts: The 'npts' option of cubegen, controls how detailed the resulting file is. Common options are 0 (default), -2 ('low' quality), -3 (medium quality), -4 (very high quality).
|
|
266
|
+
:param cube_file: An optional file path to an existing cube file to use. If this is given (and points to an actual file), then a new cube will not be made and this file will be used instead.
|
|
267
|
+
"""
|
|
268
|
+
super().__init__(*args, cubegen_type = "MO", orbital = orbital, **kwargs)
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def from_options(self, output, *, fchk_file = None, orbital = "HOMO", options, **kwargs):
|
|
272
|
+
"""
|
|
273
|
+
Constructor that takes a dictionary of config like options.
|
|
274
|
+
"""
|
|
275
|
+
return self(
|
|
276
|
+
output,
|
|
277
|
+
fchk_file = fchk_file,
|
|
278
|
+
orbital = orbital,
|
|
279
|
+
npts = options['render']['natural_transition_orbital']['cube_grid_size'].to_gaussian(),
|
|
280
|
+
dont_modify = not options['render']['enable_rendering'],
|
|
281
|
+
cubegen_executable = options['external']['cubegen'],
|
|
282
|
+
sanitize = options['render']['safe_cubes'],
|
|
283
|
+
**kwargs
|
|
284
|
+
)
|