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.
digichem/__init__.py CHANGED
@@ -20,7 +20,7 @@ from digichem.datas import get_resource
20
20
  # development = prerelease is not None
21
21
  # # The full version number of this package.
22
22
  # __version__ = "{}.{}.{}{}".format(major_version, minor_version, revision, "-pre.{}".format(prerelease) if development else "")
23
- __version__ = "6.1.0"
23
+ __version__ = "6.10.1"
24
24
  _v_parts = __version__.split("-")[0].split(".")
25
25
  major_version = int(_v_parts[0])
26
26
  minor_version = int(_v_parts[1])
@@ -39,7 +39,7 @@ __author__ = [
39
39
  ]
40
40
 
41
41
  # Program date (when we were last updated). This is changed automatically.
42
- _last_updated_string = "16/10/2024"
42
+ _last_updated_string = "28/05/2025"
43
43
  last_updated = datetime.strptime(_last_updated_string, "%d/%m/%Y")
44
44
 
45
45
  # The sys attribute 'frozen' is our flag, '_MEIPASS' is the dir location.
digichem/config/base.py CHANGED
@@ -33,6 +33,7 @@ class Digichem_options(Configurable):
33
33
  help = "Options specifying paths to various external programs that digichem may use. If no path is given, then these programs will simply be executed by name (so relying on OS path resolution to find the necessary executables, which is normally fine.)",
34
34
  formchk = Option(help = "Gaussian's formchk utility https://gaussian.com/formchk/", default = "formchk"),
35
35
  cubegen = Option(help = "Gaussian's cubegen utility https://gaussian.com/cubegen/", default = "cubegen"),
36
+ cubegen_parallel = Option(help = "What type of parallelism to use with cubegen, multithreaded runs a single instance of cubegen across multiple CPUs, pool runs multiple instances of cubegen", choices = [None, "multithreaded", "pool"], default = "pool")
36
37
  )
37
38
 
38
39
  skeletal_image = Options(
@@ -70,10 +71,11 @@ Possible options are:
70
71
  ),
71
72
  ),
72
73
  batoms = Options(help = "Beautiful Atoms/Blender specific options (only applies if engine == 'batoms'",
73
- blender = Option(help = "Path to the blender executable, in which beautiful atoms should be installed", default = None),
74
+ blender = Option(help = "Path to the blender executable, in which beautiful atoms should be installed", default = "batoms-blender"),
74
75
  cpus = Option(help = "The number of CPUs/threads to use. This option is overridden if running in a calculation environemnt (where it uses the same number of CPUs as the calculation did)", type = int, default = 1),
75
- render_samples = Option(help = "The number of render samples (or passes) to use. Higher values result in higher image quality and greater render times", type = int, default = 64),
76
- perspective = Option(help = "The perspective mode", choices = ["orthographic", "perspective"], default = "orthographic")
76
+ render_samples = Option(help = "The number of render samples (or passes) to use. Higher values result in higher image quality and greater render times", type = int, default = 32),
77
+ perspective = Option(help = "The perspective mode", choices = ["orthographic", "perspective"], default = "perspective"),
78
+ stacking = Option(help = "The number of image copies to composite together to avoid transparency artifacts", type = int, default = 10)
77
79
  # TODO: Colour options.
78
80
  ),
79
81
  safe_cubes = Option(help = "Whether to sanitize cubes so older software can parse them (VMD < 1.9.2 etc)", type = bool, default = False),
@@ -7,6 +7,16 @@
7
7
  #
8
8
  # Where 'blender' is the path to the Beautiful Atoms Blender executable.
9
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
+
10
20
  import sys
11
21
  import argparse
12
22
  import itertools
@@ -19,6 +29,50 @@ import logging
19
29
  import ase.io
20
30
  from batoms import Batoms
21
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()
22
76
 
23
77
  def add_molecule(
24
78
  cube_file,
@@ -55,25 +109,41 @@ def add_molecule(
55
109
  mol = Batoms(name, from_ase = cube["atoms"])
56
110
 
57
111
  # Set some look and feel options.
58
- # Change molecule style.
59
- mol.model_style = 1
60
112
 
61
113
  # Hide cell boundaries.
62
114
  mol.cell.hide = True
63
115
 
64
116
  # Colour tuning.
65
117
  # Carbon to 'black'.
66
- try:
67
- mol["C"].color = (0.095, 0.095, 0.095, 1)
68
- except AttributeError:
69
- pass
70
- try:
71
- mol["B"].color = (1.0, 0.396, 0.468, 1)
72
- except AttributeError:
73
- pass
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
74
137
 
75
- if not visible:
76
- mol.hide = True
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
77
147
 
78
148
  # Add volumes.
79
149
  if len(surface_settings) != 0:
@@ -85,15 +155,9 @@ def add_molecule(
85
155
  mol.isosurface.draw()
86
156
 
87
157
  # Now move the entire molecule (isosurface and all) back to the origin.
88
- mol.translate(cube["origin"][0:3])
89
-
90
- # Fix the origin point so we can still rotate properly.
91
- # For some reason, this code moves the bond objects to a new location?
92
- # object_mode()
93
- # bpy.ops.object.select_all(action='DESELECT')
94
- # mol.obj.select_set(True)
95
- # bpy.context.view_layer.objects.active = mol.obj
96
- # bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
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]
97
161
 
98
162
  # If we have any rotations, apply those.
99
163
  for axis, angle in rotations:
@@ -123,6 +187,9 @@ def add_molecule(
123
187
  bpy.ops.transform.rotate(value=angle, orient_axis=axis.upper(),
124
188
  center_override = (0,0,0))
125
189
 
190
+ if not visible:
191
+ mol.hide = True
192
+
126
193
  return mol
127
194
 
128
195
  # Adapted from https://blender.stackexchange.com/questions/5898/how-can-i-create-a-cylinder-linking-two-points-with-python?newreg=f372ba9448694f5b97879a6dab963cee
@@ -173,7 +240,13 @@ def draw_primitive(start, end, radius, mesh_type, color, collection = None):
173
240
  bsdf = nodes["Principled BSDF"]
174
241
  bsdf.inputs["Base Color"].default_value = color
175
242
  bsdf.inputs["Metallic"].default_value = 0.1
176
- bsdf.inputs["Specular"].default_value = 0.2
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
+
177
250
  bsdf.inputs["Roughness"].default_value = 0.2
178
251
  mat.diffuse_color = color
179
252
 
@@ -186,6 +259,8 @@ def draw_primitive(start, end, radius, mesh_type, color, collection = None):
186
259
  # Link each object to the target collection
187
260
  collection.objects.link(obj)
188
261
 
262
+ return obj
263
+
189
264
 
190
265
  def draw_arrow(start, end, radius, color, split = 0.9, collection = None):
191
266
  # Decide what proportion of the total vector to dedicate to the arrow stem and head.
@@ -195,8 +270,23 @@ def draw_arrow(start, end, radius, color, split = 0.9, collection = None):
195
270
  dist = math.sqrt(dx**2 + dy**2 + dz**2)
196
271
 
197
272
  join = (dx * split + start[0], dy * split + start[1], dz * split + start[2])
198
- draw_primitive(start, join, radius, "cylinder", color, collection = collection)
199
- draw_primitive(join, end, radius*2, "cone", color, collection = collection)
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
+
200
290
 
201
291
 
202
292
  def main():
@@ -205,25 +295,25 @@ def main():
205
295
  description='Render images with BAtoms')
206
296
 
207
297
  parser.add_argument("cube_file", help = "Path to the cube file to read")
208
- parser.add_argument("output", help = "File to write to")
298
+ parser.add_argument("output", help = "File to write to", nargs="?", default = None)
209
299
  parser.add_argument("--second_cube", help = "Optional second cube file to read additional isosurface data from", default = None)
210
300
  parser.add_argument("--isovalues", help = "List of isovalues to render", nargs = "*", type = float, default = [])
211
301
  parser.add_argument("--isotype", help = "Whether to render positive, negative or both isosurfaces for each isovalue", choices = ["positive", "negative", "both"], default = "both")
212
302
  parser.add_argument("--isocolor", help = "The colouring method to use for isosurfaces", choices = ["sign", "cube"], default = "sign")
213
- 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.65])
214
- 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.65])
215
- parser.add_argument("--style", help = "Material style for isosurfaces", choices = ('default', 'metallic', 'plastic', 'ceramic', 'mirror'), default = "default")
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")
216
306
  parser.add_argument("--cpus", help = "Number of parallel CPUs to use for rendering", type = int, default = 1)
217
307
  parser.add_argument("--use-gpu", help = "Whether to enable GPU rendering", action = "store_true")
218
- parser.add_argument("--orientation", help = "The orientation to render from, as x, y, z values", nargs = 3, type = float, default = [0, 0, 1])
308
+ parser.add_argument("--orientation", help = "The orientation to render from, as x, y, z values", nargs = 3, type = float, default = [0, 0, 0])
219
309
  parser.add_argument("--resolution", help = "The output resolution in px", type = int, default = 1024)
220
- 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 = 256)
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)
221
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 = [])
222
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 = [])
223
- parser.add_argument("--alpha", help = "Override the opacity value for all molecule objects (but not dipoles) to this value, useful for showing dipole arrows more clearly", default = None, type = float)
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)
224
314
  parser.add_argument("--perspective", help = "The perspective mode, either orthographic or perspective", default = "perspective", choices = ["perspective", "orthographic"])
225
315
  parser.add_argument("--padding", help = "Padding", type = float, default = 1.0)
226
-
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")
227
317
  # Both blender and python share the same command line arguments.
228
318
  # They are separated by double dash ('--'), everything before is for blender,
229
319
  # everything afterwards is for python (except for the first argument, wich is
@@ -237,11 +327,21 @@ def main():
237
327
 
238
328
  # Batoms or blender will silently set the extension to png if it's not already.
239
329
  # This is surprising, so stop now before that happens.
240
- if Path(args.output).suffix.lower() != ".png":
330
+ if args.output is not None and Path(args.output).suffix.lower() != ".png":
241
331
  raise ValueError("Output location must have a .png extension")
242
332
 
243
333
  if args.rotations is not None:
244
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!")
245
345
 
246
346
  # Remove the starting cube object.
247
347
  bpy.ops.object.select_all(action='SELECT')
@@ -250,7 +350,7 @@ def main():
250
350
  # Load the input data.
251
351
  mol = add_molecule(
252
352
  args.cube_file,
253
- name = "molecule",
353
+ name = "first_mol",
254
354
  visible = True,
255
355
  rotations = rotations,
256
356
  isovalues = args.isovalues,
@@ -263,13 +363,14 @@ def main():
263
363
  # Uncomment to show atom labels.
264
364
  # Needs some tweaking to appear in render (viewport only by default).
265
365
  #mol.show_label = 'species'
366
+ mol2 = None
266
367
 
267
368
  # If we have a second cube, add that too.
268
369
  if args.second_cube is not None:
269
370
  mol2 = add_molecule(
270
371
  args.second_cube,
271
- name = "molecule2",
272
- visible = True,
372
+ name = "second_mol",
373
+ visible = False,
273
374
  rotations = rotations,
274
375
  isovalues = args.isovalues,
275
376
  isotype = args.isotype,
@@ -282,29 +383,27 @@ def main():
282
383
  # Set all materials transparent.
283
384
  for material in bpy.data.materials:
284
385
  try:
285
- material.node_tree.nodes['Principled BSDF'].inputs['Alpha'].default_value = args.alpha
386
+ material.node_tree.nodes['Principled BSDF'].inputs['Alpha'].default_value = (1 - args.alpha)
286
387
  except Exception as e:
287
388
  pass
288
389
 
289
390
 
290
391
  # Draw any dipoles.
392
+ arrows = []
291
393
  if args.dipoles is not None:
292
394
 
293
395
  dipoles = [yaml.safe_load(dipole) for dipole in args.dipoles]
294
396
  for start_coord, end_coord, rgba in dipoles:
295
- draw_arrow(start_coord, end_coord, 0.08, rgba, collection = mol.coll)
397
+ arrows.append(draw_arrow(start_coord, end_coord, 0.1, rgba, collection = mol.coll))
296
398
 
297
399
 
298
400
  # Setup rendering settings.
299
401
  # mol.render.engine = 'workbench'
300
402
  # mol.render.engine = 'eevee'
301
403
  mol.render.engine = 'cycles'
302
- mol.render.resolution = [args.resolution, args.resolution]
303
404
  # Set up cycles for good quality rendering.
304
405
  # Prevents early end to rendering (forces us to use the actual number of samples).
305
406
  bpy.context.scene.cycles.use_adaptive_sampling = False
306
- # Quality control, more = better and slower.
307
- bpy.context.scene.cycles.samples = args.render_samples
308
407
  # Post-processing to remove noise, works well for coloured backgrounds, useless for transparency.
309
408
  bpy.context.scene.cycles.use_denoising = True
310
409
  # Ray-tracing options
@@ -316,8 +415,23 @@ def main():
316
415
  # Use maximum compression.
317
416
  bpy.context.scene.render.image_settings.compression = 1000
318
417
 
418
+
319
419
  # Change light intensity.
320
- bpy.data.lights["batoms_light_Default"].node_tree.nodes["Emission"].inputs[1].default_value = 0.5 #0.3
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
321
435
  #mol.render.lights["Default"].energy=10
322
436
 
323
437
  # Change view mode.
@@ -339,14 +453,40 @@ def main():
339
453
  bpy.context.scene.render.threads_mode = 'FIXED'
340
454
  bpy.context.scene.render.threads = args.cpus
341
455
 
342
- mol.get_image(viewport = args.orientation, output = args.output, padding = args.padding)
343
- # # Move the camera.
344
- # mol.render.camera.location = (100,0,0)
345
- # mol.render.camera.look_at = mol.get_center_of_geometry()
346
- # bpy.ops.object.select_all(action='DESELECT')
347
- # for obj in mol.coll.objects[:]:
348
- # obj.select_set(True)
349
- # #bpy.ops.view3d.camera_to_view_selected()
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)
350
490
 
351
491
  return 0
352
492