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/fchk.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# General imports.
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from digichem.exception.base import File_maker_exception
|
|
7
|
+
|
|
8
|
+
# Digichem imports.
|
|
9
|
+
from digichem.file import File_converter
|
|
10
|
+
import digichem.file.types as file_types
|
|
11
|
+
import digichem.log
|
|
12
|
+
from digichem.memory import Memory
|
|
13
|
+
from digichem.misc.io import expand_path
|
|
14
|
+
|
|
15
|
+
class Chk_to_fchk(File_converter):
|
|
16
|
+
"""
|
|
17
|
+
Class for creating Gaussian fchk files from Gaussian chk files.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Text description of our input file type, used for error messages etc.
|
|
21
|
+
#input_file_type = "chk"
|
|
22
|
+
input_file_type = file_types.gaussian_chk_file
|
|
23
|
+
# Text description of our output file type, used for error messages etc.
|
|
24
|
+
#output_file_type = "fchk"
|
|
25
|
+
output_file_type = file_types.gaussian_fchk_file
|
|
26
|
+
|
|
27
|
+
def __init__(self, *args, chk_file = None, fchk_file = None, memory = None, formchk_executable = "formchk", **kwargs):
|
|
28
|
+
"""
|
|
29
|
+
Constructor for Chk_to_fchk objects.
|
|
30
|
+
|
|
31
|
+
See Image_maker for a full signature.
|
|
32
|
+
|
|
33
|
+
:param output: The filename/path to the fchk file (this path doesn't need to point to a real file yet; we will use this path to write to).
|
|
34
|
+
:param chk_file: Optional chk_file to use to generate this fchk file.
|
|
35
|
+
:param fchk_file: An optional file path to an existing fchk file to use. If this is given (and points to an actual file), then a new fchk will not be made and this file will be used instead.
|
|
36
|
+
:param memory: The amount of memory for formchk to use.
|
|
37
|
+
:param formchk_executable: 'Path' to the executable to use for formchk.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(*args, input_file = chk_file, existing_file = fchk_file, **kwargs)
|
|
40
|
+
memory = memory if memory is not None else "3 GB"
|
|
41
|
+
self.memory = Memory(memory)
|
|
42
|
+
self.formchk_executable = expand_path(formchk_executable)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_options(self, output, *, chk_file = None, memory = None, options, **kwargs):
|
|
46
|
+
"""
|
|
47
|
+
Constructor that takes a dictionary of config like options.
|
|
48
|
+
"""
|
|
49
|
+
return self(
|
|
50
|
+
output,
|
|
51
|
+
chk_file = chk_file,
|
|
52
|
+
memory = memory,
|
|
53
|
+
formchk_executable = options['external']['formchk'],
|
|
54
|
+
**kwargs
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def make_files(self):
|
|
58
|
+
"""
|
|
59
|
+
Make the files referenced by this object.
|
|
60
|
+
"""
|
|
61
|
+
input_file = Path(str(self.input_file))
|
|
62
|
+
|
|
63
|
+
# The signature we'll use to call formchk.
|
|
64
|
+
signature = [
|
|
65
|
+
"{}".format(self.formchk_executable),
|
|
66
|
+
input_file.name,
|
|
67
|
+
str(self.output.absolute())
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Sadly (but not unexpectedly), the g09 version of formchk (and maybe other versions too) will crash if their are certain characters (at least '(' and ')') in the input directory name. The final part of the input name appears to be fine, as does everything in the output dir.
|
|
71
|
+
# This is easily fixed by making the output dir absolute and cd'ing into the input directory.
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
formchk_proc = subprocess.run(
|
|
75
|
+
signature,
|
|
76
|
+
# Capture both stdout and stderr.
|
|
77
|
+
stdout = subprocess.PIPE,
|
|
78
|
+
stderr = subprocess.STDOUT,
|
|
79
|
+
universal_newlines = True,
|
|
80
|
+
cwd = str(input_file.parent),
|
|
81
|
+
env = dict(os.environ, GAUSS_MEMDEF = str(self.memory))
|
|
82
|
+
)
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
raise File_maker_exception(self, "Could not locate formchk executable '{}'".format(self.formchk_executable))
|
|
85
|
+
|
|
86
|
+
# If something went wrong, dump output.
|
|
87
|
+
if formchk_proc.returncode != 0:
|
|
88
|
+
# An error occured.
|
|
89
|
+
raise File_maker_exception(self, "Formchk did not exit successfully:\n{}".format(formchk_proc.stdout))
|
|
90
|
+
else:
|
|
91
|
+
# Everything appeared to go ok.
|
|
92
|
+
# Dump formchk output if we're in debug.
|
|
93
|
+
digichem.log.get_logger().debug(formchk_proc.stdout)
|
|
94
|
+
|
digichem/file/prattle.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# General imports.
|
|
2
|
+
import subprocess
|
|
3
|
+
from subprocess import CalledProcessError
|
|
4
|
+
import re
|
|
5
|
+
import os
|
|
6
|
+
import copy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import warnings
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
# Digichem imports.
|
|
12
|
+
from digichem.exception.base import Digichem_exception
|
|
13
|
+
import digichem.log
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Openprattle_converter():
|
|
17
|
+
"""
|
|
18
|
+
Provides an interface to oprattle.
|
|
19
|
+
|
|
20
|
+
Openprattle provides a library interface, but because of the GPL we cannot use it.
|
|
21
|
+
Calling the oprattle command is fine, however.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, input_file = None, input_file_buffer = None, input_file_path = None, input_file_type = None, executable = "oprattle"):
|
|
25
|
+
"""
|
|
26
|
+
Constructor for the OpenBabel converter.
|
|
27
|
+
|
|
28
|
+
Input files can be specified in one of three ways:
|
|
29
|
+
- As an open file descriptor (input_file and input_file_type)
|
|
30
|
+
- As an in-memory buffer, most probably a string or bytes-like object (input_file_buffer and input_file_type)
|
|
31
|
+
- As a file path (input_file_path and optionally input_file_type)
|
|
32
|
+
|
|
33
|
+
:param input_file: An open file descriptor in the format given by input_file_type that should be converted.
|
|
34
|
+
:param input_file_buffer: Alternatively, a buffer (unicode string or bytes) in the format given by input_file_type that should be converted.
|
|
35
|
+
:param input_file_path: Alternatively, a path to a file that should be converted.
|
|
36
|
+
:param input_file_type: A shortcode identifying the format of the input file. If not given but input_file_path is given, then this will be determined automatically.
|
|
37
|
+
:param executable: Path or command name to the oprattle executable.
|
|
38
|
+
"""
|
|
39
|
+
self.input_file = input_file
|
|
40
|
+
self.input_file_buffer = input_file_buffer
|
|
41
|
+
self.input_file_path = input_file_path
|
|
42
|
+
self.input_file_type = input_file_type
|
|
43
|
+
self.executable = executable
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def input_name(self):
|
|
47
|
+
"""
|
|
48
|
+
A descriptive name of the file we are converting. Works even if converting from memory.
|
|
49
|
+
"""
|
|
50
|
+
if self.input_file_path is not None:
|
|
51
|
+
return self.input_file_path
|
|
52
|
+
else:
|
|
53
|
+
return "(file loaded from memory)"
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_cls(self, input_file_type):
|
|
57
|
+
"""
|
|
58
|
+
This function is deprecated, and does nothing.
|
|
59
|
+
"""
|
|
60
|
+
warnings.warn("get_cls() is deprecated, use Openprattle_converter directly instead", DeprecationWarning)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_file(self, input_file_path, input_file_type = None, **kwargs):
|
|
65
|
+
"""
|
|
66
|
+
This function is deprecated, and does nothing.
|
|
67
|
+
"""
|
|
68
|
+
warnings.warn("from_file() is deprecated, use the Openprattle_converter constructor directly instead", DeprecationWarning)
|
|
69
|
+
return self.get_cls(None)(input_file_path = input_file_path, input_file_type = input_file_type, **kwargs)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def type_from_file_name(self, input_file_name, allow_none = False):
|
|
73
|
+
"""
|
|
74
|
+
Get the type of a file based on its file name.
|
|
75
|
+
|
|
76
|
+
This method largely uses the file extension (.com, .tmol etc), with a few other simple rules.
|
|
77
|
+
|
|
78
|
+
:param input_file_name: The name of the file to check.
|
|
79
|
+
:param allow_none: If the type of the file cannot be determined and allow_none is False (the default), an exception is raised. Otherwise, None is returned.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
input_file_name = Path(input_file_name)
|
|
83
|
+
|
|
84
|
+
except TypeError:
|
|
85
|
+
if allow_none:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
# No file given.
|
|
90
|
+
raise ValueError("Could not automatically determine file format; no file name was given") from None
|
|
91
|
+
|
|
92
|
+
# Get file extension (removing the dot character).
|
|
93
|
+
extension = input_file_name.suffix[1:]
|
|
94
|
+
|
|
95
|
+
if extension != "":
|
|
96
|
+
# All done.
|
|
97
|
+
return extension.lower()
|
|
98
|
+
|
|
99
|
+
elif input_file_name.name == "coord":
|
|
100
|
+
# This is a turbomole file.
|
|
101
|
+
return "tmol"
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
if allow_none:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
# Don't recognise the file format.
|
|
109
|
+
raise ValueError("Could not determine file format of file '{}'; the file does not have an extension and is not recognised".format(input_file_name))
|
|
110
|
+
|
|
111
|
+
def convert(self, output_file_type = None, output_file = None, *, gen3D = None, charge = None, multiplicity = None):
|
|
112
|
+
"""
|
|
113
|
+
Convert the input file wrapped by this class to the designated output_file_type.
|
|
114
|
+
|
|
115
|
+
:param output_file_type: The file type to convert to.
|
|
116
|
+
:param output_file: Optional file name to write to. If not given, the converted file will be returned as a string (or binary string depending on format).
|
|
117
|
+
:param gen3D: If True and the loaded molecule does not have 3D coordinates, these will be generated (this will scramble atom coordinates).
|
|
118
|
+
:param charge: Optional charge of the output format.
|
|
119
|
+
:param multiplicity: Optional multiplicity of the output format.
|
|
120
|
+
:return: The converted file, or None if output_file is not None.
|
|
121
|
+
"""
|
|
122
|
+
output_file = str(output_file) if output_file is not None else None
|
|
123
|
+
|
|
124
|
+
# The signature we'll use to run oprattle.
|
|
125
|
+
sig = [
|
|
126
|
+
str(self.executable),
|
|
127
|
+
"--json"
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# Add the input path if we're reading from file.
|
|
131
|
+
if self.input_file is None:
|
|
132
|
+
sig.append(str(self.input_file_path))
|
|
133
|
+
|
|
134
|
+
# Now add the input and output switches.
|
|
135
|
+
if output_file_type:
|
|
136
|
+
sig.extend([
|
|
137
|
+
"-o", output_file_type
|
|
138
|
+
])
|
|
139
|
+
if self.input_file_type:
|
|
140
|
+
sig.extend([
|
|
141
|
+
"-i", self.input_file_type
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
# Add gen3D command if we've been asked to.
|
|
145
|
+
if gen3D is True:
|
|
146
|
+
sig.extend(["--gen3D", "True"])
|
|
147
|
+
elif gen3D is False:
|
|
148
|
+
sig.extend(["--gen3D", "False"])
|
|
149
|
+
|
|
150
|
+
# If a file to write to has been given, set it.
|
|
151
|
+
if output_file is not None:
|
|
152
|
+
sig.extend(['-O', output_file])
|
|
153
|
+
|
|
154
|
+
# Give our input_file as stdin if we're not reading from file.
|
|
155
|
+
inputs = self.input_file
|
|
156
|
+
|
|
157
|
+
# GO.
|
|
158
|
+
try:
|
|
159
|
+
done_process = subprocess.run(
|
|
160
|
+
sig,
|
|
161
|
+
input = inputs,
|
|
162
|
+
stdout = subprocess.PIPE,
|
|
163
|
+
stderr = subprocess.PIPE,
|
|
164
|
+
# TODO: Using universal newlines is probably not safe here; some formats are binary (.cdx etc...)
|
|
165
|
+
universal_newlines = True,
|
|
166
|
+
check = True,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
except CalledProcessError as e:
|
|
170
|
+
self.handle_logging(e.stderr)
|
|
171
|
+
raise
|
|
172
|
+
|
|
173
|
+
self.handle_logging(done_process.stderr)
|
|
174
|
+
|
|
175
|
+
# Return our output.
|
|
176
|
+
return done_process.stdout if output_file is None else None
|
|
177
|
+
|
|
178
|
+
def handle_logging(self, raw_output):
|
|
179
|
+
"""
|
|
180
|
+
"""
|
|
181
|
+
for raw_message in raw_output.split("\n"):
|
|
182
|
+
if raw_message == "":
|
|
183
|
+
# Nothing returned, nothing to do.
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# Each message should be in JSON, but check.
|
|
187
|
+
try:
|
|
188
|
+
message = json.loads(raw_message)
|
|
189
|
+
message_text = message['message']
|
|
190
|
+
if message['exception']:
|
|
191
|
+
message_text += "\n" + message['exception']
|
|
192
|
+
digichem.log.get_logger().log(
|
|
193
|
+
message['levelno'],
|
|
194
|
+
message_text
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
except Exception:
|
|
198
|
+
digichem.log.get_logger().error("Unexpected output from oprattle: '{}'".format(raw_message), exc_info=False)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class Oprattle_formats():
|
|
203
|
+
"""
|
|
204
|
+
Class for retrieving the supported file formats from oprattle.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, executable = "oprattle"):
|
|
208
|
+
"""
|
|
209
|
+
"""
|
|
210
|
+
self.executable = executable
|
|
211
|
+
|
|
212
|
+
def run(self, readwrite):
|
|
213
|
+
"""
|
|
214
|
+
"""
|
|
215
|
+
if readwrite not in ["read", "write"]:
|
|
216
|
+
raise ValueError("readwrite must be one of either 'read' or 'write'")
|
|
217
|
+
|
|
218
|
+
# The signature we'll use to run oprattle.
|
|
219
|
+
sig = [
|
|
220
|
+
self.executable,
|
|
221
|
+
"--json",
|
|
222
|
+
"--readable" if readwrite == "read" else "--writable"
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
# GO.
|
|
226
|
+
done_process = subprocess.run(
|
|
227
|
+
sig,
|
|
228
|
+
stdout = subprocess.PIPE,
|
|
229
|
+
stderr = subprocess.PIPE,
|
|
230
|
+
universal_newlines = True,
|
|
231
|
+
check = True,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
formats = json.loads(done_process.stdout)
|
|
235
|
+
|
|
236
|
+
return formats
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def read(self, refresh = False):
|
|
240
|
+
"""
|
|
241
|
+
Retrieve supported input (read) formats.
|
|
242
|
+
"""
|
|
243
|
+
if refresh:
|
|
244
|
+
try:
|
|
245
|
+
del self._read
|
|
246
|
+
|
|
247
|
+
except AttributeError:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
return self._read
|
|
252
|
+
|
|
253
|
+
except AttributeError:
|
|
254
|
+
# Cache miss.
|
|
255
|
+
self._read = self.run("read")
|
|
256
|
+
return self._read
|
|
257
|
+
|
|
258
|
+
def write(self, refresh = False):
|
|
259
|
+
"""
|
|
260
|
+
Retrieve supported input (read) formats.
|
|
261
|
+
"""
|
|
262
|
+
if refresh:
|
|
263
|
+
try:
|
|
264
|
+
del self._write
|
|
265
|
+
|
|
266
|
+
except AttributeError:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
return self._write
|
|
271
|
+
|
|
272
|
+
except AttributeError:
|
|
273
|
+
# Cache miss.
|
|
274
|
+
self._write = self.run("write")
|
|
275
|
+
return self._write
|
|
276
|
+
|
|
277
|
+
|
digichem/file/types.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Functions and classes for determining file types etc.
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from itertools import accumulate
|
|
4
|
+
|
|
5
|
+
from digichem.exception.base import Unknown_file_type_exception
|
|
6
|
+
|
|
7
|
+
class File_type():
|
|
8
|
+
"""
|
|
9
|
+
Class for representing a known file type.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, name, family = None, extensions = None, short_code = None):
|
|
13
|
+
"""
|
|
14
|
+
Constructor for File_type objects.
|
|
15
|
+
|
|
16
|
+
:param name: The name of the file (eg, "JPEG").
|
|
17
|
+
:param family: The name of the family/group that the file type belongs to (eg, "image").
|
|
18
|
+
:param extensions: An iterable of known file extensions (this is case-insensitive) (eg, [".jpg", ".jpeg"]).
|
|
19
|
+
"""
|
|
20
|
+
self.name = name
|
|
21
|
+
self.family = family
|
|
22
|
+
self.extensions = [extension.lower() for extension in extensions] if extensions is not None else []
|
|
23
|
+
|
|
24
|
+
if short_code is None and len(self.extensions) > 0:
|
|
25
|
+
# Take one of our extensions (without the dot).
|
|
26
|
+
self.short_code = self.extensions[0][1:]
|
|
27
|
+
|
|
28
|
+
else:
|
|
29
|
+
self.short_code = short_code
|
|
30
|
+
|
|
31
|
+
def check(self, file_path):
|
|
32
|
+
"""
|
|
33
|
+
Determine whether a given file is of this file type.
|
|
34
|
+
|
|
35
|
+
:param file_path: A string-like path to the file to check.
|
|
36
|
+
:return: True if the file is of this type, false otherwise.
|
|
37
|
+
"""
|
|
38
|
+
# Get a Path object.
|
|
39
|
+
file_path = Path(file_path)
|
|
40
|
+
|
|
41
|
+
# Check to see if the file extension matches one of our file extensions.
|
|
42
|
+
# We recurse through all the given file's extensions in case one of our extensions contains multiple parts (".tar.gz" for example).
|
|
43
|
+
for extension in accumulate(reversed(file_path.suffixes), lambda a, b: b+a):
|
|
44
|
+
if extension.lower() in self.extensions:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
# No match found.
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def file_type(self):
|
|
52
|
+
"""
|
|
53
|
+
Get a string uniquely identifying this file type.
|
|
54
|
+
"""
|
|
55
|
+
if self.family is not None:
|
|
56
|
+
return "{}/{}".format(self.family, self.name)
|
|
57
|
+
else:
|
|
58
|
+
return self.name
|
|
59
|
+
|
|
60
|
+
def __str__(self):
|
|
61
|
+
"""
|
|
62
|
+
Stringify this file type.
|
|
63
|
+
"""
|
|
64
|
+
return self.file_type
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Known file types.
|
|
68
|
+
log_file = File_type("log", "general", [".log"])
|
|
69
|
+
gaussian_chk_file = File_type("checkpoint", "gaussian", [".chk"])
|
|
70
|
+
gaussian_NTO_chk_file = File_type("NTO-checkpoint", "gaussian", [".chk"])
|
|
71
|
+
gaussian_fchk_file = File_type("formatted-checkpoint", "gaussian", [".fchk"])
|
|
72
|
+
gaussian_rwf_file = File_type("read-write", "gaussian", [".rwf"])
|
|
73
|
+
gaussian_cube_file = File_type("cube", "gaussian", [".cub", ".cube"])
|
|
74
|
+
|
|
75
|
+
orca_gbw_file = File_type("gbw", "orca", [".gbw"])
|
|
76
|
+
orca_density_file = File_type("density", "orca", [".densities"])
|
|
77
|
+
|
|
78
|
+
# A list of all our known types.
|
|
79
|
+
known_types = [log_file, gaussian_chk_file, gaussian_fchk_file, gaussian_rwf_file, gaussian_cube_file, orca_gbw_file]
|
|
80
|
+
|
|
81
|
+
# TODO: This seems to be unused?
|
|
82
|
+
def get_file_type(file_path):
|
|
83
|
+
"""
|
|
84
|
+
Get the type of a file from a list of known file types.
|
|
85
|
+
|
|
86
|
+
:raises Unknown_file_type_exception: If the given file is of an unknown type.
|
|
87
|
+
:param file_path: String-like path to the file to check.
|
|
88
|
+
:return: A File_type object.
|
|
89
|
+
"""
|
|
90
|
+
# Iterate through our list of known file types.
|
|
91
|
+
for file_type in known_types:
|
|
92
|
+
if file_type.check(file_path):
|
|
93
|
+
return file_type
|
|
94
|
+
|
|
95
|
+
# Unknown file type.
|
|
96
|
+
raise Unknown_file_type_exception(file_path)
|
|
97
|
+
|
digichem/image/base.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from digichem.file import File_maker
|
|
5
|
+
from digichem.exception import File_maker_exception
|
|
6
|
+
|
|
7
|
+
class Cropable_mixin():
|
|
8
|
+
"""Mixin class for those that can crop their images."""
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def auto_crop_image(self, im):
|
|
12
|
+
"""
|
|
13
|
+
Automatically remove excess white pixels around the outside of our image.
|
|
14
|
+
|
|
15
|
+
:param im: A PIL Image object.
|
|
16
|
+
:returns: A new PIL Image object with white-space removed.
|
|
17
|
+
"""
|
|
18
|
+
# (try to) load our image from file.
|
|
19
|
+
#im = Image.open(file_path, "r")
|
|
20
|
+
im.load()
|
|
21
|
+
|
|
22
|
+
# This solution was inspired by https://stackoverflow.com/questions/14211340/automatically-cropping-an-image-with-python-pil
|
|
23
|
+
image_data = np.asarray(im)
|
|
24
|
+
if im.mode == "RGBA":
|
|
25
|
+
image_data_bw = image_data[:,:,3]
|
|
26
|
+
|
|
27
|
+
# Now we just check which rows and columns are not pure white.
|
|
28
|
+
non_empty_columns = np.where(image_data_bw.max(axis = 0) != 0)[0]
|
|
29
|
+
non_empty_rows = np.where(image_data_bw.max(axis = 1) != 0)[0]
|
|
30
|
+
|
|
31
|
+
else:
|
|
32
|
+
# Axis 2 is the tuple/array of RGB values for each pixel. We want to know which ones are fully white (255,255,255), so we'll take the min of the 3 values and see if this is equal to 255. If it is, then all the pixels are 255
|
|
33
|
+
image_data_bw = image_data.min(axis = 2)
|
|
34
|
+
|
|
35
|
+
# Now we just check which rows and columns are not pure white.
|
|
36
|
+
non_empty_columns = np.where(image_data_bw.min(axis = 0) != 255)[0]
|
|
37
|
+
non_empty_rows = np.where(image_data_bw.min(axis = 1) != 255)[0]
|
|
38
|
+
|
|
39
|
+
if len(non_empty_rows) == 0 or len(non_empty_columns) == 0:
|
|
40
|
+
raise ValueError("The image is empty!")
|
|
41
|
+
|
|
42
|
+
# Get the bounding box of non-white stuff.
|
|
43
|
+
cropBox = (min(non_empty_rows), max(non_empty_rows), min(non_empty_columns), max(non_empty_columns))
|
|
44
|
+
|
|
45
|
+
# Copy the image data we want.
|
|
46
|
+
image_data_new = image_data[cropBox[0]:cropBox[1]+1, cropBox[2]:cropBox[3]+1 , :]
|
|
47
|
+
|
|
48
|
+
# And save over our old file.
|
|
49
|
+
new_image = Image.fromarray(image_data_new)
|
|
50
|
+
return new_image
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Image_maker(File_maker, Cropable_mixin):
|
|
54
|
+
"""
|
|
55
|
+
Top level class for image maker objects.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Text description of our output file type, used for error messages etc. This can be changed by inheriting classes.
|
|
59
|
+
output_file_type = "image"
|
|
60
|
+
|
|
61
|
+
def __init__(self, *args, enable_rendering = True, **kwargs):
|
|
62
|
+
"""
|
|
63
|
+
General constructor for Image_maker objects.
|
|
64
|
+
|
|
65
|
+
These object are used to represent and make images. Note that a single Image_maker object can represent several image_files
|
|
66
|
+
|
|
67
|
+
:param output: A path to an output file to write to. How exactly this operates depends on the inheriting class, it is often only used as a base file name. See the class you are using.
|
|
68
|
+
:param dont_modify: Flag that modifies how image creation works. If True, no new images will be written to file.
|
|
69
|
+
:param use_existing: Flag that modifies how image creation works. If True, existing files will be preferentially used if available (set to False to force overwriting existing files).
|
|
70
|
+
"""
|
|
71
|
+
super().__init__(*args, existing_file = None, dont_modify = not enable_rendering, **kwargs)
|
|
72
|
+
|
|
73
|
+
def get_image(self, name = 'file'):
|
|
74
|
+
"""
|
|
75
|
+
Get the path to one of the images that this class represents, rendering the image to file first if necessary.
|
|
76
|
+
|
|
77
|
+
The functioning of this method is controlled by the dont_modify & use_existing flags.
|
|
78
|
+
|
|
79
|
+
You can also use the normal python attribute mechanism (either through getattr() or dot notation) to get these paths.
|
|
80
|
+
|
|
81
|
+
:raises KeyError: If name is not the name of one of the images this class represents.
|
|
82
|
+
:param name: The name of an image to get. Depends on the images in self.file_path.
|
|
83
|
+
:return: A pathlib Path object pointing to the image represented by name, or None if no image could be created.
|
|
84
|
+
"""
|
|
85
|
+
return self.safe_get_file(name)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def creation_message(self):
|
|
89
|
+
"""
|
|
90
|
+
A short message that may (depending on log-level) be printed to the user before make_files() is called.
|
|
91
|
+
"""
|
|
92
|
+
return "Rendering '{}' to file(s)".format(self.output if self.full_path_names else self.output.name)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_constrained_size(self, max_width, max_height):
|
|
96
|
+
"""
|
|
97
|
+
Get the maximum possible dimensions of this image that retain the same aspect ratio that don't exceed a set of dimensions.
|
|
98
|
+
|
|
99
|
+
:param max_width: The maximum width to resize to; set to math.inf for no max.
|
|
100
|
+
:param max_heigh: The maximum height to resize to; set to math.inf for no max.
|
|
101
|
+
"""
|
|
102
|
+
# Weasyprint hack for broken max-width.
|
|
103
|
+
|
|
104
|
+
# Now we open our diagram with pillow.
|
|
105
|
+
im = Image.open(self.get_image())
|
|
106
|
+
|
|
107
|
+
width, height = im.size
|
|
108
|
+
|
|
109
|
+
scale_factor = min(max_width/width, max_height/height)
|
|
110
|
+
|
|
111
|
+
return (width *scale_factor, height * scale_factor)
|
|
112
|
+
|
|
113
|
+
|