digichem-core 6.0.3__py3-none-any.whl → 6.10.1__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 +2 -2
- digichem/config/base.py +5 -3
- digichem/data/batoms/batoms-renderer.py +190 -50
- digichem/data/batoms/batoms_renderer.py +500 -0
- digichem/file/base.py +17 -3
- digichem/file/cube.py +185 -16
- digichem/file/types.py +1 -0
- digichem/image/render.py +144 -45
- digichem/image/vmd.py +7 -2
- digichem/input/digichem_input.py +2 -2
- digichem/memory.py +10 -0
- digichem/misc/io.py +84 -1
- digichem/parse/__init__.py +6 -1
- digichem/parse/base.py +85 -54
- digichem/parse/cclib.py +103 -13
- digichem/parse/dump.py +3 -3
- digichem/parse/orca.py +1 -0
- digichem/parse/pyscf.py +25 -0
- digichem/parse/turbomole.py +5 -5
- digichem/parse/util.py +146 -65
- digichem/result/excited_state.py +17 -11
- digichem/result/metadata.py +272 -3
- digichem/result/result.py +3 -0
- digichem/result/spectroscopy.py +42 -0
- digichem/test/test_memory.py +33 -0
- digichem/test/test_parsing.py +68 -1
- digichem/test/test_result.py +1 -1
- digichem/test/util.py +2 -1
- {digichem_core-6.0.3.dist-info → digichem_core-6.10.1.dist-info}/METADATA +4 -3
- {digichem_core-6.0.3.dist-info → digichem_core-6.10.1.dist-info}/RECORD +33 -30
- {digichem_core-6.0.3.dist-info → digichem_core-6.10.1.dist-info}/WHEEL +1 -1
- {digichem_core-6.0.3.dist-info → digichem_core-6.10.1.dist-info}/licenses/COPYING.md +0 -0
- {digichem_core-6.0.3.dist-info → digichem_core-6.10.1.dist-info}/licenses/LICENSE +0 -0
digichem/file/cube.py
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import subprocess
|
|
5
5
|
import tempfile
|
|
6
|
-
import
|
|
6
|
+
import asyncio
|
|
7
7
|
import os
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
try:
|
|
10
|
+
from pyscf.tools import cubegen
|
|
11
|
+
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
# No PySCF.
|
|
14
|
+
cubegen = None
|
|
10
15
|
|
|
16
|
+
from digichem.exception.base import File_maker_exception
|
|
11
17
|
from digichem.file import File_converter
|
|
12
18
|
import digichem.file.types as file_types
|
|
13
19
|
import digichem.log
|
|
@@ -81,6 +87,7 @@ class Fchk_to_cube(File_converter):
|
|
|
81
87
|
memory = None,
|
|
82
88
|
cubegen_executable = "cubegen",
|
|
83
89
|
sanitize = False,
|
|
90
|
+
multithreading = None,
|
|
84
91
|
**kwargs):
|
|
85
92
|
"""
|
|
86
93
|
Constructor for Fchk_to_cube objects.
|
|
@@ -104,12 +111,18 @@ class Fchk_to_cube(File_converter):
|
|
|
104
111
|
memory = memory if memory is not None else "3 GB"
|
|
105
112
|
self.memory = Memory(memory)
|
|
106
113
|
# Default to number of CPUs of the system.
|
|
107
|
-
|
|
114
|
+
# If we've not been asked to run multithreaded, use 1 CPU.
|
|
115
|
+
if multithreading is None or multithreading == "pool":
|
|
116
|
+
self.num_cpu = 1
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
self.num_cpu = int(num_cpu) if num_cpu is not None else os.cpu_count()
|
|
120
|
+
|
|
108
121
|
self.cubegen_executable = expand_path(cubegen_executable)
|
|
109
122
|
self.sanitize = sanitize
|
|
110
123
|
|
|
111
124
|
@classmethod
|
|
112
|
-
def from_options(self, output, *, fchk_file = None, cubegen_type = "MO", orbital = "HOMO", options, **kwargs):
|
|
125
|
+
def from_options(self, output, *, fchk_file = None, cubegen_type = "MO", orbital = "HOMO", num_cpu = None, options, **kwargs):
|
|
113
126
|
"""
|
|
114
127
|
Constructor that takes a dictionary of config like options.
|
|
115
128
|
"""
|
|
@@ -122,34 +135,115 @@ class Fchk_to_cube(File_converter):
|
|
|
122
135
|
dont_modify = not options['render']['enable_rendering'],
|
|
123
136
|
cubegen_executable = options['external']['cubegen'],
|
|
124
137
|
sanitize = options['render']['safe_cubes'],
|
|
138
|
+
num_cpu = num_cpu,
|
|
139
|
+
multithreading = options['external']['cubegen_parallel'],
|
|
125
140
|
**kwargs
|
|
126
141
|
)
|
|
142
|
+
|
|
143
|
+
def get_parallel(self, name = 'file', cpus = 1):
|
|
144
|
+
"""
|
|
145
|
+
Generate the file represented by this object in a parallel context.
|
|
146
|
+
|
|
147
|
+
get_parallel() will be called by a higher level function as an argument to ThreadPoolExecutor.map() or similar,
|
|
148
|
+
to generate many files simultaneously. This is useful for slow operations (such as cube generation) that are
|
|
149
|
+
difficult to parallelise individually, but easy to parallelise across multiple files.
|
|
150
|
+
|
|
151
|
+
:param name: The file to generate.
|
|
152
|
+
:param cpus: The number of CPUs this operation should use, nearly always 1.
|
|
153
|
+
"""
|
|
154
|
+
# Temporarily set CPUs to 1 (in-case it got changed somewhere else.)
|
|
155
|
+
old_cpus = self.num_cpu
|
|
156
|
+
self.num_cpu = cpus
|
|
157
|
+
try:
|
|
158
|
+
self.get_file(name = name)
|
|
127
159
|
|
|
128
|
-
|
|
160
|
+
finally:
|
|
161
|
+
self.num_cpu = old_cpus
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def signature(self):
|
|
129
165
|
"""
|
|
130
|
-
|
|
166
|
+
Get the call signature to run cubegen (as can be passed to Popen etc)
|
|
131
167
|
"""
|
|
132
|
-
|
|
133
|
-
signature = [
|
|
168
|
+
return [
|
|
134
169
|
"{}".format(self.cubegen_executable),
|
|
135
|
-
#
|
|
136
|
-
"
|
|
170
|
+
# Some versions of cubegen respect this num_threads arg, some don't.
|
|
171
|
+
"{}".format(self.num_cpu),
|
|
137
172
|
"{}={}".format(self.cubegen_type, self.orbital),
|
|
138
173
|
str(self.input_file),
|
|
139
174
|
str(self.output),
|
|
140
175
|
str(self.npts)
|
|
141
176
|
]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def env(self):
|
|
180
|
+
"""
|
|
181
|
+
Get the environmental variables require to run cubegen (as can be passed to Popen etc).
|
|
182
|
+
"""
|
|
183
|
+
return dict(
|
|
184
|
+
os.environ,
|
|
185
|
+
GAUSS_MEMDEF = str(self.memory),
|
|
186
|
+
# Some versions of cubegen only respect this way of changing threads.
|
|
187
|
+
OMP_NUM_THREADS = str(self.num_cpu)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def make_files_async(self):
|
|
191
|
+
"""
|
|
192
|
+
Make the files referenced by this object.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
cubegen_proc = await asyncio.create_subprocess_exec(
|
|
196
|
+
# Unless subprocess.run(), create_subprocess_exec() takes and expanded list as first arg...
|
|
197
|
+
*self.signature,
|
|
198
|
+
# Capture both stdout and stderr.
|
|
199
|
+
# It appears that cubegen writes everything (including error messages) to stdout, but it does return meaningful exit codes.
|
|
200
|
+
stdout = subprocess.PIPE,
|
|
201
|
+
stderr = subprocess.STDOUT,
|
|
202
|
+
env = self.env
|
|
203
|
+
)
|
|
204
|
+
except FileNotFoundError:
|
|
205
|
+
raise File_maker_exception(self, "Could not locate cubegen executable '{}'".format(self.cubegen_executable))
|
|
206
|
+
|
|
207
|
+
stdout = ""
|
|
208
|
+
stderr = ""
|
|
209
|
+
# Wait for completion.
|
|
210
|
+
while cubegen_proc.returncode is None:
|
|
211
|
+
retval = await cubegen_proc.communicate()
|
|
212
|
+
stdout += retval[0].decode()
|
|
213
|
+
stderr += retval[1].decode()
|
|
214
|
+
|
|
215
|
+
# If something went wrong, dump output.
|
|
216
|
+
if cubegen_proc.returncode != 0:
|
|
217
|
+
# An error occured.
|
|
218
|
+
# Check if the input file exists, if not this is probably what went wrong.
|
|
219
|
+
message = "Cubegen did not exit successfully"
|
|
220
|
+
if not Path(str(self.input_file)).exists():
|
|
221
|
+
message += " (probably because the input file '{}' could not be found)".format(self.input_file)
|
|
222
|
+
message += ":\n{}".format(stdout)
|
|
223
|
+
raise File_maker_exception(self, message)
|
|
224
|
+
else:
|
|
225
|
+
# Everything appeared to go ok.
|
|
226
|
+
# Dump cubegen output if we're in debug.
|
|
227
|
+
digichem.log.get_logger().debug(stdout)
|
|
142
228
|
|
|
229
|
+
if self.sanitize:
|
|
230
|
+
sanitize_modern_cubes(self.output)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def make_files(self):
|
|
234
|
+
"""
|
|
235
|
+
Make the files referenced by this object.
|
|
236
|
+
"""
|
|
143
237
|
try:
|
|
144
238
|
cubegen_proc = subprocess.run(
|
|
145
|
-
signature,
|
|
239
|
+
self.signature,
|
|
146
240
|
# Capture both stdout and stderr.
|
|
147
241
|
# It appears that cubegen writes everything (including error messages) to stdout, but it does return meaningful exit codes.
|
|
148
242
|
stdout = subprocess.PIPE,
|
|
149
243
|
stderr = subprocess.STDOUT,
|
|
150
244
|
universal_newlines = True,
|
|
151
|
-
env =
|
|
152
|
-
|
|
245
|
+
env = self.env
|
|
246
|
+
)
|
|
153
247
|
except FileNotFoundError:
|
|
154
248
|
raise File_maker_exception(self, "Could not locate cubegen executable '{}'".format(self.cubegen_executable))
|
|
155
249
|
|
|
@@ -193,7 +287,7 @@ class Fchk_to_spin_cube(Fchk_to_cube):
|
|
|
193
287
|
return super().__init__(*args, cubegen_type = "Spin", orbital = spin_density, **kwargs)
|
|
194
288
|
|
|
195
289
|
@classmethod
|
|
196
|
-
def from_options(self, output, *, fchk_file = None, spin_density = "SCF", options, **kwargs):
|
|
290
|
+
def from_options(self, output, *, fchk_file = None, spin_density = "SCF", num_cpu = None, options, **kwargs):
|
|
197
291
|
"""
|
|
198
292
|
Constructor that takes a dictionary of config like options.
|
|
199
293
|
"""
|
|
@@ -205,6 +299,8 @@ class Fchk_to_spin_cube(Fchk_to_cube):
|
|
|
205
299
|
dont_modify = not options['render']['enable_rendering'],
|
|
206
300
|
cubegen_executable = options['external']['cubegen'],
|
|
207
301
|
sanitize = options['render']['safe_cubes'],
|
|
302
|
+
num_cpu = num_cpu,
|
|
303
|
+
multithreading = options['external']['cubegen_parallel'],
|
|
208
304
|
**kwargs
|
|
209
305
|
)
|
|
210
306
|
|
|
@@ -231,7 +327,7 @@ class Fchk_to_density_cube(Fchk_to_cube):
|
|
|
231
327
|
self.type = density_type
|
|
232
328
|
|
|
233
329
|
@classmethod
|
|
234
|
-
def from_options(self, output, *, fchk_file = None, density_type = "SCF", options, **kwargs):
|
|
330
|
+
def from_options(self, output, *, fchk_file = None, density_type = "SCF", num_cpu = None, options, **kwargs):
|
|
235
331
|
"""
|
|
236
332
|
Constructor that takes a dictionary of config like options.
|
|
237
333
|
"""
|
|
@@ -243,6 +339,8 @@ class Fchk_to_density_cube(Fchk_to_cube):
|
|
|
243
339
|
dont_modify = not options['render']['enable_rendering'],
|
|
244
340
|
cubegen_executable = options['external']['cubegen'],
|
|
245
341
|
sanitize = options['render']['safe_cubes'],
|
|
342
|
+
num_cpu = num_cpu,
|
|
343
|
+
multithreading = options['external']['cubegen_parallel'],
|
|
246
344
|
**kwargs
|
|
247
345
|
)
|
|
248
346
|
|
|
@@ -268,7 +366,7 @@ class Fchk_to_nto_cube(Fchk_to_cube):
|
|
|
268
366
|
super().__init__(*args, cubegen_type = "MO", orbital = orbital, **kwargs)
|
|
269
367
|
|
|
270
368
|
@classmethod
|
|
271
|
-
def from_options(self, output, *, fchk_file = None, orbital = "HOMO", options, **kwargs):
|
|
369
|
+
def from_options(self, output, *, fchk_file = None, orbital = "HOMO", num_cpu = None, options, **kwargs):
|
|
272
370
|
"""
|
|
273
371
|
Constructor that takes a dictionary of config like options.
|
|
274
372
|
"""
|
|
@@ -280,5 +378,76 @@ class Fchk_to_nto_cube(Fchk_to_cube):
|
|
|
280
378
|
dont_modify = not options['render']['enable_rendering'],
|
|
281
379
|
cubegen_executable = options['external']['cubegen'],
|
|
282
380
|
sanitize = options['render']['safe_cubes'],
|
|
381
|
+
num_cpu = num_cpu,
|
|
382
|
+
multithreading = options['external']['cubegen_parallel'],
|
|
283
383
|
**kwargs
|
|
284
384
|
)
|
|
385
|
+
|
|
386
|
+
class PySCF_to_cube(File_maker):
|
|
387
|
+
"""
|
|
388
|
+
Generate cubes from a completed PySCF calculation result object.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def __init__(
|
|
392
|
+
self,
|
|
393
|
+
*args,
|
|
394
|
+
mol,
|
|
395
|
+
target,
|
|
396
|
+
target_type = "density",
|
|
397
|
+
npts = 80,
|
|
398
|
+
cube_file = None,
|
|
399
|
+
sanitize = False,
|
|
400
|
+
**kwargs):
|
|
401
|
+
"""
|
|
402
|
+
Constructor for Fchk_to_cube objects.
|
|
403
|
+
|
|
404
|
+
See Image_maker for a full signature.
|
|
405
|
+
|
|
406
|
+
: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).
|
|
407
|
+
:param npts: The number of points per side of the cube.
|
|
408
|
+
: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.
|
|
409
|
+
:param sanitize: Whether to modify the cube file to make it compatible with older software.
|
|
410
|
+
"""
|
|
411
|
+
super().__init__(*args, existing_file = cube_file, **kwargs)
|
|
412
|
+
self.npts = npts
|
|
413
|
+
self.sanitize = sanitize
|
|
414
|
+
self.target_type = target_type
|
|
415
|
+
self.target = target
|
|
416
|
+
self.mol = mol
|
|
417
|
+
# TODO: Add some intelligence to this...
|
|
418
|
+
self.type = "SCF"
|
|
419
|
+
|
|
420
|
+
@classmethod
|
|
421
|
+
def from_options(self, output, *, options, **kwargs):
|
|
422
|
+
"""
|
|
423
|
+
Constructor that takes a dictionary of config like options.
|
|
424
|
+
"""
|
|
425
|
+
return self(
|
|
426
|
+
output,
|
|
427
|
+
npts = options['render']['orbital']['cube_grid_size'].translate("points"),
|
|
428
|
+
dont_modify = not options['render']['enable_rendering'],
|
|
429
|
+
sanitize = options['render']['safe_cubes'],
|
|
430
|
+
**kwargs
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def check_can_make(self):
|
|
434
|
+
super().check_can_make()
|
|
435
|
+
|
|
436
|
+
if cubegen is None:
|
|
437
|
+
raise File_maker_exception(self, "PySCF is not available")
|
|
438
|
+
|
|
439
|
+
def make_files(self):
|
|
440
|
+
"""
|
|
441
|
+
Make the files referenced by this object.
|
|
442
|
+
"""
|
|
443
|
+
if self.target_type == "density":
|
|
444
|
+
cubegen.density(self.mol, self.output, self.target.make_rdm1(), nx=self.npts, ny=self.npts, nz=self.npts)
|
|
445
|
+
|
|
446
|
+
elif self.target_type == "orbital":
|
|
447
|
+
cubegen.orbital(self.mol, self.output, self.target, nx=self.npts, ny=self.npts, nz=self.npts)
|
|
448
|
+
|
|
449
|
+
else:
|
|
450
|
+
raise ValueError("Unrecognised 'target_type': {}".format(self.target_type))
|
|
451
|
+
|
|
452
|
+
if self.sanitize:
|
|
453
|
+
sanitize_modern_cubes(self.output)
|
digichem/file/types.py
CHANGED
|
@@ -74,6 +74,7 @@ gaussian_cube_file = File_type("cube", "gaussian", [".cub", ".cube"])
|
|
|
74
74
|
|
|
75
75
|
orca_gbw_file = File_type("gbw", "orca", [".gbw"])
|
|
76
76
|
orca_density_file = File_type("density", "orca", [".densities"])
|
|
77
|
+
orca_density_info_file = File_type("density-info", "orca", [".densitiesinfo"])
|
|
77
78
|
|
|
78
79
|
# A list of all our known types.
|
|
79
80
|
known_types = [log_file, gaussian_chk_file, gaussian_fchk_file, gaussian_rwf_file, gaussian_cube_file, orca_gbw_file]
|
digichem/image/render.py
CHANGED
|
@@ -10,12 +10,17 @@ import subprocess
|
|
|
10
10
|
import yaml
|
|
11
11
|
from PIL import Image
|
|
12
12
|
import math
|
|
13
|
+
import numpy
|
|
14
|
+
import warnings
|
|
15
|
+
import json
|
|
13
16
|
|
|
17
|
+
import digichem.log
|
|
14
18
|
from digichem.exception.base import File_maker_exception
|
|
15
19
|
from digichem.file.base import File_converter
|
|
16
20
|
from digichem.image.base import Cropable_mixin
|
|
17
21
|
from digichem.datas import get_resource
|
|
18
22
|
|
|
23
|
+
|
|
19
24
|
class Render_maker(File_converter, Cropable_mixin):
|
|
20
25
|
"""
|
|
21
26
|
ABC for classes that make 3D renders from cube files.
|
|
@@ -35,6 +40,7 @@ class Render_maker(File_converter, Cropable_mixin):
|
|
|
35
40
|
resolution = 1024,
|
|
36
41
|
also_make_png = True,
|
|
37
42
|
isovalue = 0.2,
|
|
43
|
+
num_cpu = 1,
|
|
38
44
|
**kwargs):
|
|
39
45
|
"""
|
|
40
46
|
Constructor for Image_maker objects.
|
|
@@ -46,6 +52,7 @@ class Render_maker(File_converter, Cropable_mixin):
|
|
|
46
52
|
:param resolution: The max width or height of the rendered images in pixels.
|
|
47
53
|
:param also_make_png: If True, additional images will be rendered in PNG format. This option is useful to generate higher quality images alongside more portable formats.
|
|
48
54
|
:param isovalue: The isovalue to use for rendering isosurfaces. Has no effect when rendering only atoms.
|
|
55
|
+
:param num_cpu: The number of CPUs for multithreading.
|
|
49
56
|
:param blender_executable:
|
|
50
57
|
"""
|
|
51
58
|
super().__init__(*args, input_file = cube_file, **kwargs)
|
|
@@ -61,6 +68,7 @@ class Render_maker(File_converter, Cropable_mixin):
|
|
|
61
68
|
self.target_resolution = resolution
|
|
62
69
|
self.also_make_png = also_make_png
|
|
63
70
|
self.isovalue = isovalue
|
|
71
|
+
self.num_cpu = num_cpu
|
|
64
72
|
|
|
65
73
|
# TODO: These.
|
|
66
74
|
self.primary_colour = "red"
|
|
@@ -132,11 +140,13 @@ class Batoms_renderer(Render_maker):
|
|
|
132
140
|
rotations = None,
|
|
133
141
|
auto_crop = True,
|
|
134
142
|
resolution = 1024,
|
|
135
|
-
render_samples =
|
|
143
|
+
render_samples = 32,
|
|
144
|
+
stack = 3,
|
|
136
145
|
also_make_png = True,
|
|
137
146
|
isovalue = 0.02,
|
|
138
147
|
blender_executable = None,
|
|
139
148
|
cpus = 1,
|
|
149
|
+
num_cpu = 1,
|
|
140
150
|
perspective = "perspective",
|
|
141
151
|
logging = False,
|
|
142
152
|
**kwargs):
|
|
@@ -149,12 +159,20 @@ class Batoms_renderer(Render_maker):
|
|
|
149
159
|
:param auto_crop: If False, images will not have excess white space cropped.
|
|
150
160
|
:param resolution: The max width or height of the rendered images in pixels.
|
|
151
161
|
:param render_samples: The number of render samples, more results in longer render times but higher quality image.
|
|
162
|
+
:param stack: The number of copies of the image to composite together to avoid transparency artifacts.
|
|
152
163
|
:param also_make_png: If True, additional images will be rendered in PNG format. This option is useful to generate higher quality images alongside more portable formats.
|
|
153
164
|
:param isovalue: The isovalue to use for rendering isosurfaces. Has no effect when rendering only atoms.
|
|
154
165
|
:param blender_executable: Bath to the blender executable (can be None to use a default).
|
|
155
|
-
:param cpus: Number of parallel threads to render with
|
|
166
|
+
:param cpus: DEPREACTED: Number of parallel threads to render with (use num_cpu instead)
|
|
167
|
+
:param num_cpu: Number of parallel threads to render with.
|
|
156
168
|
:param perspective: Perspective mode (orthographic or perspective)
|
|
157
169
|
"""
|
|
170
|
+
if cpus != 1:
|
|
171
|
+
warnings.warn("cpus is deprecated, use num_cpu instead", DeprecationWarning)
|
|
172
|
+
|
|
173
|
+
if num_cpu == 1 and cpus != 1:
|
|
174
|
+
num_cpu = cpus
|
|
175
|
+
|
|
158
176
|
super().__init__(
|
|
159
177
|
*args,
|
|
160
178
|
cube_file = cube_file,
|
|
@@ -163,14 +181,15 @@ class Batoms_renderer(Render_maker):
|
|
|
163
181
|
resolution = resolution,
|
|
164
182
|
also_make_png = also_make_png,
|
|
165
183
|
isovalue = math.fabs(isovalue),
|
|
184
|
+
num_cpu = num_cpu,
|
|
166
185
|
**kwargs
|
|
167
186
|
)
|
|
168
187
|
|
|
169
188
|
# Blender specific options.
|
|
170
189
|
self.render_samples = render_samples
|
|
171
|
-
self.cpus = cpus
|
|
172
190
|
self.perspective = perspective
|
|
173
|
-
|
|
191
|
+
self.stack = stack
|
|
192
|
+
|
|
174
193
|
self.logging = logging
|
|
175
194
|
|
|
176
195
|
# Use explicit blender location if given.
|
|
@@ -182,7 +201,7 @@ class Batoms_renderer(Render_maker):
|
|
|
182
201
|
self.blender_executable = get_resource('data/batoms/blender/blender')
|
|
183
202
|
|
|
184
203
|
@classmethod
|
|
185
|
-
def from_options(self, output, *, cube_file = None, rotations = None, cpus = None, options, **kwargs):
|
|
204
|
+
def from_options(self, output, *, cube_file = None, rotations = None, cpus = None, num_cpu = 1, options, **kwargs):
|
|
186
205
|
"""
|
|
187
206
|
Constructor that takes a dictionary of config like options.
|
|
188
207
|
"""
|
|
@@ -193,17 +212,20 @@ class Batoms_renderer(Render_maker):
|
|
|
193
212
|
auto_crop = options['render']['auto_crop'],
|
|
194
213
|
resolution = options['render']['resolution'],
|
|
195
214
|
render_samples = options['render']['batoms']['render_samples'],
|
|
215
|
+
stack = options['render']['batoms']['stacking'],
|
|
196
216
|
isovalue = options['render'][self.options_name]['isovalue'],
|
|
197
217
|
use_existing = options['render']['use_existing'],
|
|
198
218
|
dont_modify = not options['render']['enable_rendering'],
|
|
199
219
|
blender_executable = options['render']['batoms']['blender'],
|
|
220
|
+
# Deprecated...
|
|
200
221
|
cpus = cpus if cpus is not None else options['render']['batoms']['cpus'],
|
|
222
|
+
num_cpu = num_cpu,
|
|
201
223
|
perspective = options['render']['batoms']['perspective'],
|
|
202
224
|
logging = options['logging']['render_logging'],
|
|
203
225
|
**kwargs
|
|
204
226
|
)
|
|
205
227
|
|
|
206
|
-
def blender_signature(self,
|
|
228
|
+
def blender_signature(self, *targets, padding = 1.0):
|
|
207
229
|
"""
|
|
208
230
|
The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
|
|
209
231
|
"""
|
|
@@ -217,44 +239,77 @@ class Batoms_renderer(Render_maker):
|
|
|
217
239
|
"--",
|
|
218
240
|
# Script specific args.
|
|
219
241
|
f"{self.input_file}",
|
|
220
|
-
f"{output}",
|
|
242
|
+
# f"{output}",
|
|
221
243
|
# Keywords.
|
|
222
|
-
"--cpus", f"{self.
|
|
223
|
-
"--orientation", "{}".format(orientation[0]), "{}".format(orientation[1]), "{}".format(orientation[2]),
|
|
224
|
-
"--resolution", f"{resolution}",
|
|
225
|
-
"--render-samples", f"{samples}",
|
|
244
|
+
"--cpus", f"{self.num_cpu}",
|
|
245
|
+
# "--orientation", "{}".format(orientation[0]), "{}".format(orientation[1]), "{}".format(orientation[2]),
|
|
246
|
+
# "--resolution", f"{resolution}",
|
|
247
|
+
# "--render-samples", f"{samples}",
|
|
226
248
|
"--perspective", f"{self.perspective}",
|
|
227
249
|
"--padding", f"{padding}",
|
|
228
250
|
"--rotations",
|
|
229
251
|
]
|
|
252
|
+
for orientation, resolution, samples, mini_file_name in targets:
|
|
253
|
+
args.extend([
|
|
254
|
+
"--multi",
|
|
255
|
+
"{}".format(orientation[0]), "{}".format(orientation[1]), "{}".format(orientation[2]),
|
|
256
|
+
f"{resolution}",
|
|
257
|
+
f"{samples}",
|
|
258
|
+
mini_file_name
|
|
259
|
+
])
|
|
260
|
+
|
|
230
261
|
# Add rotations.
|
|
231
262
|
for rotation in self.rotations:
|
|
232
|
-
args.append(
|
|
263
|
+
args.append(json.dumps(rotation))
|
|
233
264
|
|
|
234
265
|
return args
|
|
235
266
|
|
|
236
|
-
def run_blender(self, output, resolution, samples, orientation):
|
|
267
|
+
#def run_blender(self, output, resolution, samples, orientation):
|
|
268
|
+
|
|
269
|
+
def run_blender(self, *targets):
|
|
270
|
+
"""
|
|
271
|
+
Render a (number of) images with blender.
|
|
272
|
+
|
|
273
|
+
:param samples: How many render samples to use.
|
|
274
|
+
:param *targets: Images to render, each is a tuple if (orientation, resolution, samples, path).
|
|
275
|
+
"""
|
|
237
276
|
# Render with batoms.
|
|
238
277
|
env = dict(os.environ)
|
|
239
278
|
|
|
240
279
|
# Disabling the user dir helps prevent conflicting installs of certain packages
|
|
241
280
|
env["PYTHONNOUSERSITE"] = "1"
|
|
242
281
|
#env["PYTHONPATH"] = ":" + env["PYTHONPATH"]
|
|
282
|
+
blender_process = None
|
|
243
283
|
|
|
244
284
|
# Run Blender, which renders our image for us.
|
|
245
285
|
try:
|
|
246
|
-
subprocess.run(
|
|
247
|
-
self.blender_signature(
|
|
286
|
+
blender_process = subprocess.run(
|
|
287
|
+
self.blender_signature(*targets),
|
|
248
288
|
stdin = subprocess.DEVNULL,
|
|
249
|
-
stdout = subprocess.
|
|
289
|
+
stdout = subprocess.PIPE if not self.logging else None,
|
|
250
290
|
stderr = subprocess.STDOUT,
|
|
251
291
|
universal_newlines = True,
|
|
252
292
|
check = True,
|
|
253
293
|
env = env,
|
|
254
294
|
)
|
|
295
|
+
|
|
296
|
+
for target in targets:
|
|
297
|
+
if not target[3].exists():
|
|
298
|
+
raise File_maker_exception(self, "Could not find render file '{}'".format(target[3]))
|
|
299
|
+
|
|
255
300
|
except FileNotFoundError:
|
|
256
301
|
raise File_maker_exception(self, "Could not locate blender executable '{}'".format(self.blender_executable))
|
|
257
302
|
|
|
303
|
+
except subprocess.CalledProcessError as e:
|
|
304
|
+
if not self.logging:
|
|
305
|
+
digichem.log.get_logger().error("Blender did not exit successfully, dumping output:\n{}".format(e.stdout))
|
|
306
|
+
|
|
307
|
+
except Exception:
|
|
308
|
+
if not self.logging and blender_process is not None:
|
|
309
|
+
digichem.log.get_logger().error("Blender did not exit successfully, dumping output:\n{}".format(blender_process.stdout))
|
|
310
|
+
|
|
311
|
+
raise
|
|
312
|
+
|
|
258
313
|
|
|
259
314
|
def make_files(self):
|
|
260
315
|
"""
|
|
@@ -263,23 +318,42 @@ class Batoms_renderer(Render_maker):
|
|
|
263
318
|
The new image will be written to file.
|
|
264
319
|
"""
|
|
265
320
|
# TODO: This mechanism is clunky and inefficient if only one image is needed because its based off the old VMD renderer. With batoms we can do much better.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
angles = {
|
|
322
|
+
"x0y0z0": [
|
|
323
|
+
(0,0,0),
|
|
324
|
+
self.test_resolution if self.auto_crop else self.target_resolution,
|
|
325
|
+
self.test_samples if self.auto_crop else self.render_samples,
|
|
326
|
+
self.file_path['x0y0z0'].with_suffix(".tmp.png")
|
|
327
|
+
],
|
|
328
|
+
"x90y0z0": [
|
|
329
|
+
(1.5708, 0, 0),
|
|
330
|
+
self.test_resolution if self.auto_crop else self.target_resolution,
|
|
331
|
+
self.test_samples if self.auto_crop else self.render_samples,
|
|
332
|
+
self.file_path['x90y0z0'].with_suffix(".tmp.png")
|
|
333
|
+
],
|
|
334
|
+
"x0y90z0": [
|
|
335
|
+
(0, 1.5708, 0),
|
|
336
|
+
self.test_resolution if self.auto_crop else self.target_resolution,
|
|
337
|
+
self.test_samples if self.auto_crop else self.render_samples,
|
|
338
|
+
self.file_path['x0y90z0'].with_suffix(".tmp.png")
|
|
339
|
+
],
|
|
340
|
+
"x45y45z45": [
|
|
341
|
+
(0.785398, 0.785398, 0.785398),
|
|
342
|
+
self.test_resolution if self.auto_crop else self.target_resolution,
|
|
343
|
+
self.test_samples if self.auto_crop else self.render_samples,
|
|
344
|
+
self.file_path['x45y45z45'].with_suffix(".tmp.png")
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
# First we'll render a test image at a lower resolution. We'll then crop it, and use the decrease in final resolution to know how much bigger we need to render in our final image to hit our target resolution.
|
|
350
|
+
# Unless of course auto_crop is False, in which case we use our target resolution immediately.
|
|
351
|
+
self.run_blender(*list(angles.values()))
|
|
352
|
+
|
|
353
|
+
if self.auto_crop:
|
|
354
|
+
# Load the test image and autocrop it.
|
|
355
|
+
for angle, target in angles.items():
|
|
356
|
+
with Image.open(target[3], "r") as test_im:
|
|
283
357
|
small_test_im = self.auto_crop_image(test_im)
|
|
284
358
|
|
|
285
359
|
# Get the cropped size. We're interested in the largest dimension, as this is what we'll output as.
|
|
@@ -287,15 +361,28 @@ class Batoms_renderer(Render_maker):
|
|
|
287
361
|
|
|
288
362
|
# From this we can work out the ratio between our true resolution and the resolution we've been asked for.
|
|
289
363
|
resolution_ratio = cropped_resolution / self.test_resolution
|
|
364
|
+
|
|
365
|
+
# Update the target properties.
|
|
366
|
+
angles[angle] = [
|
|
367
|
+
# Orientation is the same.
|
|
368
|
+
angles[angle][0],
|
|
369
|
+
# New resolution.
|
|
370
|
+
int(self.target_resolution / resolution_ratio),
|
|
371
|
+
# New samples.
|
|
372
|
+
self.render_samples,
|
|
373
|
+
# New filename,
|
|
374
|
+
angles[angle][3]
|
|
375
|
+
]
|
|
290
376
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
except Exception:
|
|
294
|
-
raise File_maker_exception(self, "Error in blender rendering")
|
|
377
|
+
self.run_blender(*list(angles.values()))
|
|
295
378
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
379
|
+
except Exception:
|
|
380
|
+
raise File_maker_exception(self, "Error in blender rendering")
|
|
381
|
+
|
|
382
|
+
# Convert to a better set of formats.
|
|
383
|
+
# Open the files we just rendered.
|
|
384
|
+
for angle, target in angles.items():
|
|
385
|
+
with Image.open(target[3], "r") as im:
|
|
299
386
|
|
|
300
387
|
# If we've been asked to autocrop, do so.
|
|
301
388
|
if self.auto_crop:
|
|
@@ -305,10 +392,21 @@ class Batoms_renderer(Render_maker):
|
|
|
305
392
|
raise File_maker_exception(self, "Error in post-rendering auto-crop")
|
|
306
393
|
else:
|
|
307
394
|
cropped_image = im
|
|
395
|
+
|
|
396
|
+
# Transparency stacking.
|
|
397
|
+
# This 'hack' takes several copies of the same transparent image and layers them atop each other.
|
|
398
|
+
# This avoids problems with isosurfaces being too transparent with respect to the background (and
|
|
399
|
+
# thus loosing definition).
|
|
400
|
+
stacked = cropped_image.copy()
|
|
401
|
+
|
|
402
|
+
for _ in range(self.stack):
|
|
403
|
+
stacked.alpha_composite(cropped_image)
|
|
404
|
+
|
|
405
|
+
cropped_image = stacked
|
|
308
406
|
|
|
309
407
|
# Save as a higher quality png if we've been asked to.
|
|
310
408
|
if self.also_make_png:
|
|
311
|
-
cropped_image.save(self.file_path[
|
|
409
|
+
cropped_image.save(self.file_path[angle + "_big"])
|
|
312
410
|
|
|
313
411
|
# Remove transparency, which isn't supported by JPEG (which is essentially the only format we write here).
|
|
314
412
|
# TODO: Check if the output format can support transparency or not.
|
|
@@ -317,10 +415,10 @@ class Batoms_renderer(Render_maker):
|
|
|
317
415
|
cropped_image = new_image.convert('RGB')
|
|
318
416
|
|
|
319
417
|
# Now save in our main output format.
|
|
320
|
-
cropped_image.save(
|
|
418
|
+
cropped_image.save(self.file_path[angle])
|
|
321
419
|
|
|
322
420
|
# And delete the old .png.
|
|
323
|
-
os.remove(
|
|
421
|
+
os.remove(target[3])
|
|
324
422
|
|
|
325
423
|
|
|
326
424
|
class Structure_image_maker(Batoms_renderer):
|
|
@@ -540,8 +638,9 @@ class Dipole_image_maker(Structure_image_maker):
|
|
|
540
638
|
sig = super().blender_signature(output, resolution, samples, orientation)
|
|
541
639
|
sig.append("--dipoles")
|
|
542
640
|
for dipole in self.get_dipoles():
|
|
543
|
-
sig.append(yaml.safe_dump(dipole))
|
|
544
|
-
|
|
641
|
+
#sig.append(yaml.safe_dump(dipole))
|
|
642
|
+
sig.append(json.dumps(dipole))
|
|
643
|
+
sig.extend(["--alpha", "0.75"])
|
|
545
644
|
return sig
|
|
546
645
|
|
|
547
646
|
|