digichem-core 6.1.0__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.
@@ -0,0 +1,500 @@
1
+ # Render something with Beautiful Atoms.
2
+ # To use this script, run something like:
3
+ #
4
+ # # To disabled possibly conflicting packages outside of the conda env.
5
+ # PYTHONNOUSERSITE=1
6
+ # blender -b -P batoms-renderer.py
7
+ #
8
+ # Where 'blender' is the path to the Beautiful Atoms Blender executable.
9
+
10
+
11
+ # import debugpy
12
+ # debugpy.listen(5678)
13
+ # debugpy.wait_for_client()
14
+
15
+ import addon_utils
16
+ def handle_error(exception):
17
+ raise exception
18
+ addon_utils.enable("batoms", handle_error = handle_error, default_set=True)
19
+
20
+ import sys
21
+ import argparse
22
+ import itertools
23
+ import bpy
24
+ import yaml
25
+ import math
26
+ from pathlib import Path
27
+ import logging
28
+
29
+ import ase.io
30
+ from batoms import Batoms
31
+ from batoms.utils.butils import object_mode
32
+ from batoms.render import Render
33
+
34
+ class Digichem_render(Render):
35
+ def set_viewport_distance_center(self, center=None, padding=0, canvas=None):
36
+ """
37
+ Calculate canvas and direction
38
+ """
39
+ batoms = self.batoms
40
+ if padding is None:
41
+ padding = max(batoms.size) + 0.5
42
+ if center is None:
43
+ center = batoms.get_center_of_geometry()
44
+ self.center = center
45
+ if canvas is None:
46
+ width, height, depth = batoms.get_canvas_box(
47
+ direction=self.viewport, padding=padding
48
+ )
49
+ else:
50
+ width = canvas[0]
51
+ height = canvas[1]
52
+ depth = canvas[2]
53
+ if self.distance < 0:
54
+ self.distance = max(10, depth)
55
+
56
+ self.update_camera(width, height, depth / 2)
57
+
58
+ # To auto centre the camera, we need to select the molecule as well as all isosurfaces that might be present.
59
+ # Select the molecule.
60
+ self.batoms.obj.select_set(True)
61
+
62
+ # Isosurfaces.
63
+ for obj in self.batoms.coll.all_objects:
64
+ if obj.batoms.type == "ISOSURFACE":
65
+ obj.select_set(True)
66
+
67
+ # Set camera as active.
68
+ bpy.context.scene.camera = self.camera.obj
69
+
70
+ # Manually set a focal point.
71
+ self.camera.lens = 50
72
+ # Auto centre.
73
+ bpy.ops.view3d.camera_to_view_selected()
74
+
75
+ self.update_light()
76
+
77
+ def add_molecule(
78
+ cube_file,
79
+ name,
80
+ rotations = None,
81
+ visible = True,
82
+ isovalues = None,
83
+ isotype = "both",
84
+ primary_color = [1, 0.058, 0.0, 0.55],
85
+ secondary_color = [0.1, 0.1, 0.9, 0.55],
86
+ style = "default"
87
+ ):
88
+ """
89
+ """
90
+ rotations = [] if rotations is None else rotations
91
+ isovalues = [] if isovalues is None else isovalues
92
+
93
+ surface_settings = []
94
+ for isovalue in isovalues:
95
+ if isotype in ["both", "positive"]:
96
+ surface_settings.append({'level': isovalue, 'color': primary_color, 'material_style': style})
97
+
98
+ if isotype in ["both", "negative"]:
99
+ surface_settings.append({'level': -isovalue, 'color': secondary_color, 'material_style': style})
100
+
101
+
102
+ # Load the input data.
103
+ cube = ase.io.read(cube_file, format="cube", read_data=True, full_output=True)
104
+
105
+ # The centre of the cube is often offset, fix that by shifting the atoms.
106
+ cube["atoms"].translate(-cube["origin"][0:3])
107
+
108
+ # Get the mol object.
109
+ mol = Batoms(name, from_ase = cube["atoms"])
110
+
111
+ # Set some look and feel options.
112
+
113
+ # Hide cell boundaries.
114
+ mol.cell.hide = True
115
+
116
+ # Colour tuning.
117
+ # Carbon to 'black'.
118
+ new_colors = {
119
+ "C": (0.095, 0.095, 0.095, 1),
120
+ "B": (1.0, 0.396, 0.468, 1)
121
+ }
122
+
123
+ for atom, color in new_colors.items():
124
+ try:
125
+ mol[atom].color = color
126
+ except AttributeError:
127
+ pass
128
+
129
+ # And bonds.
130
+ for key, value in mol.bond.settings.items():
131
+ for atom, color in new_colors.items():
132
+ if key[0] == atom:
133
+ value['color1'] = color
134
+
135
+ if key[1] == "C":
136
+ value['color2'] = color
137
+
138
+ # Change molecule style.
139
+ mol.model_style = 1
140
+
141
+ # Slightly increase volume of all atoms.
142
+ for atom in mol.species.keys():
143
+ mol[atom].scale *= 1.25
144
+
145
+ # Increase volume of H atoms
146
+ mol['H'].scale = 0.75
147
+
148
+ # Add volumes.
149
+ if len(surface_settings) != 0:
150
+ mol.volumetric_data['surface'] = cube['data']
151
+
152
+ for index, settings in enumerate(surface_settings):
153
+ mol.isosurface.settings[index+1] = settings
154
+
155
+ mol.isosurface.draw()
156
+
157
+ # Now move the entire molecule (isosurface and all) back to the origin.
158
+ mol.obj.select_set(True)
159
+ bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center='MEDIAN')
160
+ bpy.context.object.location = [0,0,0]
161
+
162
+ # If we have any rotations, apply those.
163
+ for axis, angle in rotations:
164
+ # Convert to degree.
165
+ # Not used.
166
+ degree = angle * (180/math.pi)
167
+
168
+ if axis == 0:
169
+ axis = "x"
170
+
171
+ elif axis == 1:
172
+ axis = "y"
173
+
174
+ elif axis == 2:
175
+ axis = "z"
176
+
177
+ else:
178
+ raise ValueError("Unknown rotation axis '{}'".format(axis))
179
+
180
+ #mol.rotate(degree, axis)
181
+
182
+ # Taken from batoms source (collection.py: 51)
183
+ object_mode()
184
+ bpy.ops.object.select_all(action='DESELECT')
185
+ mol.obj.select_set(True)
186
+ bpy.context.view_layer.objects.active = mol.obj
187
+ bpy.ops.transform.rotate(value=angle, orient_axis=axis.upper(),
188
+ center_override = (0,0,0))
189
+
190
+ if not visible:
191
+ mol.hide = True
192
+
193
+ return mol
194
+
195
+ # Adapted from https://blender.stackexchange.com/questions/5898/how-can-i-create-a-cylinder-linking-two-points-with-python?newreg=f372ba9448694f5b97879a6dab963cee
196
+ def draw_primitive(start, end, radius, mesh_type, color, collection = None):
197
+ """
198
+ """
199
+ dx = end[0] - start[0]
200
+ dy = end[1] - start[1]
201
+ dz = end[2] - start[2]
202
+ dist = math.sqrt(dx**2 + dy**2 + dz**2)
203
+
204
+ # First draw the shape.
205
+ if mesh_type == "cylinder":
206
+ bpy.ops.mesh.primitive_cylinder_add(
207
+ radius = radius,
208
+ depth = dist,
209
+ vertices = 32,
210
+ location = (dx/2 + start[0], dy/2 + start[1], dz/2 + start[2])
211
+ )
212
+ elif mesh_type == "cone":
213
+ bpy.ops.mesh.primitive_cone_add(
214
+ radius1 = radius,
215
+ depth = dist,
216
+ vertices = 32,
217
+ location = (dx/2 + start[0], dy/2 + start[1], dz/2 + start[2])
218
+ )
219
+ else:
220
+ raise ValueError("Unknown mesh type '{}'".format(mesh_type))
221
+
222
+ # Get a reference to the object we just made.
223
+ obj = bpy.context.active_object
224
+
225
+ phi = math.atan2(dy, dx)
226
+ theta = math.acos(dz/dist)
227
+
228
+ bpy.context.object.rotation_euler[1] = theta
229
+ bpy.context.object.rotation_euler[2] = phi
230
+
231
+ # Get material
232
+ mat = bpy.data.materials.new(name="Material")
233
+
234
+ # assign to 1st material slot
235
+ obj.active_material = mat
236
+
237
+ mat.use_nodes = True
238
+ tree = mat.node_tree
239
+ nodes = tree.nodes
240
+ bsdf = nodes["Principled BSDF"]
241
+ bsdf.inputs["Base Color"].default_value = color
242
+ bsdf.inputs["Metallic"].default_value = 0.1
243
+ try:
244
+ # Blener 3.x
245
+ bsdf.inputs["Specular"].default_value = 0.2
246
+ except KeyError:
247
+ # Blender 4.x
248
+ bsdf.inputs["Specular IOR Level"].default_value = 0.2
249
+
250
+ bsdf.inputs["Roughness"].default_value = 0.2
251
+ mat.diffuse_color = color
252
+
253
+ # If we've been asked to, asign our new object to a given collection.
254
+ # First unlink from any old collections
255
+ for coll in obj.users_collection:
256
+ # Unlink the object
257
+ coll.objects.unlink(obj)
258
+
259
+ # Link each object to the target collection
260
+ collection.objects.link(obj)
261
+
262
+ return obj
263
+
264
+
265
+ def draw_arrow(start, end, radius, color, split = 0.9, collection = None):
266
+ # Decide what proportion of the total vector to dedicate to the arrow stem and head.
267
+ dx = end[0] - start[0]
268
+ dy = end[1] - start[1]
269
+ dz = end[2] - start[2]
270
+ dist = math.sqrt(dx**2 + dy**2 + dz**2)
271
+
272
+ join = (dx * split + start[0], dy * split + start[1], dz * split + start[2])
273
+ cylinder = draw_primitive(start, join, radius, "cylinder", color, collection = collection)
274
+ cone = draw_primitive(join, end, radius*2, "cone", color, collection = collection)
275
+
276
+ # Select the two objects and join them together.
277
+ bpy.ops.object.select_all(action='DESELECT')
278
+ cylinder.select_set(True)
279
+ cone.select_set(True)
280
+ bpy.ops.object.join()
281
+
282
+ arrow = cone
283
+
284
+ # Set the origin of the new combined object to the origin of the arrow.
285
+ bpy.context.scene.cursor.location = start
286
+ bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
287
+
288
+ return arrow
289
+
290
+
291
+
292
+ def main():
293
+ parser = argparse.ArgumentParser(
294
+ prog='Beautiful Atoms Renderer',
295
+ description='Render images with BAtoms')
296
+
297
+ parser.add_argument("cube_file", help = "Path to the cube file to read")
298
+ parser.add_argument("output", help = "File to write to", nargs="?", default = None)
299
+ parser.add_argument("--second_cube", help = "Optional second cube file to read additional isosurface data from", default = None)
300
+ parser.add_argument("--isovalues", help = "List of isovalues to render", nargs = "*", type = float, default = [])
301
+ parser.add_argument("--isotype", help = "Whether to render positive, negative or both isosurfaces for each isovalue", choices = ["positive", "negative", "both"], default = "both")
302
+ parser.add_argument("--isocolor", help = "The colouring method to use for isosurfaces", choices = ["sign", "cube"], default = "sign")
303
+ parser.add_argument("--primary-color", help = "RGBA for one of the colors to use for isosurfaces", type = float, nargs = 4, default = [0.1, 0.1, 0.9, 0.5])
304
+ parser.add_argument("--secondary-color", help = "RGBA for the other color to use for isosurfaces", type = float, nargs = 4, default = [1, 0.058, 0.0, 0.5])
305
+ parser.add_argument("--style", help = "Material style for isosurfaces", choices = ('default', 'metallic', 'plastic', 'ceramic', 'mirror'), default = "ceramic")
306
+ parser.add_argument("--cpus", help = "Number of parallel CPUs to use for rendering", type = int, default = 1)
307
+ parser.add_argument("--use-gpu", help = "Whether to enable GPU rendering", action = "store_true")
308
+ parser.add_argument("--orientation", help = "The orientation to render from, as x, y, z values", nargs = 3, type = float, default = [0, 0, 0])
309
+ parser.add_argument("--resolution", help = "The output resolution in px", type = int, default = 1024)
310
+ parser.add_argument("--render-samples", help = "The maximum number of render samples, more generally results in higher quality but longer render times", type = int, default = 64)# default = 256)
311
+ parser.add_argument("--rotations", help = "A list of rotations (in JSON) to rotate the molecule to a given alignment. The first item in each list item is the axis to rotate about (0=x, 1=y, 2=z), the second is the angle to rotate by (in radians)", nargs = "*", default = [])
312
+ parser.add_argument("--dipoles", help = "Draw dipoles from a list of the following data (in JSON): 0) start coord, 1) end coord, 2) RGBA color information", nargs = "*", default = [])
313
+ parser.add_argument("--alpha", help = "Override the opacity value for all molecule objects (but not dipoles) to 1- this value, useful for showing dipole arrows more clearly", default = None, type = float)
314
+ parser.add_argument("--perspective", help = "The perspective mode, either orthographic or perspective", default = "perspective", choices = ["perspective", "orthographic"])
315
+ parser.add_argument("--padding", help = "Padding", type = float, default = 1.0)
316
+ parser.add_argument("--multi", help = "Render multiple images, each of a different angle of the scene. Each argument should consist of 6 parts, the x y z position, the resolution, the samples, and the filename (which is appended to 'output')", nargs = 6, default =[], action="append")
317
+ # Both blender and python share the same command line arguments.
318
+ # They are separated by double dash ('--'), everything before is for blender,
319
+ # everything afterwards is for python (except for the first argument, wich is
320
+ # the program name, which is for both).
321
+ if "--" in sys.argv:
322
+ python_argv = sys.argv[sys.argv.index("--") +1:]
323
+ else:
324
+ python_argv = []
325
+
326
+ args = parser.parse_args(python_argv)
327
+
328
+ # Batoms or blender will silently set the extension to png if it's not already.
329
+ # This is surprising, so stop now before that happens.
330
+ if args.output is not None and Path(args.output).suffix.lower() != ".png":
331
+ raise ValueError("Output location must have a .png extension")
332
+
333
+ if args.rotations is not None:
334
+ rotations = [yaml.safe_load(rotation) for rotation in args.rotations]
335
+
336
+ if args.multi != []:
337
+ if args.orientation != [0, 0, 0]:
338
+ raise ValueError("You cannot set both --orientation and --multi!")
339
+
340
+ if args.resolution != 1024:
341
+ raise ValueError("You cannot set both --resolution and --multi!")
342
+
343
+ if args.output is not None:
344
+ raise ValueError("You cannot set both 'output' and --multi!")
345
+
346
+ # Remove the starting cube object.
347
+ bpy.ops.object.select_all(action='SELECT')
348
+ bpy.ops.object.delete()
349
+
350
+ # Load the input data.
351
+ mol = add_molecule(
352
+ args.cube_file,
353
+ name = "first_mol",
354
+ visible = True,
355
+ rotations = rotations,
356
+ isovalues = args.isovalues,
357
+ isotype = args.isotype,
358
+ primary_color = args.primary_color,
359
+ secondary_color = args.secondary_color if args.isocolor == "sign" else args.primary_color,
360
+ style = args.style
361
+ )
362
+
363
+ # Uncomment to show atom labels.
364
+ # Needs some tweaking to appear in render (viewport only by default).
365
+ #mol.show_label = 'species'
366
+ mol2 = None
367
+
368
+ # If we have a second cube, add that too.
369
+ if args.second_cube is not None:
370
+ mol2 = add_molecule(
371
+ args.second_cube,
372
+ name = "second_mol",
373
+ visible = False,
374
+ rotations = rotations,
375
+ isovalues = args.isovalues,
376
+ isotype = args.isotype,
377
+ primary_color = args.primary_color if args.isocolor == "sign" else args.secondary_color,
378
+ secondary_color = args.secondary_color,
379
+ style = args.style
380
+ )
381
+
382
+ if args.alpha:
383
+ # Set all materials transparent.
384
+ for material in bpy.data.materials:
385
+ try:
386
+ material.node_tree.nodes['Principled BSDF'].inputs['Alpha'].default_value = (1 - args.alpha)
387
+ except Exception as e:
388
+ pass
389
+
390
+
391
+ # Draw any dipoles.
392
+ arrows = []
393
+ if args.dipoles is not None:
394
+
395
+ dipoles = [yaml.safe_load(dipole) for dipole in args.dipoles]
396
+ for start_coord, end_coord, rgba in dipoles:
397
+ arrows.append(draw_arrow(start_coord, end_coord, 0.1, rgba, collection = mol.coll))
398
+
399
+
400
+ # Setup rendering settings.
401
+ # mol.render.engine = 'workbench'
402
+ # mol.render.engine = 'eevee'
403
+ mol.render.engine = 'cycles'
404
+ # Set up cycles for good quality rendering.
405
+ # Prevents early end to rendering (forces us to use the actual number of samples).
406
+ bpy.context.scene.cycles.use_adaptive_sampling = False
407
+ # Post-processing to remove noise, works well for coloured backgrounds, useless for transparency.
408
+ bpy.context.scene.cycles.use_denoising = True
409
+ # Ray-tracing options
410
+ bpy.context.scene.cycles.max_bounces = 48
411
+ bpy.context.scene.cycles.transparent_max_bounces = 24
412
+ if args.use_gpu:
413
+ bpy.context.scene.cycles.device = "GPU"
414
+
415
+ # Use maximum compression.
416
+ bpy.context.scene.render.image_settings.compression = 1000
417
+
418
+
419
+ # Change light intensity.
420
+ mol.render.lights["Default"].direction = [0.1, 0.1, 1]
421
+ mol.render.lights["Default"].obj.data.node_tree.nodes["Emission"].inputs[1].default_value = 0.2
422
+ mol.render.lights["Default"].obj.data.angle = 0.174533
423
+
424
+ # Add a second light for depth.
425
+ mol.render.lights.add("Accent1", direction = [1,0.5,0.75])
426
+ mol.render.lights.add("Accent2", direction = [0.5,1,0.75])
427
+
428
+ mol.render.lights["Accent1"].obj.data.angle = 0.0872665
429
+ mol.render.lights["Accent1"].obj.data.node_tree.nodes["Emission"].inputs[1].default_value = 0.25
430
+ mol.render.lights["Accent2"].obj.data.angle = 0.0872665
431
+ mol.render.lights["Accent2"].obj.data.node_tree.nodes["Emission"].inputs[1].default_value = 0.25
432
+
433
+ # bpy.data.lights["batoms_light_Default"].node_tree.nodes["Emission"].inputs[1].default_value = 0.45
434
+ # bpy.data.lights["batoms_light_Default"].angle
435
+ #mol.render.lights["Default"].energy=10
436
+
437
+ # Change view mode.
438
+ if args.perspective == "perspective":
439
+ mol.render.camera.type = "PERSP"
440
+
441
+ else:
442
+ mol.render.camera.type = "ORTHO"
443
+
444
+ # Enable to add an outline.
445
+ #bpy.context.scene.render.use_freestyle = True
446
+
447
+ # We have plenty of memory to play with, use one tile.
448
+ bpy.context.scene.cycles.tile_x = args.resolution
449
+ bpy.context.scene.cycles.tile_y = args.resolution
450
+ bpy.context.scene.cycles.tile_size = args.resolution
451
+
452
+ # Performance options.
453
+ bpy.context.scene.render.threads_mode = 'FIXED'
454
+ bpy.context.scene.render.threads = args.cpus
455
+
456
+ # Set our custom renderer so we can modify zoom etc.
457
+ mol.render = Digichem_render()
458
+
459
+ # We have two ways we can change which angle we render from.
460
+ # 1) the viewport keyword arg (which places the camera in a certain location).
461
+ # 2) rotate the molecule.
462
+ #
463
+ # We use option 2, because this gives us more control.
464
+
465
+ # Work out how many angles we're rendering from.
466
+ if args.multi == []:
467
+ # Just one.
468
+ targets = [[args.orientation[0], args.orientation[1], args.orientation[2], args.resolution, args.render_samples, args.output]]
469
+
470
+ else:
471
+ # More than one.
472
+ targets = args.multi
473
+
474
+ for x, y, z, resolution, samples, full_file_name in targets:
475
+ # Add args.output and mini_file_name together (useful for --multi).
476
+ orientation = (float(x), float(y), float(z))
477
+
478
+ mol.render.resolution = [resolution, resolution]
479
+ # Quality control, more = better and slower.
480
+ bpy.context.scene.cycles.samples = int(samples)
481
+ mol.obj.delta_rotation_euler = orientation
482
+
483
+ if mol2 is not None:
484
+ mol2.obj.delta_rotation_euler = orientation
485
+
486
+ for arrow in arrows:
487
+ arrow.delta_rotation_euler = orientation
488
+
489
+ mol.get_image(viewport = [0,0,1], output = full_file_name, padding = args.padding)
490
+
491
+ return 0
492
+
493
+ # If we've been invoked as a program, call main().
494
+ if __name__ == '__main__':
495
+ try:
496
+ sys.exit(main())
497
+
498
+ except Exception as e:
499
+ logging.error("Erro", exc_info = True)
500
+ sys.exit(1)
digichem/file/base.py CHANGED
@@ -45,6 +45,20 @@ class File_maker_ABC():
45
45
  """
46
46
  raise NotImplementedError("delete() is not implemented in this ABC")
47
47
 
48
+ def get_parallel(self, name = 'file', cpus = 1):
49
+ """
50
+ Generate the file represented by this object in a parallel context.
51
+
52
+ get_parallel() will be called by a higher level function as an argument to ThreadPoolExecutor.map() or similar,
53
+ to generate many files simultaneously. This is useful for slow operations (such as cube generation) that are
54
+ difficult to parallelise individually, but easy to parallelise across multiple files.
55
+
56
+ :param name: The file to generate.
57
+ :param cpus: The number of CPUs this operation should use, nearly always 1.
58
+ """
59
+ # This default implementation does nothing.
60
+ pass
61
+
48
62
  #################################
49
63
  # Implemented in this sub-class #
50
64
  #################################