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,61 @@
1
+ from digichem.result.alignment.AA import Average_angle
2
+ from statistics import mean
3
+
4
+ class Adjusted_average_angle(Average_angle):
5
+ """
6
+ A further enhancement to average angle, this method uses a second set of rotations to improve alignment.
7
+ """
8
+
9
+ # Names that uniquely describe this alignment protocol.
10
+ CLASS_HANDLE = ["Adjusted Average Angle", "AAA"]
11
+
12
+ def align_axes(self):
13
+ """
14
+ Realign the axes of our coordinate system, so they have the following meaning:
15
+
16
+ X-axis: The long axis (the kebab skewer), which we define as passing through the pair of atoms with the greatest separation in our set.
17
+ Y-axis: The middle axis, which we define as perpendicular to the the X-axis (obviously) and passing through the atom that is furthest from the X-axis. Note that the Y-axis only has to pass through one atom; there may not be a corresponding atom on the other side (but there will be if the molecule is symmetrical about the X-axis).
18
+ Z-axis: The short axis, defined as perpendicular to both the X and Y-axes (so we have no choice where this goes).
19
+
20
+ :return: Nothing. The atoms are rearranged in place.
21
+ """
22
+ # First complete a total AA alignment.
23
+ super().align_X()
24
+ super().align_Y()
25
+ super().align_Z()
26
+
27
+ # Now align using our methods.
28
+ self.align_X()
29
+ self.align_Y()
30
+ self.align_Z()
31
+
32
+ def align_X(self):
33
+ """
34
+ Align the X axis.
35
+
36
+ You do not need to call this method yourself; it is called as part of align_axes().
37
+ """
38
+ # This is important because average angle always produces the same results regardless of the initial rotation of the molecule, which is not true of the rotation we are about to perform. Hence it is vital that we always start with the same coordinates regardless of initial orientation. AA does that for us.
39
+
40
+ # Now determine the mean (note that this is not a true 'averaged angle' as used in the AA method, this is your bog-standard average).
41
+ mean_angle = mean([self.get_theta(atom.coords[1], atom.coords[0]) for atom in self])
42
+ self.rotate_XY(mean_angle)
43
+
44
+ # Do the same along the Y axis.
45
+ mean_angle = mean([self.get_theta(atom.coords[2], atom.coords[0]) for atom in self])
46
+ self.rotate_XZ(mean_angle)
47
+
48
+
49
+ def align_Y(self):
50
+ """
51
+ Align the Y axis.
52
+
53
+ You do not need to call this method yourself; it is called as part of align_axes().
54
+ """
55
+
56
+ # Rotate xz coords (along Y axis).
57
+ # Get our angle.
58
+ theta = mean([self.get_theta(atom.coords[2], atom.coords[1]) for atom in self])
59
+ self.rotate_YZ(theta)
60
+
61
+
@@ -0,0 +1,148 @@
1
+ # Methodologies for determining molecule linearity.
2
+ import math
3
+
4
+ from statistics import pstdev
5
+ from digichem.result.alignment import Alignment
6
+
7
+ class Furthest_atom_pair(Alignment):
8
+ """
9
+ The 'kebab' (skewer) method for estimating molecule linearity.
10
+ """
11
+
12
+ # Names that uniquely describe this alignment protocol.
13
+ CLASS_HANDLE = ["Furthest Atom Pair", "FAP", "Kebab"]
14
+
15
+ def align_axes(self):
16
+ """
17
+ Realign the axes of our coordinate system, so they have the following meaning:
18
+
19
+ X-axis: The long axis (the kebab skewer), which we define as passing through the pair of atoms with the greatest separation in our set.
20
+ Y-axis: The middle axis, which we define as perpendicular to the the X-axis (obviously) and passing through the atom that is furthest from the X-axis. Note that the Y-axis only has to pass through one atom; there may not be a corresponding atom on the other side (but there will be if the molecule is symmetrical about the X-axis).
21
+ Z-axis: The short axis, defined as perpendicular to both the X and Y-axes (so we have no choice where this goes).
22
+
23
+ :return: Nothing. The atoms are rearranged in place.
24
+ """
25
+ # First align our X axis.
26
+ self.align_X()
27
+
28
+ # Next we want to orientate our secondary (Y) axis.
29
+ self.align_Y()
30
+
31
+ # This does nothing (but might not in the future).
32
+ self.align_Z()
33
+
34
+ def align_X(self):
35
+ """
36
+ Align the X axis.
37
+
38
+ You do not need to call this method yourself; it is called as part of align_axes().
39
+ """
40
+ # First get our most separated atoms. This is probably inefficient.
41
+ # This is a tuple of (distance, atom1, atom2).
42
+ furthest = self.get_furthest_atom_pair()
43
+
44
+ # Our new origin is halfway along these two points.
45
+ new_origin = ( (furthest[1].coords[0] + furthest[2].coords[0]) /2, (furthest[1].coords[1] + furthest[2].coords[1]) /2, (furthest[1].coords[2] + furthest[2].coords[2]) /2)
46
+ # Translate to new origin.
47
+ self.translate((-new_origin[0], -new_origin[1], -new_origin[2]))
48
+
49
+ # Now we need to rotate, because we're going to set these two atoms as points on the x axis.
50
+ # We rotate twice, once to set y = 0, once to set z = 0.
51
+
52
+ # Rotate xy coords (along Z axis).
53
+ # First determine angle (we should be able to use either atom as reference because we rotate about their midpoint).
54
+ theta = self.get_theta(furthest[1].coords[1], furthest[1].coords[0])
55
+ self.rotate_XY(theta)
56
+
57
+ # Rotate xz coords (along Y axis).
58
+ # Get our angle again.
59
+ theta = self.get_theta(furthest[1].coords[2], furthest[1].coords[0])
60
+ self.rotate_XZ(theta)
61
+
62
+ def align_Y(self):
63
+ """
64
+ Align the Y axis.
65
+
66
+ You do not need to call this method yourself; it is called as part of align_axes().
67
+ """
68
+ # Note that we're not just looking for the atom furthest away, we're looking for the greatest distance across.
69
+
70
+ # Get the most separated pair of atoms in this plane.
71
+ furthest = self.get_furthest_atom_pair_in_YZ()
72
+
73
+ # Work out the angle between the two atoms.
74
+ theta = self.get_theta(furthest[1].coords[2] - furthest[2].coords[2], furthest[1].coords[1] - furthest[2].coords[1])
75
+
76
+ # Rotate so the Y axis is parallel to a line drawn between our two furthest atoms in this plane.
77
+ self.rotate_YZ(theta)
78
+
79
+ def align_Z(self):
80
+ """
81
+ Align the Z axis.
82
+
83
+ You do not need to call this method yourself; it is called as part of align_axes().
84
+ """
85
+ # We don't need to align our Z axis, as it is automatically defined once we've aligned out X and Y.
86
+ pass
87
+
88
+ def get_deviation_from_X(self):
89
+ # First get the distance of each atom from the X-axis.
90
+ distance = [math.sqrt( (atom.coords[1])**2 + (atom.coords[2])**2 ) for atom in self]
91
+ # Get the deviation.
92
+ return pstdev(distance)
93
+
94
+ def get_furthest_atom_pair(self):
95
+ """
96
+ Get the pair of atoms in our set that are separated by the greatest distance.
97
+
98
+ :return: A tuple of the form (distance, atom1, atom2) where 'distance' is the straight line distance between 'atom1' and 'atom2'. The order of 'atom1' vs 'atom2' is essentially random and is irrelevant.
99
+ """
100
+ # This is a tuple of (distance, atom1, atom2).
101
+ furthest = (0, None, None)
102
+
103
+ for atom in self:
104
+ for foreign_atom in self:
105
+ # Compute distance.
106
+ distance = (atom.distance(foreign_atom), atom, foreign_atom)
107
+ # Check to see if we're further.
108
+ if distance[0] >= furthest[0]:
109
+ # This distance is greater, so update.
110
+ furthest = distance
111
+
112
+ # And return.
113
+ return furthest
114
+
115
+ def get_furthest_atom_from_X(self):
116
+ """
117
+ Get the atom that is furthest from the X-axis.
118
+
119
+ :return: A tuple of the form (distance, atom) where 'distance' is the straight line distance from 'atom' to the X-axis.
120
+ """
121
+ furthest = (0, None)
122
+ for atom in self:
123
+ # Calculate distance
124
+ distance = math.sqrt((atom.coords[1])**2 + (atom.coords[2])**2)
125
+ # Check.
126
+ if distance >= furthest[0]:
127
+ # Set.
128
+ furthest = (distance, atom)
129
+
130
+ # And return
131
+ return furthest
132
+
133
+ def get_furthest_atom_pair_in_YZ(self):
134
+ """
135
+ Get the pair of atoms in our set that are separated by the greatest distance in the YZ plane.
136
+
137
+ :return: A tuple of the form (distance, atom1, atom2) where 'distance' is the distance between 'atom1' and 'atom2'. The order of 'atom1' vs 'atom2' is essentially random and is irrelevant.
138
+ """
139
+ furthest = (0, None, None)
140
+ for atom in self:
141
+ for foreign_atom in self:
142
+ # Get the greatest distance in this plane.
143
+ distance = (math.sqrt( (atom.coords[1] - foreign_atom.coords[1])**2 + (atom.coords[2] - foreign_atom.coords[2])**2 ), atom, foreign_atom)
144
+ if distance[0] >= furthest[0]:
145
+ furthest = distance
146
+ # And return.
147
+ return furthest
148
+
@@ -0,0 +1,3 @@
1
+ from .base import Alignment
2
+ from .base import Axis_swapper_mix
3
+ from .base import Minimal
@@ -0,0 +1,310 @@
1
+ import math
2
+
3
+ from configurables.parent import Dynamic_parent
4
+
5
+ from digichem.result.atom import Atom_list
6
+
7
+
8
+ class Alignment(Atom_list, Dynamic_parent):
9
+ """
10
+ A class that carries out a series of transformations to realign the Cartesian axes of a set of atoms in a particular manner.
11
+ """
12
+
13
+ def __init__(self, atoms, *args, charge = None, **kwargs):
14
+ """
15
+ Constructor for this alignment class.
16
+
17
+ :param atoms: The list of atoms to transform (can be any object that provides a 'coords' attribute which is a tuple of (x, y, z) coordinates.) Note that these coordinates will be transformed in place, so make a copy of your atom list if you want to preserve your original coordinates.
18
+ """
19
+ # Call our parent first.
20
+ super().__init__(atoms, *args, charge = charge, **kwargs)
21
+
22
+ # Keep track of the transformation we make so we can apply them later.
23
+ # The translations applied to all atoms.
24
+ self.translations = (0, 0, 0)
25
+ # The rotations in radians applied to all atoms. This is a list of tuples, of the form (axis, angle), where axis is 0 = X, 1 = Y, 2 = Z.
26
+ self.rotations = []
27
+
28
+ # And transform (if we have some atoms).
29
+ if len(self) > 0:
30
+ self.align_axes()
31
+
32
+ #self.debug_print()
33
+ #exit()
34
+
35
+ @property
36
+ def method_type(self):
37
+ return self.CLASS_HANDLE[1]
38
+
39
+ @property
40
+ def human_method_type(self):
41
+ return self.CLASS_HANDLE[0]
42
+
43
+
44
+ def align_axes(self):
45
+ """
46
+ The 'main' method of this alignment class; executes the necessary transformations to align the given atoms.
47
+
48
+ The inheriting subclass should provide a concrete implementation of this method.
49
+ """
50
+ pass
51
+
52
+ @classmethod
53
+ def translate_coords(self, coords, factor):
54
+ """
55
+ Translate a given set of coordinates by a constant amount.
56
+
57
+ :param coords: A tuple of the coords (x, y, z) to translate.
58
+ :param factor: A tuple of the amount to translate the coord by (x, y, z).
59
+ :return: The translated coords.
60
+ """
61
+ return (coords[0] + factor[0],
62
+ coords[1] + factor[1],
63
+ coords[2] + factor[2])
64
+
65
+ def translate(self, factor):
66
+ """
67
+ Translate all the atoms of this set by a constant amount.
68
+
69
+ :param factor: A tuple of the amount to translate each atom's (x, y, z) coord by.
70
+ """
71
+ for atom in self:
72
+ # Translate.
73
+ atom.coords = self.translate_coords(atom.coords, factor)
74
+
75
+ # Update our param which keeps track of transformations.
76
+ self.translations = self.translate_coords(self.translations, factor)
77
+
78
+ @classmethod
79
+ def rotate_coords_XY(self, coords, theta):
80
+ """
81
+ Rotate a set of coordinates in the XY plane (down the Z-axis).
82
+
83
+ :param coords: The (X, Y, Z) coordinates to rotate.
84
+ :param theta: The angle (in radians) to rotate by.
85
+ :return: The rotated coords.
86
+ """
87
+ #TODO: Some of these rotation functions rotate in unexpected directions. This works for the alignment classes written so far, but is unexpected and confusing.
88
+ # Rotate (z stays the same).
89
+ return ((coords[0] * math.cos(theta) + coords[1] * math.sin(theta)),
90
+ ((-coords[0]) * math.sin(theta) + coords[1] * math.cos(theta)),
91
+ (coords[2]))
92
+
93
+ @classmethod
94
+ def rotate_coords_XZ(self, coords, theta):
95
+ """
96
+ Rotate a set of coordinates in the XZ plane (down the Y-axis).
97
+
98
+ :param coords: The (X, Y, Z) coordinates to rotate.
99
+ :param theta: The angle (in radians) to rotate by.
100
+ :return: The rotated coords.
101
+ """
102
+ # Rotate (y stays the same).
103
+ return ((coords[0] * math.cos(theta) + coords[2] * math.sin(theta)),
104
+ (coords[1]),
105
+ ((-coords[0]) * math.sin(theta) + coords[2] * math.cos(theta)))
106
+
107
+ @classmethod
108
+ def rotate_coords_YZ(self, coords, theta):
109
+ """
110
+ Rotate a set of coordinates in the YZ plane (down the X-axis).
111
+
112
+ :param coords: The (X, Y, Z) coordinates to rotate.
113
+ :param theta: The angle (in radians) to rotate by.
114
+ :return: The rotated coords.
115
+ """
116
+ # Rotate (x stays the same).
117
+ #return ((coords[0]),
118
+ # (),
119
+ # ())
120
+ return ((coords[0]),
121
+ (coords[1] * math.cos(theta) + coords[2] * math.sin(theta)),
122
+ ((-coords[1]) * math.sin(theta) + coords[2] * math.cos(theta)))
123
+
124
+ @classmethod
125
+ def axis_to_index(self, axis):
126
+ if axis == 'X' or axis == 0:
127
+ return 0
128
+ elif axis == 'Y' or axis == 1:
129
+ return 1
130
+ elif axis == 'Z' or axis == 2:
131
+ return 2
132
+ else:
133
+ raise ValueError("Axis '{}' is out of bounds. Possible values are 0 (X), 1 (Y) or 2 (Z)")
134
+
135
+ def rotate_coords(self, coords, axis, theta):
136
+ """
137
+ Rotate a set of coordinates around an axis.
138
+
139
+ :param coords: Tuple of (X, Y, Z) coords to rotate.
140
+ :param axis: The axis to rotate around, either ('X' or 0), ('Y' or 1) or ('Z' or 2).
141
+ :param theta: The angle (in radians) to rotate by.
142
+ :return: The rotated coords.
143
+ """
144
+ # First decide which function to use.
145
+ axis = self.axis_to_index(axis)
146
+ if axis == 0:
147
+ func = self.rotate_coords_YZ
148
+ elif axis == 1:
149
+ func = self.rotate_coords_XZ
150
+ elif axis == 2:
151
+ func = self.rotate_coords_XY
152
+
153
+ # Do the rotation.
154
+ return func(coords, theta)
155
+
156
+ def rotate(self, axis, theta):
157
+ """
158
+ Rotate all the atoms of this set around a given axis by a given angle.
159
+
160
+ :param axis: The axis to rotate around, either ('X' or 0), ('Y' or 1) or ('Z' or 2). Coordinates in this axis will not be changed.
161
+ :param theta: The angle (in radians) to rotate by.
162
+ """
163
+ # Rotate each atom.
164
+ for atom in self:
165
+ atom.coords = self.rotate_coords(atom.coords, axis, theta)
166
+
167
+ # And to our track of all our rotations.
168
+ self.rotations.append((self.axis_to_index(axis), theta))
169
+
170
+
171
+ def rotate_XY(self, theta):
172
+ """
173
+ Rotate all the atoms of this set in the xy plane (down the z-axis).
174
+
175
+ :param theta: The angle (in radians) to rotate by.
176
+ """
177
+ return self.rotate('Z', theta)
178
+
179
+ def rotate_XZ(self, theta):
180
+ """
181
+ Rotate all the atoms of this set in the xz plane (down the y-axis).
182
+
183
+ :param theta: The angle (in radians) to rotate by.
184
+ """
185
+ return self.rotate('Y', theta)
186
+
187
+ def rotate_YZ(self, theta):
188
+ """
189
+ Rotate all the atoms of this set in the yz plane (down the x-axis).
190
+
191
+ :param theta: The angle (in radians) to rotate by.
192
+ """
193
+ return self.rotate('X', theta)
194
+
195
+ @classmethod
196
+ def get_theta(self, opposite, adjacent):
197
+ """
198
+ Determine the angle needed (theta) to rotate a point.
199
+
200
+ This function does atan(opposite / adjacent), watching out for div by 0.
201
+ :param opposite: the coordinate of the point along the axis opposite to the angle to calculate.. After rotation, the point will have this coord == 0.
202
+ :param adjacent: The coordinate of the point along the axis adjacent to the angle to calculate. After rotation, the point will be on this axis.
203
+ :return: The angle (in radians).
204
+ """
205
+ try:
206
+ return math.atan(opposite / adjacent)
207
+ except (FloatingPointError, ZeroDivisionError):
208
+ # Think it's safe to assume the angle is 90 degrees in this instance.
209
+ return math.pi /2
210
+
211
+ def get_coordinate_list(self):
212
+ """
213
+ Get the coordinates of all the atoms of our set as a tuple of lists of the form ([X1, X2...], [Y1, Y2...], [Z1, Z2...]).
214
+
215
+ This is transposed compared to the normal format, which is [(X1, Y1, Z1), (X2, Y2, Z2)...].
216
+ :return: A tuple of lists of X, Y and Z coordinates. Each list is guaranteed to be the same length.
217
+ """
218
+ return list(map(list, zip(* [(atom.coords[0], atom.coords[1], atom.coords[2]) for atom in self])))
219
+
220
+ def apply_transformation(self, coords):
221
+ """
222
+ Apply the transformations that have been used to align this atom set to a set of coordinates.
223
+
224
+ This method is useful if you have objects that you would like to re-align to this atom set, but you don't want to influence the actual alignment process (eg, dipole moments).
225
+
226
+ :param coords: A list-like set of 3 coordinates (X, Y, Z).
227
+ :return: The transformed coordinates as a tuple.
228
+ """
229
+ # First, translate.
230
+ coords = self.translate_coords(coords, self.translations)
231
+ # Now, rotate.
232
+ for axis, theta in self.rotations:
233
+ coords = self.rotate_coords(coords, axis, theta)
234
+
235
+ # Return as a tuple.
236
+ return coords
237
+
238
+ def debug_print(self):
239
+
240
+ for atom in self:
241
+ print("{}, {}, {}, {}".format(atom.element, atom.coords[0], atom.coords[1], atom.coords[2]))
242
+
243
+ def dump(self, digichem_options):
244
+ """
245
+ Get a representation of this result object in primitive format.
246
+ """
247
+ dump_dict = super().dump(digichem_options)
248
+ dump_dict['alignment_method'] = self.human_method_type
249
+ return dump_dict
250
+
251
+
252
+ # @classmethod
253
+ # def merge(self, *multiple_lists):
254
+ # """
255
+ # Merge multiple lists of atoms into a single list.
256
+ #
257
+ # Note that it does not make logical sense to combine different list of atoms into one; hence the method only ensures that all given lists are the same and then returns the first given.
258
+ # If the atom lists are not equivalent, a warning will be issued.
259
+ # If any of the alignment methods are not the same, a warning will be issued.
260
+ # """
261
+ # alignment = multiple_lists[0]
262
+ #
263
+ # # Check all other lists are the same.
264
+ # for atom_list in multiple_lists[1:]:
265
+ # if type(alignment) != type(atom_list):
266
+ # warnings.warn("")
267
+ #
268
+ # # Return the 'merged' list.
269
+ # return alignment
270
+
271
+
272
+ class Axis_swapper_mix():
273
+ """
274
+ A class mixin for automatically re-assigning the axes so that X, Y & Z axes are in decreasing length.
275
+ """
276
+
277
+ def reassign_axes(self):
278
+ """
279
+ Automatically swap the axes of this class so that X, Y & Z axes are in decreasing length.
280
+ """
281
+ if self.X_length < self.Y_length and self.Y_length > self.Z_length:
282
+ # Swap X and Y.
283
+ self.rotate_XY(-math.pi/2)
284
+ elif self.X_length < self.Z_length:
285
+ # Swap X and Z.
286
+ self.rotate_XZ(-math.pi/2)
287
+
288
+ if self.Y_length < self.Z_length:
289
+ # Swap. Y and Z.
290
+ self.rotate_YZ(-math.pi/2)
291
+
292
+
293
+ class Minimal(Alignment, Axis_swapper_mix):
294
+ """
295
+ A basic alignment class, only checks to ensure that the X, Y & Z axes are in decreasing length.
296
+ """
297
+
298
+ # Names that uniquely describe this alignment protocol.
299
+ CLASS_HANDLE = ["Minimal", "MIN"]
300
+
301
+ def align_axes(self):
302
+ """
303
+ The 'main' method of this alignment class; executes the necessary transformations to align the given atoms.
304
+ """
305
+ # All we do is swap axes.
306
+ self.reassign_axes()
307
+
308
+
309
+
310
+
@@ -0,0 +1,153 @@
1
+ import math
2
+
3
+ import digichem.config
4
+
5
+
6
+ class Angle():
7
+ """
8
+ A class for representing angles (either in radians or degrees).
9
+ """
10
+
11
+ # # The default units to print angles in.
12
+ # _default_angle_units = "deg"
13
+ #
14
+ # @classmethod
15
+ # def set_default_angle_units(self, angle_units):
16
+ # """
17
+ # Change the default angle units used by subsequent objects.
18
+ # """
19
+ # if angle_units != "rad" and angle_units != "deg":
20
+ # self._raise_angle_unit_error(angle_units)
21
+ # else:
22
+ # self._default_angle_units = angle_units
23
+
24
+ @property
25
+ def _default_angle_units(self):
26
+ return digichem.config.get_config()['angle_units']
27
+
28
+
29
+ def __init__(self, angle, angle_units = None, output_units = "def"):
30
+ """
31
+ Constructor for angle objects.
32
+
33
+ :param angle: The numerical value of the angle. Note that this will be stored internally in radians under the 'radians' attribute, and as degrees under 'degrees'.
34
+ :param angle_units: The units of the angle, either "rad" for radians or "deg" for degrees. If angle is an Angle object, angle_units is ignored and determined automatically. Otherwise and if angle_units is None, "rad" is assumed as the default.
35
+ :param output_units: The units to use when accessing the angle, either "def" to use the class default 'default_angle_units', "rad" for radians or "deg" for degrees. This can be changed post init by setting the 'units' property.
36
+ """
37
+ if isinstance(angle, Angle):
38
+ angle_units = angle.units
39
+ angle = float(angle)
40
+
41
+ if angle_units is None:
42
+ angle_units = "rad"
43
+
44
+ # Check our units are valid and save our angle.
45
+ if angle_units == "rad":
46
+ self._angle = angle
47
+ elif angle_units == "deg":
48
+ self._angle = self.deg_to_rad(angle)
49
+ else:
50
+ # Get upset.
51
+ self._raise_angle_unit_error(angle_units)
52
+
53
+ # Save our output units (this is type checked for us).
54
+ if output_units == "def":
55
+ # Use our default (which can be set at the module level).
56
+ self.units = self._default_angle_units
57
+ else:
58
+ self.units = output_units
59
+
60
+ @classmethod
61
+ def _raise_angle_unit_error(self, angle_unit):
62
+ """
63
+ Convenience function that raises an exception.
64
+ """
65
+ raise ValueError('\'{}\' is not a valid angle unit. Accepted values are "rad" or "deg"'.format(angle_unit))
66
+
67
+ @property
68
+ def radians(self):
69
+ """
70
+ The angle in radians.
71
+ """
72
+ return self._angle
73
+
74
+ @property
75
+ def degrees(self):
76
+ """
77
+ The angle in degrees.
78
+ """
79
+ return self.rad_to_deg(self._angle)
80
+
81
+ @property
82
+ def angle(self):
83
+ if self._units == "rad":
84
+ return self._angle
85
+ elif self._units == "deg":
86
+ return self.rad_to_deg(self._angle)
87
+ else:
88
+ self._raise_angle_unit_error(self._units)
89
+
90
+ @property
91
+ def units(self):
92
+ """
93
+ The units of the angle, either "deg" for degrees or "rad" for radians.
94
+ """
95
+ return self._units
96
+
97
+ @units.setter
98
+ def units(self, value):
99
+ if value == "rad" or value == "deg":
100
+ self._units = value
101
+ else:
102
+ self._raise_angle_unit_error(self._units)
103
+
104
+ @property
105
+ def pretty_units(self):
106
+ """
107
+ The units of the angle, currently either "°" for degrees or "rad" for radians.
108
+
109
+ Note that the units returned by this method may change without warning so they are not suitable for type checking, use 'units' for actually checking the type of angle.
110
+ """
111
+ return self.units_to_pretty_units(self.units)
112
+
113
+ @classmethod
114
+ def units_to_pretty_units(self, units):
115
+ """
116
+ Convert a string describing angle units (either 'deg' or 'rad') to an appropriate symbol, currently either "°" for degrees or "rad" for radians.
117
+
118
+ :param units: The units as a string.
119
+ :return: An appropriate unit symbol as a string.
120
+ """
121
+ if units == "deg":
122
+ return "°"
123
+ elif units == "rad":
124
+ return "rad"
125
+ else:
126
+ return ""
127
+
128
+
129
+ def __float__(self):
130
+ """
131
+ Get the numerical value of this angle, according to our current 'units'.
132
+
133
+ Use either the 'radians' or 'degrees' properties to use a specific unit.
134
+ """
135
+ return self.angle
136
+
137
+ def __str__(self):
138
+ """
139
+ String representation of this angle, currently in the format: "angle" "units".
140
+ """
141
+ return "{} {}".format(self.angle, self.pretty_units)
142
+
143
+
144
+ @classmethod
145
+ def deg_to_rad(self, deg):
146
+ return math.radians(deg)
147
+
148
+ @classmethod
149
+ def rad_to_deg(self, rad):
150
+ return math.degrees(rad)
151
+
152
+
153
+