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.
- digichem/__init__.py +75 -0
- digichem/basis.py +116 -0
- digichem/config/README +3 -0
- digichem/config/__init__.py +5 -0
- digichem/config/base.py +321 -0
- digichem/config/locations.py +14 -0
- digichem/config/parse.py +90 -0
- digichem/config/util.py +117 -0
- digichem/data/README +4 -0
- digichem/data/batoms/COPYING +18 -0
- digichem/data/batoms/LICENSE +674 -0
- digichem/data/batoms/README +2 -0
- digichem/data/batoms/__init__.py +0 -0
- digichem/data/batoms/batoms-renderer.py +351 -0
- digichem/data/config/digichem.yaml +714 -0
- digichem/data/functionals.csv +15 -0
- digichem/data/solvents.csv +185 -0
- digichem/data/tachyon/COPYING.md +5 -0
- digichem/data/tachyon/LICENSE +30 -0
- digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
- digichem/data/vmd/common.tcl +468 -0
- digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
- digichem/data/vmd/generate_density_images.tcl +45 -0
- digichem/data/vmd/generate_dipole_images.tcl +68 -0
- digichem/data/vmd/generate_orbital_images.tcl +57 -0
- digichem/data/vmd/generate_spin_images.tcl +66 -0
- digichem/data/vmd/generate_structure_images.tcl +40 -0
- digichem/datas.py +14 -0
- digichem/exception/__init__.py +7 -0
- digichem/exception/base.py +133 -0
- digichem/exception/uncatchable.py +63 -0
- digichem/file/__init__.py +1 -0
- digichem/file/base.py +364 -0
- digichem/file/cube.py +284 -0
- digichem/file/fchk.py +94 -0
- digichem/file/prattle.py +277 -0
- digichem/file/types.py +97 -0
- digichem/image/__init__.py +6 -0
- digichem/image/base.py +113 -0
- digichem/image/excited_states.py +335 -0
- digichem/image/graph.py +293 -0
- digichem/image/orbitals.py +239 -0
- digichem/image/render.py +617 -0
- digichem/image/spectroscopy.py +797 -0
- digichem/image/structure.py +115 -0
- digichem/image/vmd.py +826 -0
- digichem/input/__init__.py +3 -0
- digichem/input/base.py +78 -0
- digichem/input/digichem_input.py +500 -0
- digichem/input/gaussian.py +140 -0
- digichem/log.py +179 -0
- digichem/memory.py +166 -0
- digichem/misc/__init__.py +4 -0
- digichem/misc/argparse.py +44 -0
- digichem/misc/base.py +61 -0
- digichem/misc/io.py +239 -0
- digichem/misc/layered_dict.py +285 -0
- digichem/misc/text.py +139 -0
- digichem/misc/time.py +73 -0
- digichem/parse/__init__.py +13 -0
- digichem/parse/base.py +220 -0
- digichem/parse/cclib.py +138 -0
- digichem/parse/dump.py +253 -0
- digichem/parse/gaussian.py +130 -0
- digichem/parse/orca.py +96 -0
- digichem/parse/turbomole.py +201 -0
- digichem/parse/util.py +523 -0
- digichem/result/__init__.py +6 -0
- digichem/result/alignment/AA.py +114 -0
- digichem/result/alignment/AAA.py +61 -0
- digichem/result/alignment/FAP.py +148 -0
- digichem/result/alignment/__init__.py +3 -0
- digichem/result/alignment/base.py +310 -0
- digichem/result/angle.py +153 -0
- digichem/result/atom.py +742 -0
- digichem/result/base.py +258 -0
- digichem/result/dipole_moment.py +332 -0
- digichem/result/emission.py +402 -0
- digichem/result/energy.py +323 -0
- digichem/result/excited_state.py +821 -0
- digichem/result/ground_state.py +94 -0
- digichem/result/metadata.py +644 -0
- digichem/result/multi.py +98 -0
- digichem/result/nmr.py +1086 -0
- digichem/result/orbital.py +647 -0
- digichem/result/result.py +244 -0
- digichem/result/soc.py +272 -0
- digichem/result/spectroscopy.py +514 -0
- digichem/result/tdm.py +267 -0
- digichem/result/vibration.py +167 -0
- digichem/test/__init__.py +6 -0
- digichem/test/conftest.py +4 -0
- digichem/test/test_basis.py +71 -0
- digichem/test/test_calculate.py +30 -0
- digichem/test/test_config.py +78 -0
- digichem/test/test_cube.py +369 -0
- digichem/test/test_exception.py +16 -0
- digichem/test/test_file.py +104 -0
- digichem/test/test_image.py +337 -0
- digichem/test/test_input.py +64 -0
- digichem/test/test_parsing.py +79 -0
- digichem/test/test_prattle.py +36 -0
- digichem/test/test_result.py +489 -0
- digichem/test/test_translate.py +112 -0
- digichem/test/util.py +207 -0
- digichem/translate.py +591 -0
- digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
- digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
- digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
- digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
- 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,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
|
+
|
digichem/result/angle.py
ADDED
|
@@ -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
|
+
|