digichem-core 6.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. digichem/__init__.py +75 -0
  2. digichem/basis.py +116 -0
  3. digichem/config/README +3 -0
  4. digichem/config/__init__.py +5 -0
  5. digichem/config/base.py +321 -0
  6. digichem/config/locations.py +14 -0
  7. digichem/config/parse.py +90 -0
  8. digichem/config/util.py +117 -0
  9. digichem/data/README +4 -0
  10. digichem/data/batoms/COPYING +18 -0
  11. digichem/data/batoms/LICENSE +674 -0
  12. digichem/data/batoms/README +2 -0
  13. digichem/data/batoms/__init__.py +0 -0
  14. digichem/data/batoms/batoms-renderer.py +351 -0
  15. digichem/data/config/digichem.yaml +714 -0
  16. digichem/data/functionals.csv +15 -0
  17. digichem/data/solvents.csv +185 -0
  18. digichem/data/tachyon/COPYING.md +5 -0
  19. digichem/data/tachyon/LICENSE +30 -0
  20. digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
  21. digichem/data/vmd/common.tcl +468 -0
  22. digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
  23. digichem/data/vmd/generate_density_images.tcl +45 -0
  24. digichem/data/vmd/generate_dipole_images.tcl +68 -0
  25. digichem/data/vmd/generate_orbital_images.tcl +57 -0
  26. digichem/data/vmd/generate_spin_images.tcl +66 -0
  27. digichem/data/vmd/generate_structure_images.tcl +40 -0
  28. digichem/datas.py +14 -0
  29. digichem/exception/__init__.py +7 -0
  30. digichem/exception/base.py +133 -0
  31. digichem/exception/uncatchable.py +63 -0
  32. digichem/file/__init__.py +1 -0
  33. digichem/file/base.py +364 -0
  34. digichem/file/cube.py +284 -0
  35. digichem/file/fchk.py +94 -0
  36. digichem/file/prattle.py +277 -0
  37. digichem/file/types.py +97 -0
  38. digichem/image/__init__.py +6 -0
  39. digichem/image/base.py +113 -0
  40. digichem/image/excited_states.py +335 -0
  41. digichem/image/graph.py +293 -0
  42. digichem/image/orbitals.py +239 -0
  43. digichem/image/render.py +617 -0
  44. digichem/image/spectroscopy.py +797 -0
  45. digichem/image/structure.py +115 -0
  46. digichem/image/vmd.py +826 -0
  47. digichem/input/__init__.py +3 -0
  48. digichem/input/base.py +78 -0
  49. digichem/input/digichem_input.py +500 -0
  50. digichem/input/gaussian.py +140 -0
  51. digichem/log.py +179 -0
  52. digichem/memory.py +166 -0
  53. digichem/misc/__init__.py +4 -0
  54. digichem/misc/argparse.py +44 -0
  55. digichem/misc/base.py +61 -0
  56. digichem/misc/io.py +239 -0
  57. digichem/misc/layered_dict.py +285 -0
  58. digichem/misc/text.py +139 -0
  59. digichem/misc/time.py +73 -0
  60. digichem/parse/__init__.py +13 -0
  61. digichem/parse/base.py +220 -0
  62. digichem/parse/cclib.py +138 -0
  63. digichem/parse/dump.py +253 -0
  64. digichem/parse/gaussian.py +130 -0
  65. digichem/parse/orca.py +96 -0
  66. digichem/parse/turbomole.py +201 -0
  67. digichem/parse/util.py +523 -0
  68. digichem/result/__init__.py +6 -0
  69. digichem/result/alignment/AA.py +114 -0
  70. digichem/result/alignment/AAA.py +61 -0
  71. digichem/result/alignment/FAP.py +148 -0
  72. digichem/result/alignment/__init__.py +3 -0
  73. digichem/result/alignment/base.py +310 -0
  74. digichem/result/angle.py +153 -0
  75. digichem/result/atom.py +742 -0
  76. digichem/result/base.py +258 -0
  77. digichem/result/dipole_moment.py +332 -0
  78. digichem/result/emission.py +402 -0
  79. digichem/result/energy.py +323 -0
  80. digichem/result/excited_state.py +821 -0
  81. digichem/result/ground_state.py +94 -0
  82. digichem/result/metadata.py +644 -0
  83. digichem/result/multi.py +98 -0
  84. digichem/result/nmr.py +1086 -0
  85. digichem/result/orbital.py +647 -0
  86. digichem/result/result.py +244 -0
  87. digichem/result/soc.py +272 -0
  88. digichem/result/spectroscopy.py +514 -0
  89. digichem/result/tdm.py +267 -0
  90. digichem/result/vibration.py +167 -0
  91. digichem/test/__init__.py +6 -0
  92. digichem/test/conftest.py +4 -0
  93. digichem/test/test_basis.py +71 -0
  94. digichem/test/test_calculate.py +30 -0
  95. digichem/test/test_config.py +78 -0
  96. digichem/test/test_cube.py +369 -0
  97. digichem/test/test_exception.py +16 -0
  98. digichem/test/test_file.py +104 -0
  99. digichem/test/test_image.py +337 -0
  100. digichem/test/test_input.py +64 -0
  101. digichem/test/test_parsing.py +79 -0
  102. digichem/test/test_prattle.py +36 -0
  103. digichem/test/test_result.py +489 -0
  104. digichem/test/test_translate.py +112 -0
  105. digichem/test/util.py +207 -0
  106. digichem/translate.py +591 -0
  107. digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
  108. digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
  109. digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
  110. digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
  111. digichem_core-6.0.0rc1.dist-info/licenses/LICENSE +11 -0
@@ -0,0 +1,617 @@
1
+ """
2
+ Classes for rendering 3D images of molecules and densities, primarily with blender and beautiful atoms.
3
+
4
+ Also see vmd.py for an older render engine.
5
+ """
6
+ from pathlib import Path
7
+ import os
8
+ import copy
9
+ import subprocess
10
+ import yaml
11
+ from PIL import Image
12
+ import math
13
+
14
+ from digichem.exception.base import File_maker_exception
15
+ from digichem.file.base import File_converter
16
+ from digichem.image.base import Cropable_mixin
17
+ from digichem.datas import get_resource
18
+
19
+ class Render_maker(File_converter, Cropable_mixin):
20
+ """
21
+ ABC for classes that make 3D renders from cube files.
22
+ """
23
+
24
+ # Text description of our input file type, used for error messages etc. This can be changed by inheriting classes.
25
+ input_file_type = "cube"
26
+ # Text description of our output file type, used for error messages etc. This can be changed by inheriting classes.
27
+ output_file_type = "render"
28
+
29
+ def __init__(
30
+ self,
31
+ *args,
32
+ cube_file = None,
33
+ rotations = None,
34
+ auto_crop = True,
35
+ resolution = 1024,
36
+ also_make_png = True,
37
+ isovalue = 0.2,
38
+ **kwargs):
39
+ """
40
+ Constructor for Image_maker objects.
41
+
42
+ :param output: The path to write image files to. As more than one image can be created by this class, this name is used as the base name and is modified for each image. The suffix will be honoured (rendered imgaes will use it as a hint for their output format).
43
+ :param cube_file: The path to a cube_file to use to render images.
44
+ :param rotations: A list of tuples of rotations, where the first index in the tuple specifies the axis to rotate about and the second is the angle to rotate (in radians).
45
+ :param auto_crop: If False, images will not have excess white space cropped.
46
+ :param resolution: The max width or height of the rendered images in pixels.
47
+ :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
+ :param isovalue: The isovalue to use for rendering isosurfaces. Has no effect when rendering only atoms.
49
+ :param blender_executable:
50
+ """
51
+ super().__init__(*args, input_file = cube_file, **kwargs)
52
+ # Save our translations list.
53
+ #self.translations = translations if translations is not None else (0,0,0)
54
+ # This is not used, why not?
55
+ self.translations = (0,0,0)
56
+ # Save our rotations list.
57
+ self._rotations = rotations if rotations is not None else []
58
+
59
+ # Some options that control how we function.
60
+ self.auto_crop = auto_crop
61
+ self.target_resolution = resolution
62
+ self.also_make_png = also_make_png
63
+ self.isovalue = isovalue
64
+
65
+ # TODO: These.
66
+ self.primary_colour = "red"
67
+ self.secondary_colour = "blue"
68
+
69
+ # These 4 attributes are file paths to the four images we create.
70
+ # We'll keep the same file extension as the was given to us.
71
+ self.file_path = {
72
+ 'x0y0z0': self.output.with_suffix(".x0y0z0" + self.output.suffix),
73
+ 'x90y0z0': self.output.with_suffix(".x90y0z0" + self.output.suffix),
74
+ 'x0y90z0': self.output.with_suffix(".x0y90z0" + self.output.suffix),
75
+ 'x45y45z45': self.output.with_suffix(".x45y45z45" + self.output.suffix),
76
+ # TODO: This should respect also_make_png, but works fine for now (these are actually unused).
77
+ # There are higher quality PNG versions.
78
+ 'x0y0z0_big': self.output.with_suffix(".x0y0z0.png"),
79
+ 'x90y0z0_big': self.output.with_suffix(".x90y0z0.png"),
80
+ 'x0y90z0_big': self.output.with_suffix(".x0y90z0.png"),
81
+ 'x45y45z45_big': self.output.with_suffix(".x45y45z45.png")
82
+ }
83
+
84
+ def get_image(self, name = 'file'):
85
+ """
86
+ Get the path to one of the images that this class represents, rendering the image to file first if necessary.
87
+
88
+ The functioning of this method is controlled by the dont_modify & use_existing flags.
89
+
90
+ You can also use the normal python attribute mechanism (either through getattr() or dot notation) to get these paths.
91
+
92
+ :raises KeyError: If name is not the name of one of the images this class represents.
93
+ :param name: The name of an image to get. Depends on the images in self.file_path.
94
+ :return: A pathlib Path object pointing to the image represented by name, or None if no image could be created.
95
+ """
96
+ return self.safe_get_file(name)
97
+
98
+ @property
99
+ def rotations(self):
100
+ # Digichem rotates the wrong way round for some reason, reverse for our rendering engine.
101
+ return [(axis, theta) for axis, theta in self._rotations]
102
+
103
+
104
+ class Batoms_renderer(Render_maker):
105
+ """
106
+ Class for rendering images with Beautiful Atoms and Blender.
107
+ """
108
+
109
+ # Text description of our input file type, used for error messages etc. This can be changed by inheriting classes.
110
+ input_file_type = "cube"
111
+ # Text description of our output file type, used for error messages etc. This can be changed by inheriting classes.
112
+ output_file_type = "render"
113
+
114
+ # TODO: Do we need this? Can probably remove if/when VMD is dropped.
115
+ # Name of the section where we get some specific configs.
116
+ options_name = "orbital"
117
+
118
+ test_resolution = 300
119
+ test_samples = 5
120
+
121
+ @property
122
+ def batoms_script_path(self):
123
+ """
124
+ Get the file path to the VMD script to be used by this class to render images (as a pathlib Path).
125
+ """
126
+ return get_resource('data/batoms/batoms-renderer.py')
127
+
128
+ def __init__(
129
+ self,
130
+ *args,
131
+ cube_file = None,
132
+ rotations = None,
133
+ auto_crop = True,
134
+ resolution = 1024,
135
+ render_samples = 256,
136
+ also_make_png = True,
137
+ isovalue = 0.02,
138
+ blender_executable = None,
139
+ cpus = 1,
140
+ perspective = "perspective",
141
+ logging = False,
142
+ **kwargs):
143
+ """
144
+ Constructor for Image_maker objects.
145
+
146
+ :param output: The path to write image files to. As more than one image can be created by this class, this name is used as the base name and is modified for each image. The suffix will be honoured (rendered imgaes will use it as a hint for their output format).
147
+ :param cube_file: The path to a cube_file to use to render images.
148
+ :param rotations: A list of tuples of rotations, where the first index in the tuple specifies the axis to rotate about and the second is the angle to rotate (in radians).
149
+ :param auto_crop: If False, images will not have excess white space cropped.
150
+ :param resolution: The max width or height of the rendered images in pixels.
151
+ :param render_samples: The number of render samples, more results in longer render times but higher quality image.
152
+ :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
+ :param isovalue: The isovalue to use for rendering isosurfaces. Has no effect when rendering only atoms.
154
+ :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.
156
+ :param perspective: Perspective mode (orthographic or perspective)
157
+ """
158
+ super().__init__(
159
+ *args,
160
+ cube_file = cube_file,
161
+ rotations = rotations,
162
+ auto_crop = auto_crop,
163
+ resolution = resolution,
164
+ also_make_png = also_make_png,
165
+ isovalue = math.fabs(isovalue),
166
+ **kwargs
167
+ )
168
+
169
+ # Blender specific options.
170
+ self.render_samples = render_samples
171
+ self.cpus = cpus
172
+ self.perspective = perspective
173
+
174
+ self.logging = logging
175
+
176
+ # Use explicit blender location if given.
177
+ if blender_executable is not None:
178
+ self.blender_executable = blender_executable
179
+
180
+ else:
181
+ # Otherwise, use a default location.
182
+ self.blender_executable = get_resource('data/batoms/blender/blender')
183
+
184
+ @classmethod
185
+ def from_options(self, output, *, cube_file = None, rotations = None, cpus = None, options, **kwargs):
186
+ """
187
+ Constructor that takes a dictionary of config like options.
188
+ """
189
+ return self(
190
+ output,
191
+ cube_file = cube_file,
192
+ rotations = rotations,
193
+ auto_crop = options['render']['auto_crop'],
194
+ resolution = options['render']['resolution'],
195
+ render_samples = options['render']['batoms']['render_samples'],
196
+ isovalue = options['render'][self.options_name]['isovalue'],
197
+ use_existing = options['render']['use_existing'],
198
+ dont_modify = not options['render']['enable_rendering'],
199
+ blender_executable = options['render']['batoms']['blender'],
200
+ cpus = cpus if cpus is not None else options['render']['batoms']['cpus'],
201
+ perspective = options['render']['batoms']['perspective'],
202
+ logging = options['logging']['render_logging'],
203
+ **kwargs
204
+ )
205
+
206
+ def blender_signature(self, output, resolution, samples, orientation, padding = 1.0):
207
+ """
208
+ The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
209
+ """
210
+ args = [
211
+ # Blender args.
212
+ f"{self.blender_executable}",
213
+ # Run in background.
214
+ "-b",
215
+ # Our script.
216
+ "-P", f"{self.batoms_script_path}",
217
+ "--",
218
+ # Script specific args.
219
+ f"{self.input_file}",
220
+ f"{output}",
221
+ # Keywords.
222
+ "--cpus", f"{self.cpus}",
223
+ "--orientation", "{}".format(orientation[0]), "{}".format(orientation[1]), "{}".format(orientation[2]),
224
+ "--resolution", f"{resolution}",
225
+ "--render-samples", f"{samples}",
226
+ "--perspective", f"{self.perspective}",
227
+ "--padding", f"{padding}",
228
+ "--rotations",
229
+ ]
230
+ # Add rotations.
231
+ for rotation in self.rotations:
232
+ args.append(yaml.safe_dump(rotation))
233
+
234
+ return args
235
+
236
+ def run_blender(self, output, resolution, samples, orientation):
237
+ # Render with batoms.
238
+ env = dict(os.environ)
239
+
240
+ # Disabling the user dir helps prevent conflicting installs of certain packages
241
+ env["PYTHONNOUSERSITE"] = "1"
242
+ #env["PYTHONPATH"] = ":" + env["PYTHONPATH"]
243
+
244
+ # Run Blender, which renders our image for us.
245
+ try:
246
+ subprocess.run(
247
+ self.blender_signature(output, resolution, samples, orientation),
248
+ stdin = subprocess.DEVNULL,
249
+ stdout = subprocess.DEVNULL if not self.logging else None,
250
+ stderr = subprocess.STDOUT,
251
+ universal_newlines = True,
252
+ check = True,
253
+ env = env,
254
+ )
255
+ except FileNotFoundError:
256
+ raise File_maker_exception(self, "Could not locate blender executable '{}'".format(self.blender_executable))
257
+
258
+
259
+ def make_files(self):
260
+ """
261
+ Make the image files referenced by this object.
262
+
263
+ The new image will be written to file.
264
+ """
265
+ # 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
+ for image_name, orientation in [
267
+ ('x0y0z0', (0,0,1)),
268
+ ('x90y0z0', (1,0,0)),
269
+ ('x0y90z0', (0,1,0)),
270
+ ('x45y45z45',(1,1,1))
271
+ ]:
272
+ image_path = self.file_path[image_name]
273
+ try:
274
+ # 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.
275
+ # Unless of course auto_crop is False, in which case we use our target resolution immediately.
276
+ resolution = self.test_resolution if self.auto_crop else self.target_resolution
277
+ samples = self.test_samples if self.auto_crop else self.render_samples
278
+ self.run_blender(image_path.with_suffix(".tmp.png"), resolution, samples, orientation)
279
+
280
+ if self.auto_crop:
281
+ # Load the test image and autocrop it.
282
+ with Image.open(image_path.with_suffix(".tmp.png"), "r") as test_im:
283
+ small_test_im = self.auto_crop_image(test_im)
284
+
285
+ # Get the cropped size. We're interested in the largest dimension, as this is what we'll output as.
286
+ cropped_resolution = max(small_test_im.size)
287
+
288
+ # From this we can work out the ratio between our true resolution and the resolution we've been asked for.
289
+ resolution_ratio = cropped_resolution / self.test_resolution
290
+
291
+ self.run_blender(image_path.with_suffix(".tmp.png"), int(self.target_resolution / resolution_ratio), self.render_samples, orientation)
292
+
293
+ except Exception:
294
+ raise File_maker_exception(self, "Error in blender rendering")
295
+
296
+ # Convert to a better set of formats.
297
+ # Open the file we just rendered.
298
+ with Image.open(image_path.with_suffix(".tmp.png"), "r") as im:
299
+
300
+ # If we've been asked to autocrop, do so.
301
+ if self.auto_crop:
302
+ try:
303
+ cropped_image = self.auto_crop_image(im)
304
+ except Exception:
305
+ raise File_maker_exception(self, "Error in post-rendering auto-crop")
306
+ else:
307
+ cropped_image = im
308
+
309
+ # Save as a higher quality png if we've been asked to.
310
+ if self.also_make_png:
311
+ cropped_image.save(self.file_path[image_name + "_big"])
312
+
313
+ # Remove transparency, which isn't supported by JPEG (which is essentially the only format we write here).
314
+ # TODO: Check if the output format can support transparency or not.
315
+ new_image = Image.new("RGBA", cropped_image.size, "WHITE") # Create a white rgba background
316
+ new_image.paste(cropped_image, (0, 0), cropped_image) # Paste the image on the background. Go to the links given below for details.
317
+ cropped_image = new_image.convert('RGB')
318
+
319
+ # Now save in our main output format.
320
+ cropped_image.save(image_path)
321
+
322
+ # And delete the old .png.
323
+ os.remove(image_path.with_suffix(".tmp.png"))
324
+
325
+
326
+ class Structure_image_maker(Batoms_renderer):
327
+ """
328
+ Class for creating structure images.
329
+ """
330
+
331
+
332
+ class Orbital_image_maker(Structure_image_maker):
333
+ """
334
+ Class for creating orbital images.
335
+ """
336
+
337
+ def blender_signature(self, output, resolution, samples, orientation, isotype = "both", isocolor = "sign"):
338
+ """
339
+ The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
340
+ """
341
+ sig = super().blender_signature(output, resolution, samples, orientation)
342
+ sig.extend(["--isovalues", f"{self.isovalue}"])
343
+ sig.extend(["--isotype", isotype])
344
+ sig.extend(["--isocolor", isocolor])
345
+
346
+ return sig
347
+
348
+
349
+ class Difference_density_image_maker(Orbital_image_maker):
350
+
351
+ # Name of the section where we get some specific configs.
352
+ options_name = "difference_density"
353
+
354
+
355
+ class Spin_density_image_maker(Orbital_image_maker):
356
+ """
357
+ Class for creating spin density images.
358
+ """
359
+
360
+ # Name of the section where we get some specific configs.
361
+ options_name = "spin"
362
+
363
+ def __init__(self, *args, spin = "both", **kwargs):
364
+ """
365
+ Constructor for Spin_density_image_maker objects.
366
+
367
+ See Orbital_image_maker for a complete constructor.
368
+ :param spin: A string indicating which net-spins to render, either 'positive', 'negative' or 'both'.
369
+ """
370
+ super().__init__(*args, **kwargs)
371
+ self.spin = spin
372
+
373
+ def blender_signature(self, output, resolution, samples, orientation):
374
+ """
375
+ The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
376
+ """
377
+ sig = super().blender_signature(output, resolution, samples, orientation, isotype = self.spin)
378
+ return sig
379
+
380
+
381
+ class Alpha_orbital_image_maker(Orbital_image_maker):
382
+ pass
383
+
384
+
385
+ class Beta_orbital_image_maker(Orbital_image_maker):
386
+ pass
387
+
388
+
389
+ class Density_image_maker(Orbital_image_maker):
390
+ """
391
+ Class for creating spin density images.
392
+ """
393
+
394
+ # Name of the section where we get some specific configs.
395
+ options_name = "density"
396
+
397
+ @property
398
+ def type(self):
399
+ """
400
+ The density type.
401
+ """
402
+ return self.input_file.type
403
+
404
+ def blender_signature(self, output, resolution, samples, orientation):
405
+ """
406
+ The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
407
+ """
408
+ # There is no negative density for total density.
409
+ sig = super().blender_signature(output, resolution, samples, orientation, isotype = "positive")
410
+ return sig
411
+
412
+
413
+ class Combined_orbital_image_maker(Orbital_image_maker):
414
+ """
415
+ Class for creating images with both the HOMO and LUMO shown together.
416
+ """
417
+
418
+ vmd_script ="generate_combined_orbital_images.tcl"
419
+
420
+ def __init__(self, *args, cube_file = None, LUMO_cube_file = None, **kwargs):
421
+ """
422
+ Constructor for combined orbital image maker objects.
423
+
424
+ :param output: Path to write to. See the constructor for Batoms_renderer for how this works.
425
+ :param HOMO_cube_file: Path to the HOMO cube file.
426
+ :param LUMO_cube_file: Path to the LUMO cube file.
427
+ :param *args: See the constructor for Batoms_renderer for further options.
428
+ :param **kwargs: See the constructor for VMD_image_maker for further options.
429
+ """
430
+ super().__init__(*args, cube_file = cube_file, **kwargs)
431
+ self.LUMO_cube_file = LUMO_cube_file
432
+
433
+ @property
434
+ def HOMO_cube_file(self):
435
+ return self.input_file
436
+
437
+ @classmethod
438
+ def from_options(self, output, *, HOMO_cube_file = None, LUMO_cube_file = None, options, **kwargs):
439
+ """
440
+ Constructor that takes a dictionary of config like options.
441
+ """
442
+ return super().from_options(
443
+ output,
444
+ cube_file = HOMO_cube_file,
445
+ LUMO_cube_file = LUMO_cube_file,
446
+ options = options,
447
+ **kwargs
448
+ )
449
+
450
+ def check_can_make(self):
451
+ """
452
+ Check whether it is feasible to try and render the image(s) that we represent.
453
+
454
+ Reasons for rendering not being possible are varied and are up to the inheriting class, but include eg, a required input (cube, fchk) file not being given.
455
+
456
+ This method returns nothing, but will raise a File_maker_exception exception if the rendering is not possible.
457
+ """
458
+ try:
459
+ if self.HOMO_cube_file is None or self.HOMO_cube_file.safe_get_file() is None:
460
+ raise File_maker_exception(self, "No HOMO cube file is available")
461
+ except AttributeError:
462
+ if not hasattr(self.HOMO_cube_file, 'safe_get_file'):
463
+ # Input file does not have a safe_get_file() method.
464
+ pass
465
+
466
+ else:
467
+ raise
468
+
469
+ try:
470
+ if self.LUMO_cube_file is None or self.LUMO_cube_file.safe_get_file() is None:
471
+ raise File_maker_exception(self, "No LUMO cube file is available")
472
+
473
+ except AttributeError:
474
+ if not hasattr(self.LUMO_cube_file, 'safe_get_file'):
475
+ # Input file does not have a safe_get_file() method.
476
+ pass
477
+
478
+ else:
479
+ raise
480
+
481
+ def blender_signature(self, output, resolution, samples, orientation):
482
+ """
483
+ The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
484
+ """
485
+ sig = super().blender_signature(output, resolution, samples, orientation, isotype = "both", isocolor = "cube")
486
+ sig.extend(["--second_cube", f"{self.LUMO_cube_file}"])
487
+ return sig
488
+
489
+
490
+ class Dipole_image_maker(Structure_image_maker):
491
+ """
492
+ Class for creating dipole images.
493
+ """
494
+
495
+
496
+ def __init__(self, *args, dipole_moment = None, magnetic_dipole_moment = None, scaling = 1, magnetic_scaling = 1, **kwargs):
497
+ """
498
+ Constructor for Dipole_image_maker objects.
499
+
500
+ :param output: Path to write to. See the constructor for VMD_image_maker for how this works.
501
+ :param cube_file: A Gaussian cube file to use to render the new images.
502
+ :param dipole_moment: A Dipole_moment object that will be rendered as a red arrow in the scene.
503
+ :param magnetic_dipole_moment: A second dipole moment object that will be rendered as a blue arrow in the scene.
504
+ :param scaling: A factor to scale the dipole moment by.
505
+ :param magnetic_scaling: A factor to scale the magnetic dipole moment by.
506
+ :param *args: See the constructor for VMD_image_maker for further options.
507
+ :param **kwargs: See the constructor for VMD_image_maker for further options.
508
+ """
509
+ super().__init__(*args, **kwargs)
510
+ self.dipole_moment = dipole_moment
511
+ self.magnetic_dipole_moment = magnetic_dipole_moment
512
+ self.scaling = scaling
513
+ self.magnetic_scaling = magnetic_scaling
514
+ self.electric_arrow_colour = "red"
515
+ self.magnetic_arrow_colour = "green"
516
+
517
+ def get_dipoles(self):
518
+ """
519
+ Get required dipole information as a nested list.
520
+ """
521
+ dipoles = []
522
+ # First electric (if we have it).
523
+ for dipole, scaling, colour in [
524
+ (self.dipole_moment, self.scaling, [1.0, 0.0, 0.0, 1.0]),
525
+ (self.magnetic_dipole_moment, self.magnetic_scaling, [0.0, 1.0, 0.0, 1.0]),
526
+ ]:
527
+ if dipole is not None:
528
+ dipoles.append([
529
+ [float(coord * scaling) for coord in dipole.origin_coords],
530
+ [float(coord * scaling) for coord in dipole.vector_coords],
531
+ colour
532
+ ])
533
+
534
+ return dipoles
535
+
536
+ def blender_signature(self, output, resolution, samples, orientation):
537
+ """
538
+ The signature passed to subprocess.run used to call Blender. Inheriting classes should write their own implementation.
539
+ """
540
+ sig = super().blender_signature(output, resolution, samples, orientation)
541
+ sig.append("--dipoles")
542
+ for dipole in self.get_dipoles():
543
+ sig.append(yaml.safe_dump(dipole))
544
+ sig.extend(["--alpha", "0.5"])
545
+ return sig
546
+
547
+
548
+ class Permanent_dipole_image_maker(Dipole_image_maker):
549
+ """
550
+ Class for creating dipole images.
551
+ """
552
+
553
+ @classmethod
554
+ def from_options(self, output, *, dipole_moment = None, cube_file = None, rotations = None, options, **kwargs):
555
+ """
556
+ Constructor that takes a dictionary of config like options.
557
+ """
558
+ return super().from_options(
559
+ output,
560
+ dipole_moment = dipole_moment,
561
+ cube_file = cube_file,
562
+ rotations = rotations,
563
+ scaling = options['render']['dipole_moment']['scaling'],
564
+ options = options,
565
+ **kwargs
566
+ )
567
+
568
+ def check_can_make(self):
569
+ """
570
+ Check whether it is feasible to try and render the image(s) that we represent.
571
+
572
+ Reasons for rendering not being possible are varied and are up to the inheriting class, but include eg, a required input (cube, fchk) file not being given.
573
+
574
+ This method returns nothing, but will raise a File_maker_exception exception if the rendering is not possible.
575
+ """
576
+ super().check_can_make()
577
+
578
+ # Also make sure we have a dipole.
579
+ if self.dipole_moment is None:
580
+ raise File_maker_exception(self, "No dipole moment is available.")
581
+
582
+
583
+ class Transition_dipole_image_maker(Dipole_image_maker):
584
+ """
585
+ Class for creating TDM images.
586
+ """
587
+
588
+ def check_can_make(self):
589
+ """
590
+ Check whether it is feasible to try and render the image(s) that we represent.
591
+
592
+ Reasons for rendering not being possible are varied and are up to the inheriting class, but include eg, a required input (cube, fchk) file not being given.
593
+
594
+ This method returns nothing, but will raise a File_maker_exception exception if the rendering is not possible.
595
+ """
596
+ super().check_can_make()
597
+
598
+ # Also make sure we have a dipole.
599
+ if self.dipole_moment is None and self.magnetic_dipole_moment is None:
600
+ raise File_maker_exception(self, "No dipole moment is available.")
601
+
602
+ @classmethod
603
+ def from_options(self, output, *, dipole_moment = None, cube_file = None, magnetic_dipole_moment, rotations = None, options, **kwargs):
604
+ """
605
+ Constructor that takes a dictionary of config like options.
606
+ """
607
+ return super().from_options(
608
+ output,
609
+ dipole_moment = dipole_moment,
610
+ magnetic_dipole_moment = magnetic_dipole_moment,
611
+ cube_file = cube_file,
612
+ rotations = rotations,
613
+ scaling = options['render']['dipole_moment']['scaling'],
614
+ magnetic_scaling = options['render']['transition_dipole_moment']['magnetic_scaling'],
615
+ options = options,
616
+ **kwargs
617
+ )