firecode 1.0.0__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 (59) hide show
  1. firecode/TEST_NOTEBOOK.ipynb +3940 -0
  2. firecode/__init__.py +0 -0
  3. firecode/__main__.py +118 -0
  4. firecode/_gaussian.py +97 -0
  5. firecode/algebra.py +405 -0
  6. firecode/ase_manipulations.py +879 -0
  7. firecode/atropisomer_module.py +516 -0
  8. firecode/automep.py +130 -0
  9. firecode/calculators/__init__.py +29 -0
  10. firecode/calculators/_gaussian.py +98 -0
  11. firecode/calculators/_mopac.py +242 -0
  12. firecode/calculators/_openbabel.py +154 -0
  13. firecode/calculators/_orca.py +129 -0
  14. firecode/calculators/_xtb.py +786 -0
  15. firecode/concurrent_test.py +119 -0
  16. firecode/embedder.py +2590 -0
  17. firecode/embedder_options.py +577 -0
  18. firecode/embeds.py +881 -0
  19. firecode/errors.py +65 -0
  20. firecode/graph_manipulations.py +333 -0
  21. firecode/hypermolecule_class.py +364 -0
  22. firecode/mep_relaxer.py +199 -0
  23. firecode/modify_settings.py +186 -0
  24. firecode/mprof.py +65 -0
  25. firecode/multiembed.py +148 -0
  26. firecode/nci.py +186 -0
  27. firecode/numba_functions.py +260 -0
  28. firecode/operators.py +776 -0
  29. firecode/optimization_methods.py +609 -0
  30. firecode/parameters.py +84 -0
  31. firecode/pka.py +275 -0
  32. firecode/profiler.py +17 -0
  33. firecode/pruning.py +421 -0
  34. firecode/pt.py +32 -0
  35. firecode/quotes.json +6651 -0
  36. firecode/quotes.py +9 -0
  37. firecode/reactive_atoms_classes.py +666 -0
  38. firecode/references.py +11 -0
  39. firecode/rmsd.py +74 -0
  40. firecode/settings.py +75 -0
  41. firecode/solvents.py +126 -0
  42. firecode/tests/C2F2H4.xyz +10 -0
  43. firecode/tests/C2H4.xyz +8 -0
  44. firecode/tests/CH3Cl.xyz +7 -0
  45. firecode/tests/HCOOH.xyz +7 -0
  46. firecode/tests/HCOOOH.xyz +8 -0
  47. firecode/tests/chelotropic.txt +3 -0
  48. firecode/tests/cyclical.txt +3 -0
  49. firecode/tests/dihedral.txt +2 -0
  50. firecode/tests/string.txt +3 -0
  51. firecode/tests/trimolecular.txt +9 -0
  52. firecode/tests.py +151 -0
  53. firecode/torsion_module.py +1035 -0
  54. firecode/utils.py +541 -0
  55. firecode-1.0.0.dist-info/LICENSE +165 -0
  56. firecode-1.0.0.dist-info/METADATA +321 -0
  57. firecode-1.0.0.dist-info/RECORD +59 -0
  58. firecode-1.0.0.dist-info/WHEEL +5 -0
  59. firecode-1.0.0.dist-info/top_level.txt +1 -0
firecode/__init__.py ADDED
File without changes
firecode/__main__.py ADDED
@@ -0,0 +1,118 @@
1
+ # coding=utf-8
2
+ '''
3
+
4
+ FIRECODE: Filtering Refiner and Embedder for Conformationally Dense Ensembles
5
+ Copyright (C) 2021-2024 Nicolò Tampellini
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ https://github.com/ntampellini/firecode
18
+
19
+ Nicolo' Tampellini - nicolo.tampellini@yale.edu
20
+
21
+ '''
22
+ import argparse
23
+ import os
24
+ import sys
25
+
26
+ __version__ = '1.0.0'
27
+
28
+ if __name__ == '__main__':
29
+
30
+
31
+ usage = '''\n\n 🔥 python -m firecode [-h] [-s] [-t] inputfile [-n NAME]
32
+
33
+ positional arguments:
34
+ inputfile Input filename, can be any text file.
35
+
36
+ optional arguments:
37
+ -h, --help Show this help message and exit.
38
+ -s, --setup Guided setup of the calculation settings.
39
+ -t, --test Perform some tests to check the software setup.
40
+ -n, --name NAME Specify a custom name for the run.
41
+ -cl,--command_line Read instructions from the command line instead of from an input file.
42
+ -c, --cite Print citation links.
43
+ -p, --profile Profile the run through cProfiler.
44
+ -b, --benchmark FILE Benchmark the geometry optimization of FILE (.xyz) to get the optimal number of procs per job.
45
+ --procs Number of processors to be used by each optimization job.
46
+ '''
47
+
48
+ parser = argparse.ArgumentParser(usage=usage)
49
+ parser.add_argument("-s", "--setup", help="Guided setup of the calculation settings.", action="store_true")
50
+ parser.add_argument("-t", "--test", help="Perform some tests to check the software setup.", action="store_true")
51
+ parser.add_argument("-cl", "--command_line", help="Read instructions from the command line instead of from an input file.", action="store")
52
+ parser.add_argument("inputfile", help="Input filename, can be any text file.", action='store', nargs='?', default=None)
53
+ parser.add_argument("-n", "--name", help="Specify a custom name for the run.", action='store', required=False)
54
+ parser.add_argument("-c", "--cite", help="Print the appropriate document links for citation purposes.", action='store_true', required=False)
55
+ parser.add_argument("-p", "--profile", help="Profile the run through cProfiler.", action='store_true', required=False)
56
+ parser.add_argument("-b", "--benchmark", help=("Benchmark the geometry optimization of FILE to get the optimal number " +
57
+ "of procs per job."), action='store', required=False, default=False)
58
+ # parser.add_argument("-r", "--restart", help="Restarts previous run from an embedder.pickle object.", action='store', required=False, default=False)
59
+ parser.add_argument("--procs", help="Number of processors to be used by each optimization job.", action='store', required=False, default=None)
60
+
61
+ args = parser.parse_args()
62
+
63
+ if (not (args.test or args.setup or args.command_line or args.benchmark)) and args.inputfile is None:
64
+ parser.error("One of the following arguments are required: inputfile, -t, -s, -b.\n")
65
+
66
+ if args.benchmark:
67
+ from firecode.concurrent_test import run_concurrent_test
68
+ run_concurrent_test(args.benchmark)
69
+ sys.exit()
70
+
71
+ if args.setup:
72
+ from firecode.modify_settings import run_setup
73
+ run_setup()
74
+ sys.exit()
75
+
76
+ if args.cite:
77
+ print('No citation link is available for FIRECODE yet. You can link to the code on https://www.github.com/ntampellini/firecode')
78
+ sys.exit()
79
+
80
+ if args.test:
81
+ from firecode.tests import run_tests
82
+ run_tests()
83
+ sys.exit()
84
+
85
+ if args.command_line:
86
+
87
+ filename = 'input_firecode.txt'
88
+ with open(filename, 'w') as f:
89
+ f.write(args.command_line)
90
+
91
+ args.inputfile = filename
92
+
93
+ filename = os.path.realpath(args.inputfile)
94
+
95
+ from firecode.embedder import Embedder
96
+
97
+ if args.profile:
98
+ from firecode.profiler import profiled_wrapper
99
+ profiled_wrapper(filename, args.name)
100
+ sys.exit()
101
+
102
+ # if args.restart:
103
+ # import pickle
104
+ # with open(args.restart, 'rb') as _f:
105
+ # embedder = pickle.load(_f)
106
+ # initialize embedder from pickle file
107
+
108
+ # embedder.run()
109
+ # run the program
110
+
111
+ # import faulthandler
112
+ # faulthandler.enable()
113
+
114
+ embedder = Embedder(filename, stamp=args.name, procs=args.procs)
115
+ # initialize embedder from input file
116
+
117
+ embedder.run()
118
+ # run the program
firecode/_gaussian.py ADDED
@@ -0,0 +1,97 @@
1
+ # coding=utf-8
2
+ '''
3
+ FIRECODE: Filtering Refiner and Embedder for Conformationally Dense Ensembles
4
+ Copyright (C) 2021-2024 Nicolò Tampellini
5
+
6
+ SPDX-License-Identifier: LGPL-3.0-or-later
7
+
8
+ This program is free software: you can redistribute it and/or modify
9
+ it under the terms of the GNU Lesser General Public License as published by
10
+ the Free Software Foundation, either version 3 of the License, or
11
+ (at your option) any later version.
12
+
13
+ This program is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU Lesser General Public License for more details.
17
+
18
+ You should have received a copy of the GNU Lesser General Public License
19
+ along with this program. If not, see
20
+ https://www.gnu.org/licenses/lgpl-3.0.en.html#license-text.
21
+
22
+ '''
23
+ import sys
24
+ from subprocess import DEVNULL, STDOUT, check_call
25
+
26
+ from firecode.settings import COMMANDS, MEM_GB
27
+ from firecode.solvents import get_solvent_line
28
+ from firecode.utils import clean_directory, pt, read_xyz
29
+
30
+
31
+ def gaussian_opt(coords, atomnos, constrained_indices=None, method='PM6', procs=1, solvent=None, title='temp', read_output=True, **kwargs):
32
+ '''
33
+ This function writes a Gaussian .inp file, runs it with the subprocess
34
+ module and reads its output.
35
+
36
+ :params coords: array of shape (n,3) with cartesian coordinates for atoms.
37
+ :params atomnos: array of atomic numbers for atoms.
38
+ :params constrained_indices: array of shape (n,2), with the indices
39
+ of atomic pairs to be constrained.
40
+ :params method: string, specifiyng the first line of keywords for the MOPAC input file.
41
+ :params title: string, used as a file name and job title for the mopac input file.
42
+ :params read_output: Whether to read the output file and return anything.
43
+ '''
44
+
45
+ s = ''
46
+
47
+ if MEM_GB is not None:
48
+ if MEM_GB < 1:
49
+ s += f'%mem={int(1000*MEM_GB)}MB\n'
50
+ else:
51
+ s += f'%mem={MEM_GB}GB\n'
52
+
53
+ if procs > 1:
54
+ s += f'%nprocshared={procs}\n'
55
+
56
+ s = '# opt ' if constrained_indices is not None else '# opt=modredundant '
57
+ s += method
58
+
59
+ if solvent is not None:
60
+ s += ' ' + get_solvent_line(solvent, 'GAUSSIAN', method)
61
+
62
+ s += '\n\nGaussian input generated by FIRECODE\n\n0 1\n'
63
+
64
+ for i, atom in enumerate(coords):
65
+ s += '%s % .6f % .6f % .6f\n' % (pt[atomnos[i]].symbol, atom[0], atom[1], atom[2])
66
+
67
+ s += '\n'
68
+
69
+ if constrained_indices is not None:
70
+
71
+ for a, b in constrained_indices:
72
+ s += 'B %s %s F\n' % (a+1, b+1) # Gaussian numbering starts at 1
73
+
74
+ s = ''.join(s)
75
+ with open(f'{title}.com', 'w') as f:
76
+ f.write(s)
77
+
78
+ try:
79
+ check_call(f'{COMMANDS["GAUSSIAN"]} {title}.com'.split(), stdout=DEVNULL, stderr=STDOUT)
80
+
81
+ except KeyboardInterrupt:
82
+ print('KeyboardInterrupt requested by user. Quitting.')
83
+ sys.exit()
84
+
85
+ if read_output:
86
+
87
+ try:
88
+ data = read_xyz(f'{title}.out')
89
+ opt_coords = data.atomcoords[0]
90
+ energy = data.scfenergies[-1] * 23.060548867 # eV to kcal/mol
91
+
92
+ clean_directory((f'{title}.com',))
93
+
94
+ return opt_coords, energy, True
95
+
96
+ except FileNotFoundError:
97
+ return None, None, False
firecode/algebra.py ADDED
@@ -0,0 +1,405 @@
1
+ '''
2
+
3
+ FIRECODE: Filtering Refiner and Embedder for Conformationally Dense Ensembles
4
+ Copyright (C) 2021-2024 Nicolò Tampellini
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ '''
17
+ from math import sqrt
18
+
19
+ import numba as nb
20
+ import numpy as np
21
+
22
+
23
+ @nb.njit
24
+ def dihedral(p):
25
+ '''
26
+ Returns dihedral angle in degrees from 4 3D vecs
27
+ Praxeolitic formula: 1 sqrt, 1 cross product
28
+
29
+ '''
30
+ p0 = p[0]
31
+ p1 = p[1]
32
+ p2 = p[2]
33
+ p3 = p[3]
34
+
35
+ b0 = -1.0*(p1 - p0)
36
+ b1 = p2 - p1
37
+ b2 = p3 - p2
38
+
39
+ # normalize b1 so that it does not influence magnitude of vector
40
+ # rejections that come next
41
+ b1 /= norm_of(b1)
42
+
43
+ # vector rejections
44
+ # v = projection of b0 onto plane perpendicular to b1
45
+ # = b0 minus component that aligns with b1
46
+ # w = projection of b2 onto plane perpendicular to b1
47
+ # = b2 minus component that aligns with b1
48
+ v = b0 - np.dot(b0, b1)*b1
49
+ w = b2 - np.dot(b2, b1)*b1
50
+
51
+ # angle between v and w in a plane is the torsion angle
52
+ # v and w may not be normalized but that's fine since tan is y/x
53
+ x = np.dot(v, w)
54
+ y = np.dot(np.cross(b1, v), w)
55
+
56
+ return np.degrees(np.arctan2(y, x))
57
+
58
+ @nb.njit
59
+ def vec_angle(v1, v2):
60
+ v1_u = norm(v1)
61
+ v2_u = norm(v2)
62
+ return np.arccos(clip(np.dot(v1_u, v2_u), -1.0, 1.0))*180/np.pi
63
+
64
+ @nb.njit
65
+ def clip(n, lower, higher):
66
+ '''
67
+ jittable version of np.clip for single values
68
+ '''
69
+ if n > higher:
70
+ return higher
71
+ elif n < lower:
72
+ return lower
73
+ else:
74
+ return n
75
+
76
+ @nb.njit
77
+ def point_angle(p1, p2, p3):
78
+ return np.arccos(np.clip(norm(p1 - p2) @ norm(p3 - p2), -1.0, 1.0))*180/np.pi
79
+
80
+ @nb.njit
81
+ def norm(vec):
82
+ '''
83
+ Returns the normalized vector.
84
+ Reasonably faster than Numpy version.
85
+ Only for 3D vectors.
86
+ '''
87
+ return vec / sqrt((vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2]))
88
+
89
+ @nb.njit
90
+ def norm_of(vec):
91
+ '''
92
+ Returns the norm of the vector.
93
+ Faster than Numpy version, but
94
+ only compatible with 3D vectors.
95
+ '''
96
+ return sqrt((vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2]))
97
+
98
+ @nb.njit(fastmath=True)
99
+ def all_dists(A, B):
100
+ assert A.shape[1]==B.shape[1]
101
+ C=np.empty((A.shape[0],B.shape[0]),A.dtype)
102
+ I_BLK=32
103
+ J_BLK=32
104
+
105
+ #workaround to get the right datatype for acc
106
+ init_val_arr=np.zeros(1,A.dtype)
107
+ init_val=init_val_arr[0]
108
+
109
+ #Blocking and partial unrolling
110
+ #Beneficial if the second dimension is large -> computationally bound problem
111
+ #
112
+ for ii in nb.prange(A.shape[0]//I_BLK):
113
+ for jj in range(B.shape[0]//J_BLK):
114
+ for i in range(I_BLK//4):
115
+ for j in range(J_BLK//2):
116
+ acc_0=init_val
117
+ acc_1=init_val
118
+ acc_2=init_val
119
+ acc_3=init_val
120
+ acc_4=init_val
121
+ acc_5=init_val
122
+ acc_6=init_val
123
+ acc_7=init_val
124
+ for k in range(A.shape[1]):
125
+ acc_0+=(A[ii*I_BLK+i*4+0,k] - B[jj*J_BLK+j*2+0,k])**2
126
+ acc_1+=(A[ii*I_BLK+i*4+0,k] - B[jj*J_BLK+j*2+1,k])**2
127
+ acc_2+=(A[ii*I_BLK+i*4+1,k] - B[jj*J_BLK+j*2+0,k])**2
128
+ acc_3+=(A[ii*I_BLK+i*4+1,k] - B[jj*J_BLK+j*2+1,k])**2
129
+ acc_4+=(A[ii*I_BLK+i*4+2,k] - B[jj*J_BLK+j*2+0,k])**2
130
+ acc_5+=(A[ii*I_BLK+i*4+2,k] - B[jj*J_BLK+j*2+1,k])**2
131
+ acc_6+=(A[ii*I_BLK+i*4+3,k] - B[jj*J_BLK+j*2+0,k])**2
132
+ acc_7+=(A[ii*I_BLK+i*4+3,k] - B[jj*J_BLK+j*2+1,k])**2
133
+ C[ii*I_BLK+i*4+0,jj*J_BLK+j*2+0]=np.sqrt(acc_0)
134
+ C[ii*I_BLK+i*4+0,jj*J_BLK+j*2+1]=np.sqrt(acc_1)
135
+ C[ii*I_BLK+i*4+1,jj*J_BLK+j*2+0]=np.sqrt(acc_2)
136
+ C[ii*I_BLK+i*4+1,jj*J_BLK+j*2+1]=np.sqrt(acc_3)
137
+ C[ii*I_BLK+i*4+2,jj*J_BLK+j*2+0]=np.sqrt(acc_4)
138
+ C[ii*I_BLK+i*4+2,jj*J_BLK+j*2+1]=np.sqrt(acc_5)
139
+ C[ii*I_BLK+i*4+3,jj*J_BLK+j*2+0]=np.sqrt(acc_6)
140
+ C[ii*I_BLK+i*4+3,jj*J_BLK+j*2+1]=np.sqrt(acc_7)
141
+ #Remainder j
142
+ for i in range(I_BLK):
143
+ for j in range((B.shape[0]//J_BLK)*J_BLK,B.shape[0]):
144
+ acc_0=init_val
145
+ for k in range(A.shape[1]):
146
+ acc_0+=(A[ii*I_BLK+i,k] - B[j,k])**2
147
+ C[ii*I_BLK+i,j]=np.sqrt(acc_0)
148
+
149
+ #Remainder i
150
+ for i in range((A.shape[0]//I_BLK)*I_BLK,A.shape[0]):
151
+ for j in range(B.shape[0]):
152
+ acc_0=init_val
153
+ for k in range(A.shape[1]):
154
+ acc_0+=(A[i,k] - B[j,k])**2
155
+ C[i,j]=np.sqrt(acc_0)
156
+
157
+ return C
158
+
159
+ @nb.njit
160
+ def kronecker_delta(i, j) -> int:
161
+ if i == j:
162
+ return 1
163
+ return 0
164
+
165
+ @nb.njit
166
+ def get_inertia_moments(coords, masses):
167
+ '''
168
+ Returns the diagonal of the diagonalized inertia tensor, that is
169
+ a shape (3,) array with the moments of inertia along the main axes.
170
+ (I_x, I_y and largest I_z last)
171
+ '''
172
+
173
+ coords -= center_of_mass(coords, masses)
174
+ inertia_moment_matrix = np.array([[0.,0.,0.],
175
+ [0.,0.,0.],
176
+ [0.,0.,0.]])
177
+
178
+ for i in range(3):
179
+ for j in range(3):
180
+ k = kronecker_delta(i,j)
181
+ inertia_moment_matrix[i][j] = sum([masses[n]*((norm_of(coords[n])**2)*k - coords[n][i]*coords[n][j])
182
+ for n, _ in enumerate(coords)])
183
+
184
+ inertia_moment_matrix = diagonalize(inertia_moment_matrix)
185
+
186
+ return np.diag(inertia_moment_matrix)
187
+
188
+ @nb.njit
189
+ def diagonalize(A):
190
+ eigenvalues_of_A, eigenvectors_of_A = np.linalg.eig(A)
191
+ B = eigenvectors_of_A[:,np.abs(eigenvalues_of_A).argsort()]
192
+ diagonal_matrix= np.dot(np.linalg.inv(B), np.dot(A, B))
193
+ return diagonal_matrix
194
+
195
+ @nb.njit
196
+ def center_of_mass(coords, masses):
197
+ '''
198
+ Returns the center of mass for the atomic system.
199
+ '''
200
+ total_mass = sum([masses[i] for i in range(len(coords))])
201
+ w = np.array([0.,0.,0.])
202
+ for i in range(len(coords)):
203
+ w += coords[i]*masses[i]
204
+ return w / total_mass
205
+
206
+ @nb.njit
207
+ def internal_mean(arr):
208
+ '''
209
+ same as np.mean(arr, axis=1), but jitted
210
+ since numba does not support kwargs in np.mean
211
+ '''
212
+ assert len(arr.shape) == 3
213
+
214
+ out = np.zeros((arr.shape[0], arr.shape[2]), dtype=arr.dtype)
215
+ dim = arr.shape[1]
216
+ for i, vecs in enumerate(arr):
217
+ for v in vecs:
218
+ out[i] += v
219
+ return out / dim
220
+
221
+ @nb.njit
222
+ def vec_mean(arr):
223
+ '''
224
+ same as np.mean(arr, axis=0), but jitted
225
+ since numba does not support kwargs in np.mean
226
+ '''
227
+ assert len(arr.shape) == 2
228
+
229
+ out = np.zeros(arr.shape[1], dtype=arr.dtype)
230
+ dim = arr.shape[0]
231
+
232
+ for v in arr:
233
+ out += v
234
+
235
+ arr /= dim
236
+
237
+ return out
238
+
239
+ @nb.njit
240
+ def align_vec_pair(ref, tgt):
241
+ '''
242
+ ref, tgt: iterables of two 3D vectors each
243
+
244
+ return: rotation matrix that when applied to tgt,
245
+ optimally aligns it to ref
246
+ '''
247
+
248
+ B = np.zeros((3,3))
249
+ for i in range(3):
250
+ for k in range(3):
251
+ tot = 0
252
+ for j in range(2):
253
+ tot += ref[j][i]*tgt[j][k]
254
+ B[i,k] = tot
255
+
256
+ u, s, vh = np.linalg.svd(B)
257
+
258
+ # Correct improper rotation if necessary (as in Kabsch algorithm)
259
+ if np.linalg.det(u @ vh) < 0:
260
+ s[-1] = -s[-1]
261
+ u[:, -1] = -u[:, -1]
262
+
263
+ return np.ascontiguousarray(np.dot(u, vh))
264
+
265
+ @nb.njit
266
+ def quaternion_to_rotation_matrix(Q):
267
+ """
268
+ Covert a quaternion into a full three-dimensional rotation matrix.
269
+
270
+ Input
271
+ :param Q: A 4 element array representing the quaternion (q0,q1,q2,q3)
272
+
273
+ Output
274
+ :return: A 3x3 element matrix representing the full 3D rotation matrix.
275
+ This rotation matrix converts a point in the local reference
276
+ frame to a point in the global reference frame.
277
+ """
278
+ # Extract the values from Q (adjusting for scalar last in input)
279
+ q0 = Q[3]
280
+ q1 = Q[0]
281
+ q2 = Q[1]
282
+ q3 = Q[2]
283
+
284
+ # First row of the rotation matrix
285
+ r00 = 2 * (q0 * q0 + q1 * q1) - 1
286
+ r01 = 2 * (q1 * q2 - q0 * q3)
287
+ r02 = 2 * (q1 * q3 + q0 * q2)
288
+
289
+ # Second row of the rotation matrix
290
+ r10 = 2 * (q1 * q2 + q0 * q3)
291
+ r11 = 2 * (q0 * q0 + q2 * q2) - 1
292
+ r12 = 2 * (q2 * q3 - q0 * q1)
293
+
294
+ # Third row of the rotation matrix
295
+ r20 = 2 * (q1 * q3 - q0 * q2)
296
+ r21 = 2 * (q2 * q3 + q0 * q1)
297
+ r22 = 2 * (q0 * q0 + q3 * q3) - 1
298
+
299
+ # 3x3 rotation matrix
300
+ rot_matrix = np.array([[r00, r01, r02],
301
+ [r10, r11, r12],
302
+ [r20, r21, r22]])
303
+
304
+ return np.ascontiguousarray(rot_matrix)
305
+
306
+ @nb.njit
307
+ def rot_mat_from_pointer(pointer, angle):
308
+ '''
309
+ Returns the rotation matrix that rotates a system around the given pointer
310
+ of angle degrees. The algorithm is based on scipy quaternions.
311
+ :params pointer: a 3D vector
312
+ :params angle: an int/float, in degrees
313
+ :return rotation_matrix: matrix that applied to a point, rotates it along the pointer
314
+ '''
315
+ assert pointer.shape[0] == 3
316
+
317
+ pointer = norm(pointer)
318
+ angle *= np.pi/180
319
+ quat = np.array([np.sin(angle/2)*pointer[0],
320
+ np.sin(angle/2)*pointer[1],
321
+ np.sin(angle/2)*pointer[2],
322
+ np.cos(angle/2)])
323
+ # normalized quaternion, scalar last (i j k w)
324
+
325
+ return quaternion_to_rotation_matrix(quat)
326
+
327
+ @nb.njit(nb.int32[:,:](nb.int32[:]))
328
+ def cart_prod_idx(sizes: np.ndarray):
329
+ """Generates ids tuples for a cartesian product"""
330
+ assert len(sizes) >= 2
331
+ tuples_count = np.prod(sizes)
332
+ tuples = np.zeros((tuples_count, len(sizes)), dtype=np.int32)
333
+ tuple_idx = 0
334
+ # stores the current combination
335
+ current_tuple = np.zeros(len(sizes))
336
+ while tuple_idx < tuples_count:
337
+ tuples[tuple_idx] = current_tuple
338
+ current_tuple[0] += 1
339
+ # using a condition here instead of including this in the inner loop
340
+ # to gain a bit of speed: this is going to be tested each iteration,
341
+ # and starting a loop to have it end right away is a bit silly
342
+ if current_tuple[0] == sizes[0]:
343
+ # the reset to 0 and subsequent increment amount to carrying
344
+ # the number to the higher "power"
345
+ current_tuple[0] = 0
346
+ current_tuple[1] += 1
347
+ for i in range(1, len(sizes) - 1):
348
+ if current_tuple[i] == sizes[i]:
349
+ # same as before, but in a loop, since this is going
350
+ # to get run less often
351
+ current_tuple[i + 1] += 1
352
+ current_tuple[i] = 0
353
+ else:
354
+ break
355
+ tuple_idx += 1
356
+ return tuples
357
+
358
+ @nb.njit
359
+ def vector_cartesian_product(x, y):
360
+ '''
361
+ Cartesian product, but with vectors instead of indices
362
+ '''
363
+ indices = cart_prod_idx(np.asarray((x.shape[0], y.shape[0]), dtype=np.int32))
364
+ dim = x.shape[-1] if len(x.shape) > 1 else 1
365
+ new_arr = np.zeros((*indices.shape, dim), dtype=x.dtype)
366
+ for i, (x_, y_) in enumerate(indices):
367
+ new_arr[i][0] = x[x_]
368
+ new_arr[i][1] = y[y_]
369
+ return np.ascontiguousarray(new_arr)
370
+
371
+ @nb.njit
372
+ def transform_coords(coords, rot, pos):
373
+ '''
374
+ Returns the rotated and tranlated
375
+ coordinates. Slightly faster than
376
+ Numpy, uses memory-contiguous arrays.
377
+ '''
378
+ t = np.transpose(coords)
379
+ m = rot @ t
380
+ f = np.transpose(m)
381
+ return f + pos
382
+
383
+ @nb.njit
384
+ def get_alignment_matrix(p, q):
385
+ '''
386
+ Returns the rotation matrix that aligns
387
+ vectors q to p (Kabsch algorithm)
388
+ Assumes centered vector sets (mean is origin)
389
+ '''
390
+
391
+ # calculate the covariance matrix
392
+ cov_mat = np.ascontiguousarray(p.T) @ q
393
+ # cov_mat = np.transpose(p) * q
394
+
395
+ # Compute the SVD
396
+ v, _, w = np.linalg.svd(cov_mat)
397
+ d = (np.linalg.det(v) * np.linalg.det(w)) < 0.0
398
+
399
+ if d:
400
+ v[:, -1] = -v[:, -1]
401
+
402
+ # Create Rotation matrix u
403
+ rot_mat = np.dot(v, w)
404
+
405
+ return rot_mat