patme 0.4.4__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.

Potentially problematic release.


This version of patme might be problematic. Click here for more details.

Files changed (46) hide show
  1. patme/__init__.py +52 -0
  2. patme/buildtools/__init__.py +7 -0
  3. patme/buildtools/rce_releasecreator.py +336 -0
  4. patme/buildtools/release.py +26 -0
  5. patme/femtools/__init__.py +5 -0
  6. patme/femtools/abqmsgfilechecker.py +137 -0
  7. patme/femtools/fecall.py +1092 -0
  8. patme/geometry/__init__.py +0 -0
  9. patme/geometry/area.py +124 -0
  10. patme/geometry/coordinatesystem.py +635 -0
  11. patme/geometry/intersect.py +284 -0
  12. patme/geometry/line.py +183 -0
  13. patme/geometry/misc.py +420 -0
  14. patme/geometry/plane.py +464 -0
  15. patme/geometry/rotate.py +244 -0
  16. patme/geometry/scale.py +152 -0
  17. patme/geometry/shape2d.py +50 -0
  18. patme/geometry/transformations.py +1831 -0
  19. patme/geometry/translate.py +139 -0
  20. patme/mechanics/__init__.py +4 -0
  21. patme/mechanics/loads.py +435 -0
  22. patme/mechanics/material.py +1260 -0
  23. patme/service/__init__.py +7 -0
  24. patme/service/decorators.py +85 -0
  25. patme/service/duration.py +96 -0
  26. patme/service/exceptionhook.py +104 -0
  27. patme/service/exceptions.py +36 -0
  28. patme/service/io/__init__.py +3 -0
  29. patme/service/io/basewriter.py +122 -0
  30. patme/service/logger.py +375 -0
  31. patme/service/mathutils.py +108 -0
  32. patme/service/misc.py +71 -0
  33. patme/service/moveimports.py +217 -0
  34. patme/service/stringutils.py +419 -0
  35. patme/service/systemutils.py +290 -0
  36. patme/sshtools/__init__.py +3 -0
  37. patme/sshtools/cara.py +435 -0
  38. patme/sshtools/clustercaller.py +420 -0
  39. patme/sshtools/facluster.py +350 -0
  40. patme/sshtools/sshcall.py +168 -0
  41. patme-0.4.4.dist-info/LICENSE +21 -0
  42. patme-0.4.4.dist-info/LICENSES/MIT.txt +9 -0
  43. patme-0.4.4.dist-info/METADATA +168 -0
  44. patme-0.4.4.dist-info/RECORD +46 -0
  45. patme-0.4.4.dist-info/WHEEL +4 -0
  46. patme-0.4.4.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1831 @@
1
+ # Copyright (c) 2006, Christoph Gohlke
2
+ # Copyright (c) 2006-2010, The Regents of the University of California
3
+ #
4
+ # SPDX-License-Identifier: MIT
5
+ # All rights reserved.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright
11
+ # notice, this list of conditions and the following disclaimer.
12
+ # * Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ # * Neither the name of the copyright holders nor the names of any
16
+ # contributors may be used to endorse or promote products derived
17
+ # from this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ # POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ """Homogeneous Transformation Matrices and Quaternions.
32
+
33
+ A library for calculating 4x4 matrices for translating, rotating, reflecting,
34
+ scaling, shearing, projecting, orthogonalizing, and superimposing arrays of
35
+ 3D homogeneous coordinates as well as for converting between rotation matrices,
36
+ Euler angles, and quaternions. Also includes an Arcball control object and
37
+ functions to decompose transformation matrices.
38
+
39
+ :Authors:
40
+ `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`__,
41
+ Laboratory for Fluorescence Dynamics, University of California, Irvine
42
+
43
+ :Version: 2010.04.10
44
+
45
+ Requirements
46
+ ------------
47
+
48
+ * `Python 2.6 or 3.1 <http://www.python.org>`__
49
+ * `Numpy 1.4 <http://numpy.scipy.org>`__
50
+ * `transformations.c 2010.04.10 <http://www.lfd.uci.edu/~gohlke/>`__
51
+ (optional implementation of some functions in C)
52
+
53
+ Notes
54
+ -----
55
+
56
+ The API is not stable yet and is expected to change between revisions.
57
+
58
+ This Python code is not optimized for speed. Refer to the transformations.c
59
+ module for a faster implementation of some functions.
60
+
61
+ Documentation in HTML format can be generated with epydoc.
62
+
63
+ Matrices (M) can be inverted using numpy.linalg.inv(M), concatenated using
64
+ numpy.dot(M0, M1), or used to transform homogeneous coordinates (v) using
65
+ numpy.dot(M, v) for shape (4, *) "point of arrays", respectively
66
+ numpy.dot(v, M.T) for shape (*, 4) "array of points".
67
+
68
+ Use the transpose of transformation matrices for OpenGL glMultMatrixd().
69
+
70
+ Calculations are carried out with numpy.float64 precision.
71
+
72
+ Vector, point, quaternion, and matrix function arguments are expected to be
73
+ "array like", i.e. tuple, list, or numpy arrays.
74
+
75
+ Return types are numpy arrays unless specified otherwise.
76
+
77
+ Angles are in radians unless specified otherwise.
78
+
79
+ Quaternions w+ix+jy+kz are represented as [w, x, y, z].
80
+
81
+ A triple of Euler angles can be applied/interpreted in 24 ways, which can
82
+ be specified using a 4 character string or encoded 4-tuple:
83
+
84
+ *Axes 4-string*: e.g. 'sxyz' or 'ryxy'
85
+
86
+ - first character : rotations are applied to 's'tatic or 'r'otating frame
87
+ - remaining characters : successive rotation axis 'x', 'y', or 'z'
88
+
89
+ *Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1)
90
+
91
+ - inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix.
92
+ - parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed
93
+ by 'z', or 'z' is followed by 'x'. Otherwise odd (1).
94
+ - repetition : first and last axis are same (1) or different (0).
95
+ - frame : rotations are applied to static (0) or rotating (1) frame.
96
+
97
+ References
98
+ ----------
99
+
100
+ (1) Matrices and transformations. Ronald Goldman.
101
+ In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990.
102
+ (2) More matrices and transformations: shear and pseudo-perspective.
103
+ Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
104
+ (3) Decomposing a matrix into simple transformations. Spencer Thomas.
105
+ In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
106
+ (4) Recovering the data from the transformation matrix. Ronald Goldman.
107
+ In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991.
108
+ (5) Euler angle conversion. Ken Shoemake.
109
+ In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994.
110
+ (6) Arcball rotation control. Ken Shoemake.
111
+ In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994.
112
+ (7) Representing attitude: Euler angles, unit quaternions, and rotation
113
+ vectors. James Diebel. 2006.
114
+ (8) A discussion of the solution for the best rotation to relate two sets
115
+ of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828.
116
+ (9) Closed-form solution of absolute orientation using unit quaternions.
117
+ BKP Horn. J Opt Soc Am A. 1987. 4(4):629-642.
118
+ (10) Quaternions. Ken Shoemake.
119
+ http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf
120
+ (11) From quaternion to matrix and back. JMP van Waveren. 2005.
121
+ http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm
122
+ (12) Uniform random rotations. Ken Shoemake.
123
+ In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992.
124
+ (13) Quaternion in molecular modeling. CFF Karney.
125
+ J Mol Graph Mod, 25(5):595-604
126
+ (14) New method for extracting the quaternion from a rotation matrix.
127
+ Itzhack Y Bar-Itzhack, J Guid Contr Dynam. 2000. 23(6): 1085-1087.
128
+
129
+ Examples
130
+ --------
131
+
132
+ >>> alpha, beta, gamma = 0.123, -1.234, 2.345
133
+ >>> origin, xaxis, yaxis, zaxis = (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)
134
+ >>> I = identity_matrix()
135
+ >>> Rx = rotation_matrix(alpha, xaxis)
136
+ >>> Ry = rotation_matrix(beta, yaxis)
137
+ >>> Rz = rotation_matrix(gamma, zaxis)
138
+ >>> R = concatenate_matrices(Rx, Ry, Rz)
139
+ >>> euler = euler_from_matrix(R, 'rxyz')
140
+ >>> numpy.allclose([alpha, beta, gamma], euler)
141
+ True
142
+ >>> Re = euler_matrix(alpha, beta, gamma, 'rxyz')
143
+ >>> is_same_transform(R, Re)
144
+ True
145
+ >>> al, be, ga = euler_from_matrix(Re, 'rxyz')
146
+ >>> is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz'))
147
+ True
148
+ >>> qx = quaternion_about_axis(alpha, xaxis)
149
+ >>> qy = quaternion_about_axis(beta, yaxis)
150
+ >>> qz = quaternion_about_axis(gamma, zaxis)
151
+ >>> q = quaternion_multiply(qx, qy)
152
+ >>> q = quaternion_multiply(q, qz)
153
+ >>> Rq = quaternion_matrix(q)
154
+ >>> is_same_transform(R, Rq)
155
+ True
156
+ >>> S = scale_matrix(1.23, origin)
157
+ >>> T = translation_matrix((1, 2, 3))
158
+ >>> Z = shear_matrix(beta, xaxis, origin, zaxis)
159
+ >>> R = random_rotation_matrix(numpy.random.rand(3))
160
+ >>> M = concatenate_matrices(T, R, Z, S)
161
+ >>> scale, shear, angles, trans, persp = decompose_matrix(M)
162
+ >>> numpy.allclose(scale, 1.23)
163
+ True
164
+ >>> numpy.allclose(trans, (1, 2, 3))
165
+ True
166
+ >>> numpy.allclose(shear, (0, math.tan(beta), 0))
167
+ True
168
+ >>> is_same_transform(R, euler_matrix(axes='sxyz', *angles))
169
+ True
170
+ >>> M1 = compose_matrix(scale, shear, angles, trans, persp)
171
+ >>> is_same_transform(M, M1)
172
+ True
173
+
174
+ """
175
+
176
+
177
+ import math
178
+ import warnings
179
+
180
+ import numpy
181
+
182
+
183
+ def identity_matrix():
184
+ """Return 4x4 identity/unit matrix.
185
+
186
+ >>> I = identity_matrix()
187
+ >>> numpy.allclose(I, numpy.dot(I, I))
188
+ True
189
+ >>> numpy.sum(I), numpy.trace(I)
190
+ (4.0, 4.0)
191
+ >>> numpy.allclose(I, numpy.identity(4, dtype=numpy.float64))
192
+ True
193
+
194
+ """
195
+ return numpy.identity(4, dtype=numpy.float64)
196
+
197
+
198
+ def translation_matrix(direction):
199
+ """Return matrix to translate by direction vector.
200
+
201
+ >>> v = numpy.random.random(3) - 0.5
202
+ >>> numpy.allclose(v, translation_matrix(v)[:3, 3])
203
+ True
204
+
205
+ """
206
+ M = numpy.identity(4)
207
+ M[:3, 3] = direction[:3]
208
+ return M
209
+
210
+
211
+ def translation_from_matrix(matrix):
212
+ """Return translation vector from translation matrix.
213
+
214
+ >>> v0 = numpy.random.random(3) - 0.5
215
+ >>> v1 = translation_from_matrix(translation_matrix(v0))
216
+ >>> numpy.allclose(v0, v1)
217
+ True
218
+
219
+ """
220
+ return numpy.array(matrix, copy=False)[:3, 3].copy()
221
+
222
+
223
+ def reflection_matrix(point, normal):
224
+ """Return matrix to mirror at plane defined by point and normal vector.
225
+
226
+ >>> v0 = numpy.random.random(4) - 0.5
227
+ >>> v0[3] = 1.0
228
+ >>> v1 = numpy.random.random(3) - 0.5
229
+ >>> R = reflection_matrix(v0, v1)
230
+ >>> numpy.allclose(2., numpy.trace(R))
231
+ True
232
+ >>> numpy.allclose(v0, numpy.dot(R, v0))
233
+ True
234
+ >>> v2 = v0.copy()
235
+ >>> v2[:3] += v1
236
+ >>> v3 = v0.copy()
237
+ >>> v2[:3] -= v1
238
+ >>> numpy.allclose(v2, numpy.dot(R, v3))
239
+ True
240
+
241
+ """
242
+ normal = unit_vector(normal[:3])
243
+ M = numpy.identity(4)
244
+ M[:3, :3] -= 2.0 * numpy.outer(normal, normal)
245
+ M[:3, 3] = (2.0 * numpy.dot(point[:3], normal)) * normal
246
+ return M
247
+
248
+
249
+ def reflection_from_matrix(matrix):
250
+ """Return mirror plane point and normal vector from reflection matrix.
251
+
252
+ >>> v0 = numpy.random.random(3) - 0.5
253
+ >>> v1 = numpy.random.random(3) - 0.5
254
+ >>> M0 = reflection_matrix(v0, v1)
255
+ >>> point, normal = reflection_from_matrix(M0)
256
+ >>> M1 = reflection_matrix(point, normal)
257
+ >>> is_same_transform(M0, M1)
258
+ True
259
+
260
+ """
261
+ M = numpy.array(matrix, dtype=numpy.float64, copy=False)
262
+ # normal: unit eigenvector corresponding to eigenvalue -1
263
+ l, V = numpy.linalg.eig(M[:3, :3])
264
+ i = numpy.where(abs(numpy.real(l) + 1.0) < 1e-8)[0]
265
+ if not len(i):
266
+ raise ValueError("no unit eigenvector corresponding to eigenvalue -1")
267
+ normal = numpy.real(V[:, i[0]]).squeeze()
268
+ # point: any unit eigenvector corresponding to eigenvalue 1
269
+ l, V = numpy.linalg.eig(M)
270
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
271
+ if not len(i):
272
+ raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
273
+ point = numpy.real(V[:, i[-1]]).squeeze()
274
+ point /= point[3]
275
+ return point, normal
276
+
277
+
278
+ def rotation_matrix(angle, direction, point=None):
279
+ """Return matrix to rotate about axis defined by point and direction.
280
+
281
+ >>> import random
282
+ >>> R = rotation_matrix(math.pi/2.0, [0, 0, 1], [1, 0, 0])
283
+ >>> numpy.allclose(numpy.dot(R, [0, 0, 0, 1]), [ 1., -1., 0., 1.])
284
+ True
285
+ >>> angle = (random.random() - 0.5) * (2*math.pi)
286
+ >>> direc = numpy.random.random(3) - 0.5
287
+ >>> point = numpy.random.random(3) - 0.5
288
+ >>> R0 = rotation_matrix(angle, direc, point)
289
+ >>> R1 = rotation_matrix(angle-2*math.pi, direc, point)
290
+ >>> is_same_transform(R0, R1)
291
+ True
292
+ >>> R0 = rotation_matrix(angle, direc, point)
293
+ >>> R1 = rotation_matrix(-angle, -direc, point)
294
+ >>> is_same_transform(R0, R1)
295
+ True
296
+ >>> I = numpy.identity(4, numpy.float64)
297
+ >>> numpy.allclose(I, rotation_matrix(math.pi*2, direc))
298
+ True
299
+ >>> numpy.allclose(2., numpy.trace(rotation_matrix(math.pi/2,
300
+ ... direc, point)))
301
+ True
302
+
303
+ """
304
+ sina = math.sin(angle)
305
+ cosa = math.cos(angle)
306
+ direction = unit_vector(direction[:3])
307
+ # rotation matrix around unit vector
308
+ R = numpy.array(((cosa, 0.0, 0.0), (0.0, cosa, 0.0), (0.0, 0.0, cosa)), dtype=numpy.float64)
309
+ R += numpy.outer(direction, direction) * (1.0 - cosa)
310
+ direction *= sina
311
+ R += numpy.array(
312
+ ((0.0, -direction[2], direction[1]), (direction[2], 0.0, -direction[0]), (-direction[1], direction[0], 0.0)),
313
+ dtype=numpy.float64,
314
+ )
315
+ M = numpy.identity(4)
316
+ M[:3, :3] = R
317
+ if point is not None:
318
+ # rotation not around origin
319
+ point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
320
+ M[:3, 3] = point - numpy.dot(R, point)
321
+ return M
322
+
323
+
324
+ def rotation_from_matrix(matrix):
325
+ """Return rotation angle and axis from rotation matrix.
326
+
327
+ >>> import random
328
+ >>> import math
329
+ >>> angle = (random.random() - 0.5) * (2*math.pi)
330
+ >>> direc = numpy.random.random(3) - 0.5
331
+ >>> point = numpy.random.random(3) - 0.5
332
+ >>> R0 = rotation_matrix(angle, direc, point)
333
+ >>> angle, direc, point = rotation_from_matrix(R0)
334
+ >>> R1 = rotation_matrix(angle, direc, point)
335
+ >>> is_same_transform(R0, R1)
336
+ True
337
+
338
+ """
339
+ R = numpy.array(matrix, dtype=numpy.float64, copy=False)
340
+ R33 = R[:3, :3]
341
+ # direction: unit eigenvector of R33 corresponding to eigenvalue of 1
342
+ l, W = numpy.linalg.eig(R33.T)
343
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
344
+ if not len(i):
345
+ raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
346
+ direction = numpy.real(W[:, i[-1]]).squeeze()
347
+ # point: unit eigenvector of R33 corresponding to eigenvalue of 1
348
+ l, Q = numpy.linalg.eig(R)
349
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
350
+ if not len(i):
351
+ raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
352
+ point = numpy.real(Q[:, i[-1]]).squeeze()
353
+ point /= point[3]
354
+ # rotation angle depending on direction
355
+ cosa = (numpy.trace(R33) - 1.0) / 2.0
356
+ if abs(direction[2]) > 1e-8:
357
+ sina = (R[1, 0] + (cosa - 1.0) * direction[0] * direction[1]) / direction[2]
358
+ elif abs(direction[1]) > 1e-8:
359
+ sina = (R[0, 2] + (cosa - 1.0) * direction[0] * direction[2]) / direction[1]
360
+ else:
361
+ sina = (R[2, 1] + (cosa - 1.0) * direction[1] * direction[2]) / direction[0]
362
+ angle = math.atan2(sina, cosa)
363
+ return angle, direction, point
364
+
365
+
366
+ def scale_matrix(factor, origin=None, direction=None):
367
+ """Return matrix to scale by factor around origin in direction.
368
+
369
+ Use factor -1 for point symmetry.
370
+
371
+ >>> import random
372
+ >>> v = (numpy.random.rand(4, 5) - 0.5) * 20.0
373
+ >>> v[3] = 1.0
374
+ >>> S = scale_matrix(-1.234)
375
+ >>> numpy.allclose(numpy.dot(S, v)[:3], -1.234*v[:3])
376
+ True
377
+ >>> factor = random.random() * 10 - 5
378
+ >>> origin = numpy.random.random(3) - 0.5
379
+ >>> direct = numpy.random.random(3) - 0.5
380
+ >>> S = scale_matrix(factor, origin)
381
+ >>> S = scale_matrix(factor, origin, direct)
382
+
383
+ """
384
+ if direction is None:
385
+ # uniform scaling
386
+ M = numpy.array(
387
+ ((factor, 0.0, 0.0, 0.0), (0.0, factor, 0.0, 0.0), (0.0, 0.0, factor, 0.0), (0.0, 0.0, 0.0, 1.0)),
388
+ dtype=numpy.float64,
389
+ )
390
+ if origin is not None:
391
+ M[:3, 3] = origin[:3]
392
+ M[:3, 3] *= 1.0 - factor
393
+ else:
394
+ # nonuniform scaling
395
+ direction = unit_vector(direction[:3])
396
+ factor = 1.0 - factor
397
+ M = numpy.identity(4)
398
+ M[:3, :3] -= factor * numpy.outer(direction, direction)
399
+ if origin is not None:
400
+ M[:3, 3] = (factor * numpy.dot(origin[:3], direction)) * direction
401
+ return M
402
+
403
+
404
+ def scale_from_matrix(matrix):
405
+ """Return scaling factor, origin and direction from scaling matrix.
406
+
407
+ >>> import random
408
+ >>> factor = random.random() * 10 - 5
409
+ >>> origin = numpy.random.random(3) - 0.5
410
+ >>> direct = numpy.random.random(3) - 0.5
411
+ >>> S0 = scale_matrix(factor, origin)
412
+ >>> factor, origin, direction = scale_from_matrix(S0)
413
+ >>> S1 = scale_matrix(factor, origin, direction)
414
+ >>> is_same_transform(S0, S1)
415
+ True
416
+ >>> S0 = scale_matrix(factor, origin, direct)
417
+ >>> factor, origin, direction = scale_from_matrix(S0)
418
+ >>> S1 = scale_matrix(factor, origin, direction)
419
+ >>> is_same_transform(S0, S1)
420
+ True
421
+
422
+ """
423
+ M = numpy.array(matrix, dtype=numpy.float64, copy=False)
424
+ M33 = M[:3, :3]
425
+ factor = numpy.trace(M33) - 2.0
426
+ try:
427
+ # direction: unit eigenvector corresponding to eigenvalue factor
428
+ l, V = numpy.linalg.eig(M33)
429
+ i = numpy.where(abs(numpy.real(l) - factor) < 1e-8)[0][0]
430
+ direction = numpy.real(V[:, i]).squeeze()
431
+ direction /= vector_norm(direction)
432
+ except IndexError:
433
+ # uniform scaling
434
+ factor = (factor + 2.0) / 3.0
435
+ direction = None
436
+ # origin: any eigenvector corresponding to eigenvalue 1
437
+ l, V = numpy.linalg.eig(M)
438
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
439
+ if not len(i):
440
+ raise ValueError("no eigenvector corresponding to eigenvalue 1")
441
+ origin = numpy.real(V[:, i[-1]]).squeeze()
442
+ origin /= origin[3]
443
+ return factor, origin, direction
444
+
445
+
446
+ def projection_matrix(point, normal, direction=None, perspective=None, pseudo=False):
447
+ """Return matrix to project onto plane defined by point and normal.
448
+
449
+ Using either perspective point, projection direction, or none of both.
450
+
451
+ If pseudo is True, perspective projections will preserve relative depth
452
+ such that Perspective = dot(Orthogonal, PseudoPerspective).
453
+
454
+ >>> P = projection_matrix((0, 0, 0), (1, 0, 0))
455
+ >>> numpy.allclose(P[1:, 1:], numpy.identity(4)[1:, 1:])
456
+ True
457
+ >>> point = numpy.random.random(3) - 0.5
458
+ >>> normal = numpy.random.random(3) - 0.5
459
+ >>> direct = numpy.random.random(3) - 0.5
460
+ >>> persp = numpy.random.random(3) - 0.5
461
+ >>> P0 = projection_matrix(point, normal)
462
+ >>> P1 = projection_matrix(point, normal, direction=direct)
463
+ >>> P2 = projection_matrix(point, normal, perspective=persp)
464
+ >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True)
465
+ >>> is_same_transform(P2, numpy.dot(P0, P3))
466
+ True
467
+ >>> P = projection_matrix((3, 0, 0), (1, 1, 0), (1, 0, 0))
468
+ >>> v0 = (numpy.random.rand(4, 5) - 0.5) * 20.0
469
+ >>> v0[3] = 1.0
470
+ >>> v1 = numpy.dot(P, v0)
471
+ >>> numpy.allclose(v1[1], v0[1])
472
+ True
473
+ >>> numpy.allclose(v1[0], 3.0-v1[1])
474
+ True
475
+
476
+ """
477
+ M = numpy.identity(4)
478
+ point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
479
+ normal = unit_vector(normal[:3])
480
+ if perspective is not None:
481
+ # perspective projection
482
+ perspective = numpy.array(perspective[:3], dtype=numpy.float64, copy=False)
483
+ M[0, 0] = M[1, 1] = M[2, 2] = numpy.dot(perspective - point, normal)
484
+ M[:3, :3] -= numpy.outer(perspective, normal)
485
+ if pseudo:
486
+ # preserve relative depth
487
+ M[:3, :3] -= numpy.outer(normal, normal)
488
+ M[:3, 3] = numpy.dot(point, normal) * (perspective + normal)
489
+ else:
490
+ M[:3, 3] = numpy.dot(point, normal) * perspective
491
+ M[3, :3] = -normal
492
+ M[3, 3] = numpy.dot(perspective, normal)
493
+ elif direction is not None:
494
+ # parallel projection
495
+ direction = numpy.array(direction[:3], dtype=numpy.float64, copy=False)
496
+ scale = numpy.dot(direction, normal)
497
+ M[:3, :3] -= numpy.outer(direction, normal) / scale
498
+ M[:3, 3] = direction * (numpy.dot(point, normal) / scale)
499
+ else:
500
+ # orthogonal projection
501
+ M[:3, :3] -= numpy.outer(normal, normal)
502
+ M[:3, 3] = numpy.dot(point, normal) * normal
503
+ return M
504
+
505
+
506
+ def projection_from_matrix(matrix, pseudo=False):
507
+ """Return projection plane and perspective point from projection matrix.
508
+
509
+ Return values are same as arguments for projection_matrix function:
510
+ point, normal, direction, perspective, and pseudo.
511
+
512
+ >>> point = numpy.random.random(3) - 0.5
513
+ >>> normal = numpy.random.random(3) - 0.5
514
+ >>> direct = numpy.random.random(3) - 0.5
515
+ >>> persp = numpy.random.random(3) - 0.5
516
+ >>> P0 = projection_matrix(point, normal)
517
+ >>> result = projection_from_matrix(P0)
518
+ >>> P1 = projection_matrix(*result)
519
+ >>> is_same_transform(P0, P1)
520
+ True
521
+ >>> P0 = projection_matrix(point, normal, direct)
522
+ >>> result = projection_from_matrix(P0)
523
+ >>> P1 = projection_matrix(*result)
524
+ >>> is_same_transform(P0, P1)
525
+ True
526
+ >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False)
527
+ >>> result = projection_from_matrix(P0, pseudo=False)
528
+ >>> P1 = projection_matrix(*result)
529
+ >>> is_same_transform(P0, P1)
530
+ True
531
+ >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True)
532
+ >>> result = projection_from_matrix(P0, pseudo=True)
533
+ >>> P1 = projection_matrix(*result)
534
+ >>> is_same_transform(P0, P1)
535
+ True
536
+
537
+ """
538
+ M = numpy.array(matrix, dtype=numpy.float64, copy=False)
539
+ M33 = M[:3, :3]
540
+ l, V = numpy.linalg.eig(M)
541
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
542
+ if not pseudo and len(i):
543
+ # point: any eigenvector corresponding to eigenvalue 1
544
+ point = numpy.real(V[:, i[-1]]).squeeze()
545
+ point /= point[3]
546
+ # direction: unit eigenvector corresponding to eigenvalue 0
547
+ l, V = numpy.linalg.eig(M33)
548
+ i = numpy.where(abs(numpy.real(l)) < 1e-8)[0]
549
+ if not len(i):
550
+ raise ValueError("no eigenvector corresponding to eigenvalue 0")
551
+ direction = numpy.real(V[:, i[0]]).squeeze()
552
+ direction /= vector_norm(direction)
553
+ # normal: unit eigenvector of M33.T corresponding to eigenvalue 0
554
+ l, V = numpy.linalg.eig(M33.T)
555
+ i = numpy.where(abs(numpy.real(l)) < 1e-8)[0]
556
+ if len(i):
557
+ # parallel projection
558
+ normal = numpy.real(V[:, i[0]]).squeeze()
559
+ normal /= vector_norm(normal)
560
+ return point, normal, direction, None, False
561
+ else:
562
+ # orthogonal projection, where normal equals direction vector
563
+ return point, direction, None, None, False
564
+ else:
565
+ # perspective projection
566
+ i = numpy.where(abs(numpy.real(l)) > 1e-8)[0]
567
+ if not len(i):
568
+ raise ValueError("no eigenvector not corresponding to eigenvalue 0")
569
+ point = numpy.real(V[:, i[-1]]).squeeze()
570
+ point /= point[3]
571
+ normal = -M[3, :3]
572
+ perspective = M[:3, 3] / numpy.dot(point[:3], normal)
573
+ if pseudo:
574
+ perspective -= normal
575
+ return point, normal, None, perspective, pseudo
576
+
577
+
578
+ def clip_matrix(left, right, bottom, top, near, far, perspective=False):
579
+ """Return matrix to obtain normalized device coordinates from frustrum.
580
+
581
+ The frustrum bounds are axis-aligned along x (left, right),
582
+ y (bottom, top) and z (near, far).
583
+
584
+ Normalized device coordinates are in range [-1, 1] if coordinates are
585
+ inside the frustrum.
586
+
587
+ If perspective is True the frustrum is a truncated pyramid with the
588
+ perspective point at origin and direction along z axis, otherwise an
589
+ orthographic canonical view volume (a box).
590
+
591
+ Homogeneous coordinates transformed by the perspective clip matrix
592
+ need to be dehomogenized (devided by w coordinate).
593
+
594
+ >>> frustrum = numpy.random.rand(6)
595
+ >>> frustrum[1] += frustrum[0]
596
+ >>> frustrum[3] += frustrum[2]
597
+ >>> frustrum[5] += frustrum[4]
598
+ >>> M = clip_matrix(perspective=False, *frustrum)
599
+ >>> numpy.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0])
600
+ array([-1., -1., -1., 1.])
601
+ >>> numpy.dot(M, [frustrum[1], frustrum[3], frustrum[5], 1.0])
602
+ array([1., 1., 1., 1.])
603
+ >>> M = clip_matrix(perspective=True, *frustrum)
604
+ >>> v = numpy.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0])
605
+ >>> v / v[3]
606
+ array([-1., -1., -1., 1.])
607
+ >>> v = numpy.dot(M, [frustrum[1], frustrum[3], frustrum[4], 1.0])
608
+ >>> v / v[3]
609
+ array([ 1., 1., -1., 1.])
610
+
611
+ """
612
+ if left >= right or bottom >= top or near >= far:
613
+ raise ValueError("invalid frustrum")
614
+ if perspective:
615
+ if near <= _EPS:
616
+ raise ValueError("invalid frustrum: near <= 0")
617
+ t = 2.0 * near
618
+ M = (
619
+ (-t / (right - left), 0.0, (right + left) / (right - left), 0.0),
620
+ (0.0, -t / (top - bottom), (top + bottom) / (top - bottom), 0.0),
621
+ (0.0, 0.0, -(far + near) / (far - near), t * far / (far - near)),
622
+ (0.0, 0.0, -1.0, 0.0),
623
+ )
624
+ else:
625
+ M = (
626
+ (2.0 / (right - left), 0.0, 0.0, (right + left) / (left - right)),
627
+ (0.0, 2.0 / (top - bottom), 0.0, (top + bottom) / (bottom - top)),
628
+ (0.0, 0.0, 2.0 / (far - near), (far + near) / (near - far)),
629
+ (0.0, 0.0, 0.0, 1.0),
630
+ )
631
+ return numpy.array(M, dtype=numpy.float64)
632
+
633
+
634
+ def shear_matrix(angle, direction, point, normal):
635
+ """Return matrix to shear by angle along direction vector on shear plane.
636
+
637
+ The shear plane is defined by a point and normal vector. The direction
638
+ vector must be orthogonal to the plane's normal vector.
639
+
640
+ A point P is transformed by the shear matrix into P" such that
641
+ the vector P-P" is parallel to the direction vector and its extent is
642
+ given by the angle of P-P'-P", where P' is the orthogonal projection
643
+ of P onto the shear plane.
644
+
645
+ >>> import random
646
+ >>> angle = (random.random() - 0.5) * 4*math.pi
647
+ >>> direct = numpy.random.random(3) - 0.5
648
+ >>> point = numpy.random.random(3) - 0.5
649
+ >>> normal = numpy.cross(direct, numpy.random.random(3))
650
+ >>> S = shear_matrix(angle, direct, point, normal)
651
+ >>> numpy.allclose(1.0, numpy.linalg.det(S))
652
+ True
653
+
654
+ """
655
+ normal = unit_vector(normal[:3])
656
+ direction = unit_vector(direction[:3])
657
+ if abs(numpy.dot(normal, direction)) > 1e-6:
658
+ raise ValueError("direction and normal vectors are not orthogonal")
659
+ angle = math.tan(angle)
660
+ M = numpy.identity(4)
661
+ M[:3, :3] += angle * numpy.outer(direction, normal)
662
+ M[:3, 3] = -angle * numpy.dot(point[:3], normal) * direction
663
+ return M
664
+
665
+
666
+ def shear_from_matrix(matrix):
667
+ """Return shear angle, direction and plane from shear matrix.
668
+
669
+ >>> import random
670
+ >>> angle = (random.random() - 0.5) * 4*math.pi
671
+ >>> direct = numpy.random.random(3) - 0.5
672
+ >>> point = numpy.random.random(3) - 0.5
673
+ >>> normal = numpy.cross(direct, numpy.random.random(3))
674
+ >>> S0 = shear_matrix(angle, direct, point, normal)
675
+ >>> angle, direct, point, normal = shear_from_matrix(S0)
676
+ >>> # the following does not always work somehow
677
+ >>> #S1 = shear_matrix(angle, direct, point, normal)
678
+ >>> #is_same_transform(S0, S1)
679
+ True
680
+
681
+ """
682
+ M = numpy.array(matrix, dtype=numpy.float64, copy=False)
683
+ M33 = M[:3, :3]
684
+ # normal: cross independent eigenvectors corresponding to the eigenvalue 1
685
+ l, V = numpy.linalg.eig(M33)
686
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-4)[0]
687
+ if len(i) < 2:
688
+ raise ValueError("no two linear independent eigenvectors found %s" % l)
689
+ V = numpy.real(V[:, i]).squeeze().T
690
+ lenorm = -1.0
691
+ for i0, i1 in ((0, 1), (0, 2), (1, 2)):
692
+ n = numpy.cross(V[i0], V[i1])
693
+ l = vector_norm(n)
694
+ if l > lenorm:
695
+ lenorm = l
696
+ normal = n
697
+ normal /= lenorm
698
+ # direction and angle
699
+ direction = numpy.dot(M33 - numpy.identity(3), normal)
700
+ angle = vector_norm(direction)
701
+ direction /= angle
702
+ angle = math.atan(angle)
703
+ # point: eigenvector corresponding to eigenvalue 1
704
+ l, V = numpy.linalg.eig(M)
705
+ i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
706
+ if not len(i):
707
+ raise ValueError("no eigenvector corresponding to eigenvalue 1")
708
+ point = numpy.real(V[:, i[-1]]).squeeze()
709
+ point /= point[3]
710
+ return angle, direction, point, normal
711
+
712
+
713
+ def decompose_matrix(matrix):
714
+ """Return sequence of transformations from transformation matrix.
715
+
716
+ matrix : array_like
717
+ Non-degenerative homogeneous transformation matrix
718
+
719
+ Return tuple of:
720
+ scale : vector of 3 scaling factors
721
+ shear : list of shear factors for x-y, x-z, y-z axes
722
+ angles : list of Euler angles about static x, y, z axes
723
+ translate : translation vector along x, y, z axes
724
+ perspective : perspective partition of matrix
725
+
726
+ Raise ValueError if matrix is of wrong type or degenerative.
727
+
728
+ >>> T0 = translation_matrix((1, 2, 3))
729
+ >>> scale, shear, angles, trans, persp = decompose_matrix(T0)
730
+ >>> T1 = translation_matrix(trans)
731
+ >>> numpy.allclose(T0, T1)
732
+ True
733
+ >>> S = scale_matrix(0.123)
734
+ >>> scale, shear, angles, trans, persp = decompose_matrix(S)
735
+ >>> scale[0]
736
+ 0.123
737
+ >>> R0 = euler_matrix(1, 2, 3)
738
+ >>> scale, shear, angles, trans, persp = decompose_matrix(R0)
739
+ >>> R1 = euler_matrix(*angles)
740
+ >>> numpy.allclose(R0, R1)
741
+ True
742
+
743
+ """
744
+ M = numpy.array(matrix, dtype=numpy.float64, copy=True).T
745
+ if abs(M[3, 3]) < _EPS:
746
+ raise ValueError("M[3, 3] is zero")
747
+ M /= M[3, 3]
748
+ P = M.copy()
749
+ P[:, 3] = 0, 0, 0, 1
750
+ if not numpy.linalg.det(P):
751
+ raise ValueError("matrix is singular")
752
+
753
+ scale = numpy.zeros((3,), dtype=numpy.float64)
754
+ shear = [0, 0, 0]
755
+ angles = [0, 0, 0]
756
+
757
+ if any(abs(M[:3, 3]) > _EPS):
758
+ perspective = numpy.dot(M[:, 3], numpy.linalg.inv(P.T))
759
+ M[:, 3] = 0, 0, 0, 1
760
+ else:
761
+ perspective = numpy.array((0, 0, 0, 1), dtype=numpy.float64)
762
+
763
+ translate = M[3, :3].copy()
764
+ M[3, :3] = 0
765
+
766
+ row = M[:3, :3].copy()
767
+ scale[0] = vector_norm(row[0])
768
+ row[0] /= scale[0]
769
+ shear[0] = numpy.dot(row[0], row[1])
770
+ row[1] -= row[0] * shear[0]
771
+ scale[1] = vector_norm(row[1])
772
+ row[1] /= scale[1]
773
+ shear[0] /= scale[1]
774
+ shear[1] = numpy.dot(row[0], row[2])
775
+ row[2] -= row[0] * shear[1]
776
+ shear[2] = numpy.dot(row[1], row[2])
777
+ row[2] -= row[1] * shear[2]
778
+ scale[2] = vector_norm(row[2])
779
+ row[2] /= scale[2]
780
+ shear[1:] /= scale[2]
781
+
782
+ if numpy.dot(row[0], numpy.cross(row[1], row[2])) < 0:
783
+ scale *= -1
784
+ row *= -1
785
+
786
+ angles[1] = math.asin(-row[0, 2])
787
+ if math.cos(angles[1]):
788
+ angles[0] = math.atan2(row[1, 2], row[2, 2])
789
+ angles[2] = math.atan2(row[0, 1], row[0, 0])
790
+ else:
791
+ # angles[0] = math.atan2(row[1, 0], row[1, 1])
792
+ angles[0] = math.atan2(-row[2, 1], row[1, 1])
793
+ angles[2] = 0.0
794
+
795
+ return scale, shear, angles, translate, perspective
796
+
797
+
798
+ def compose_matrix(scale=None, shear=None, angles=None, translate=None, perspective=None):
799
+ """Return transformation matrix from sequence of transformations.
800
+
801
+ This is the inverse of the decompose_matrix function.
802
+
803
+ Sequence of transformations:
804
+ scale : vector of 3 scaling factors
805
+ shear : list of shear factors for x-y, x-z, y-z axes
806
+ angles : list of Euler angles about static x, y, z axes
807
+ translate : translation vector along x, y, z axes
808
+ perspective : perspective partition of matrix
809
+
810
+ >>> scale = numpy.random.random(3) - 0.5
811
+ >>> shear = numpy.random.random(3) - 0.5
812
+ >>> angles = (numpy.random.random(3) - 0.5) * (2*math.pi)
813
+ >>> trans = numpy.random.random(3) - 0.5
814
+ >>> persp = numpy.random.random(4) - 0.5
815
+ >>> M0 = compose_matrix(scale, shear, angles, trans, persp)
816
+ >>> result = decompose_matrix(M0)
817
+ >>> M1 = compose_matrix(*result)
818
+ >>> is_same_transform(M0, M1)
819
+ True
820
+
821
+ """
822
+ M = numpy.identity(4)
823
+ if perspective is not None:
824
+ P = numpy.identity(4)
825
+ P[3, :] = perspective[:4]
826
+ M = numpy.dot(M, P)
827
+ if translate is not None:
828
+ T = numpy.identity(4)
829
+ T[:3, 3] = translate[:3]
830
+ M = numpy.dot(M, T)
831
+ if angles is not None:
832
+ R = euler_matrix(angles[0], angles[1], angles[2], "sxyz")
833
+ M = numpy.dot(M, R)
834
+ if shear is not None:
835
+ Z = numpy.identity(4)
836
+ Z[1, 2] = shear[2]
837
+ Z[0, 2] = shear[1]
838
+ Z[0, 1] = shear[0]
839
+ M = numpy.dot(M, Z)
840
+ if scale is not None:
841
+ S = numpy.identity(4)
842
+ S[0, 0] = scale[0]
843
+ S[1, 1] = scale[1]
844
+ S[2, 2] = scale[2]
845
+ M = numpy.dot(M, S)
846
+ M /= M[3, 3]
847
+ return M
848
+
849
+
850
+ def orthogonalization_matrix(lengths, angles):
851
+ """Return orthogonalization matrix for crystallographic cell coordinates.
852
+
853
+ Angles are expected in degrees.
854
+
855
+ The de-orthogonalization matrix is the inverse.
856
+
857
+ >>> O = orthogonalization_matrix((10., 10., 10.), (90., 90., 90.))
858
+ >>> numpy.allclose(O[:3, :3], numpy.identity(3, float) * 10)
859
+ True
860
+ >>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7])
861
+ >>> numpy.allclose(numpy.sum(O), 43.063229)
862
+ True
863
+
864
+ """
865
+ a, b, c = lengths
866
+ angles = numpy.radians(angles)
867
+ sina, sinb, _ = numpy.sin(angles)
868
+ cosa, cosb, cosg = numpy.cos(angles)
869
+ co = (cosa * cosb - cosg) / (sina * sinb)
870
+ return numpy.array(
871
+ (
872
+ (a * sinb * math.sqrt(1.0 - co * co), 0.0, 0.0, 0.0),
873
+ (-a * sinb * co, b * sina, 0.0, 0.0),
874
+ (a * cosb, b * cosa, c, 0.0),
875
+ (0.0, 0.0, 0.0, 1.0),
876
+ ),
877
+ dtype=numpy.float64,
878
+ )
879
+
880
+
881
+ def superimposition_matrix(v0, v1, scaling=False, usesvd=True):
882
+ """Return matrix to transform given vector set into second vector set.
883
+
884
+ v0 and v1 are shape (3, *) or (4, *) arrays of at least 3 vectors.
885
+
886
+ If usesvd is True, the weighted sum of squared deviations (RMSD) is
887
+ minimized according to the algorithm by W. Kabsch [8]. Otherwise the
888
+ quaternion based algorithm by B. Horn [9] is used (slower when using
889
+ this Python implementation).
890
+
891
+ The returned matrix performs rotation, translation and uniform scaling
892
+ (if specified).
893
+
894
+ >>> import random
895
+ >>> v0 = numpy.random.rand(3, 10)
896
+ >>> M = superimposition_matrix(v0, v0)
897
+ >>> numpy.allclose(M, numpy.identity(4))
898
+ True
899
+ >>> R = random_rotation_matrix(numpy.random.random(3))
900
+ >>> v0 = ((1,0,0), (0,1,0), (0,0,1), (1,1,1))
901
+ >>> v1 = numpy.dot(R, v0)
902
+ >>> M = superimposition_matrix(v0, v1)
903
+ >>> numpy.allclose(v1, numpy.dot(M, v0))
904
+ True
905
+ >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20.0
906
+ >>> v0[3] = 1.0
907
+ >>> v1 = numpy.dot(R, v0)
908
+ >>> M = superimposition_matrix(v0, v1)
909
+ >>> numpy.allclose(v1, numpy.dot(M, v0))
910
+ True
911
+ >>> S = scale_matrix(random.random())
912
+ >>> T = translation_matrix(numpy.random.random(3)-0.5)
913
+ >>> M = concatenate_matrices(T, R, S)
914
+ >>> v1 = numpy.dot(M, v0)
915
+ >>> v0[:3] += numpy.random.normal(0.0, 1e-9, 300).reshape(3, -1)
916
+ >>> M = superimposition_matrix(v0, v1, scaling=True)
917
+ >>> numpy.allclose(v1, numpy.dot(M, v0))
918
+ True
919
+ >>> M = superimposition_matrix(v0, v1, scaling=True, usesvd=False)
920
+ >>> numpy.allclose(v1, numpy.dot(M, v0))
921
+ True
922
+ >>> v = numpy.empty((4, 100, 3), dtype=numpy.float64)
923
+ >>> v[:, :, 0] = v0
924
+ >>> M = superimposition_matrix(v0, v1, scaling=True, usesvd=False)
925
+ >>> numpy.allclose(v1, numpy.dot(M, v[:, :, 0]))
926
+ True
927
+
928
+ """
929
+ v0 = numpy.array(v0, dtype=numpy.float64, copy=False)[:3]
930
+ v1 = numpy.array(v1, dtype=numpy.float64, copy=False)[:3]
931
+
932
+ if v0.shape != v1.shape or v0.shape[1] < 3:
933
+ raise ValueError("vector sets are of wrong shape or type")
934
+
935
+ # move centroids to origin
936
+ t0 = numpy.mean(v0, axis=1)
937
+ t1 = numpy.mean(v1, axis=1)
938
+ v0 = v0 - t0.reshape(3, 1)
939
+ v1 = v1 - t1.reshape(3, 1)
940
+
941
+ if usesvd:
942
+ # Singular Value Decomposition of covariance matrix
943
+ u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T))
944
+ # rotation matrix from SVD orthonormal bases
945
+ R = numpy.dot(u, vh)
946
+ if numpy.linalg.det(R) < 0.0:
947
+ # R does not constitute right handed system
948
+ R -= numpy.outer(u[:, 2], vh[2, :] * 2.0)
949
+ s[-1] *= -1.0
950
+ # homogeneous transformation matrix
951
+ M = numpy.identity(4)
952
+ M[:3, :3] = R
953
+ else:
954
+ # compute symmetric matrix N
955
+ xx, yy, zz = numpy.sum(v0 * v1, axis=1)
956
+ xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1)
957
+ xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1)
958
+ N = (
959
+ (xx + yy + zz, 0.0, 0.0, 0.0),
960
+ (yz - zy, xx - yy - zz, 0.0, 0.0),
961
+ (zx - xz, xy + yx, -xx + yy - zz, 0.0),
962
+ (xy - yx, zx + xz, yz + zy, -xx - yy + zz),
963
+ )
964
+ # quaternion: eigenvector corresponding to most positive eigenvalue
965
+ l, V = numpy.linalg.eigh(N)
966
+ q = V[:, numpy.argmax(l)]
967
+ q /= vector_norm(q) # unit quaternion
968
+ # homogeneous transformation matrix
969
+ M = quaternion_matrix(q)
970
+
971
+ # scale: ratio of rms deviations from centroid
972
+ if scaling:
973
+ v0 *= v0
974
+ v1 *= v1
975
+ M[:3, :3] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0))
976
+
977
+ # translation
978
+ M[:3, 3] = t1
979
+ T = numpy.identity(4)
980
+ T[:3, 3] = -t0
981
+ M = numpy.dot(M, T)
982
+ return M
983
+
984
+
985
+ def euler_matrix(ai, aj, ak, axes="sxyz"):
986
+ """Return homogeneous rotation matrix from Euler angles and axis sequence.
987
+
988
+ ai, aj, ak : Euler's roll, pitch and yaw angles
989
+ axes : One of 24 axis sequences as string or encoded tuple
990
+
991
+ >>> R = euler_matrix(1, 2, 3, 'syxz')
992
+ >>> numpy.allclose(numpy.sum(R[0]), -1.34786452)
993
+ True
994
+ >>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1))
995
+ >>> numpy.allclose(numpy.sum(R[0]), -0.383436184)
996
+ True
997
+ >>> ai, aj, ak = (4.0*math.pi) * (numpy.random.random(3) - 0.5)
998
+ >>> for axes in _AXES2TUPLE.keys():
999
+ ... R = euler_matrix(ai, aj, ak, axes)
1000
+ >>> for axes in _TUPLE2AXES.keys():
1001
+ ... R = euler_matrix(ai, aj, ak, axes)
1002
+
1003
+ """
1004
+ try:
1005
+ firstaxis, parity, repetition, frame = _AXES2TUPLE[axes]
1006
+ except (AttributeError, KeyError):
1007
+ _ = _TUPLE2AXES[axes]
1008
+ firstaxis, parity, repetition, frame = axes
1009
+
1010
+ i = firstaxis
1011
+ j = _NEXT_AXIS[i + parity]
1012
+ k = _NEXT_AXIS[i - parity + 1]
1013
+
1014
+ if frame:
1015
+ ai, ak = ak, ai
1016
+ if parity:
1017
+ ai, aj, ak = -ai, -aj, -ak
1018
+
1019
+ si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak)
1020
+ ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak)
1021
+ cc, cs = ci * ck, ci * sk
1022
+ sc, ss = si * ck, si * sk
1023
+
1024
+ M = numpy.identity(4)
1025
+ if repetition:
1026
+ M[i, i] = cj
1027
+ M[i, j] = sj * si
1028
+ M[i, k] = sj * ci
1029
+ M[j, i] = sj * sk
1030
+ M[j, j] = -cj * ss + cc
1031
+ M[j, k] = -cj * cs - sc
1032
+ M[k, i] = -sj * ck
1033
+ M[k, j] = cj * sc + cs
1034
+ M[k, k] = cj * cc - ss
1035
+ else:
1036
+ M[i, i] = cj * ck
1037
+ M[i, j] = sj * sc - cs
1038
+ M[i, k] = sj * cc + ss
1039
+ M[j, i] = cj * sk
1040
+ M[j, j] = sj * ss + cc
1041
+ M[j, k] = sj * cs - sc
1042
+ M[k, i] = -sj
1043
+ M[k, j] = cj * si
1044
+ M[k, k] = cj * ci
1045
+ return M
1046
+
1047
+
1048
+ def euler_from_matrix(matrix, axes="sxyz"):
1049
+ """Return Euler angles from rotation matrix for specified axis sequence.
1050
+
1051
+ axes : One of 24 axis sequences as string or encoded tuple
1052
+
1053
+ Note that many Euler angle triplets can describe one matrix.
1054
+
1055
+ >>> R0 = euler_matrix(1, 2, 3, 'syxz')
1056
+ >>> al, be, ga = euler_from_matrix(R0, 'syxz')
1057
+ >>> R1 = euler_matrix(al, be, ga, 'syxz')
1058
+ >>> numpy.allclose(R0, R1)
1059
+ True
1060
+ >>> angles = (4.0*math.pi) * (numpy.random.random(3) - 0.5)
1061
+ >>> for axes in _AXES2TUPLE.keys():
1062
+ ... R0 = euler_matrix(axes=axes, *angles)
1063
+ ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes))
1064
+ ... if not numpy.allclose(R0, R1): print(axes, "failed")
1065
+
1066
+ """
1067
+ try:
1068
+ firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
1069
+ except (AttributeError, KeyError):
1070
+ _ = _TUPLE2AXES[axes]
1071
+ firstaxis, parity, repetition, frame = axes
1072
+
1073
+ i = firstaxis
1074
+ j = _NEXT_AXIS[i + parity]
1075
+ k = _NEXT_AXIS[i - parity + 1]
1076
+
1077
+ M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:3, :3]
1078
+ if repetition:
1079
+ sy = math.sqrt(M[i, j] * M[i, j] + M[i, k] * M[i, k])
1080
+ if sy > _EPS:
1081
+ ax = math.atan2(M[i, j], M[i, k])
1082
+ ay = math.atan2(sy, M[i, i])
1083
+ az = math.atan2(M[j, i], -M[k, i])
1084
+ else:
1085
+ ax = math.atan2(-M[j, k], M[j, j])
1086
+ ay = math.atan2(sy, M[i, i])
1087
+ az = 0.0
1088
+ else:
1089
+ cy = math.sqrt(M[i, i] * M[i, i] + M[j, i] * M[j, i])
1090
+ if cy > _EPS:
1091
+ ax = math.atan2(M[k, j], M[k, k])
1092
+ ay = math.atan2(-M[k, i], cy)
1093
+ az = math.atan2(M[j, i], M[i, i])
1094
+ else:
1095
+ ax = math.atan2(-M[j, k], M[j, j])
1096
+ ay = math.atan2(-M[k, i], cy)
1097
+ az = 0.0
1098
+
1099
+ if parity:
1100
+ ax, ay, az = -ax, -ay, -az
1101
+ if frame:
1102
+ ax, az = az, ax
1103
+ return ax, ay, az
1104
+
1105
+
1106
+ def euler_from_quaternion(quaternion, axes="sxyz"):
1107
+ """Return Euler angles from quaternion for specified axis sequence.
1108
+
1109
+ >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0])
1110
+ >>> numpy.allclose(angles, [0.123, 0, 0])
1111
+ True
1112
+
1113
+ """
1114
+ return euler_from_matrix(quaternion_matrix(quaternion), axes)
1115
+
1116
+
1117
+ def quaternion_from_euler(ai, aj, ak, axes="sxyz"):
1118
+ """Return quaternion from Euler angles and axis sequence.
1119
+
1120
+ ai, aj, ak : Euler's roll, pitch and yaw angles
1121
+ axes : One of 24 axis sequences as string or encoded tuple
1122
+
1123
+ >>> q = quaternion_from_euler(1, 2, 3, 'ryxz')
1124
+ >>> numpy.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435])
1125
+ True
1126
+
1127
+ """
1128
+ try:
1129
+ firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
1130
+ except (AttributeError, KeyError):
1131
+ _ = _TUPLE2AXES[axes]
1132
+ firstaxis, parity, repetition, frame = axes
1133
+
1134
+ i = firstaxis + 1
1135
+ j = _NEXT_AXIS[i + parity - 1] + 1
1136
+ k = _NEXT_AXIS[i - parity] + 1
1137
+
1138
+ if frame:
1139
+ ai, ak = ak, ai
1140
+ if parity:
1141
+ aj = -aj
1142
+
1143
+ ai /= 2.0
1144
+ aj /= 2.0
1145
+ ak /= 2.0
1146
+ ci = math.cos(ai)
1147
+ si = math.sin(ai)
1148
+ cj = math.cos(aj)
1149
+ sj = math.sin(aj)
1150
+ ck = math.cos(ak)
1151
+ sk = math.sin(ak)
1152
+ cc = ci * ck
1153
+ cs = ci * sk
1154
+ sc = si * ck
1155
+ ss = si * sk
1156
+
1157
+ quaternion = numpy.empty((4,), dtype=numpy.float64)
1158
+ if repetition:
1159
+ quaternion[0] = cj * (cc - ss)
1160
+ quaternion[i] = cj * (cs + sc)
1161
+ quaternion[j] = sj * (cc + ss)
1162
+ quaternion[k] = sj * (cs - sc)
1163
+ else:
1164
+ quaternion[0] = cj * cc + sj * ss
1165
+ quaternion[i] = cj * sc - sj * cs
1166
+ quaternion[j] = cj * ss + sj * cc
1167
+ quaternion[k] = cj * cs - sj * sc
1168
+ if parity:
1169
+ quaternion[j] *= -1
1170
+
1171
+ return quaternion
1172
+
1173
+
1174
+ def quaternion_about_axis(angle, axis):
1175
+ """Return quaternion for rotation about axis.
1176
+
1177
+ >>> q = quaternion_about_axis(0.123, (1, 0, 0))
1178
+ >>> numpy.allclose(q, [0.99810947, 0.06146124, 0, 0])
1179
+ True
1180
+
1181
+ """
1182
+ quaternion = numpy.zeros((4,), dtype=numpy.float64)
1183
+ quaternion[1] = axis[0]
1184
+ quaternion[2] = axis[1]
1185
+ quaternion[3] = axis[2]
1186
+ qlen = vector_norm(quaternion)
1187
+ if qlen > _EPS:
1188
+ quaternion *= math.sin(angle / 2.0) / qlen
1189
+ quaternion[0] = math.cos(angle / 2.0)
1190
+ return quaternion
1191
+
1192
+
1193
+ def quaternion_matrix(quaternion):
1194
+ """Return homogeneous rotation matrix from quaternion.
1195
+
1196
+ >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0])
1197
+ >>> numpy.allclose(M, rotation_matrix(0.123, (1, 0, 0)))
1198
+ True
1199
+ >>> M = quaternion_matrix([1, 0, 0, 0])
1200
+ >>> numpy.allclose(M, identity_matrix())
1201
+ True
1202
+ >>> M = quaternion_matrix([0, 1, 0, 0])
1203
+ >>> numpy.allclose(M, numpy.diag([1, -1, -1, 1]))
1204
+ True
1205
+
1206
+ """
1207
+ q = numpy.array(quaternion[:4], dtype=numpy.float64, copy=True)
1208
+ nq = numpy.dot(q, q)
1209
+ if nq < _EPS:
1210
+ return numpy.identity(4)
1211
+ q *= math.sqrt(2.0 / nq)
1212
+ q = numpy.outer(q, q)
1213
+ return numpy.array(
1214
+ (
1215
+ (1.0 - q[2, 2] - q[3, 3], q[1, 2] - q[3, 0], q[1, 3] + q[2, 0], 0.0),
1216
+ (q[1, 2] + q[3, 0], 1.0 - q[1, 1] - q[3, 3], q[2, 3] - q[1, 0], 0.0),
1217
+ (q[1, 3] - q[2, 0], q[2, 3] + q[1, 0], 1.0 - q[1, 1] - q[2, 2], 0.0),
1218
+ (0.0, 0.0, 0.0, 1.0),
1219
+ ),
1220
+ dtype=numpy.float64,
1221
+ )
1222
+
1223
+
1224
+ def quaternion_from_matrix(matrix, isprecise=False):
1225
+ """Return quaternion from rotation matrix.
1226
+
1227
+ If isprecise=True, the input matrix is assumed to be a precise rotation
1228
+ matrix and a faster algorithm is used.
1229
+
1230
+ >>> q = quaternion_from_matrix(identity_matrix(), True)
1231
+ >>> numpy.allclose(q, [1., 0., 0., 0.])
1232
+ True
1233
+ >>> q = quaternion_from_matrix(numpy.diag([1., -1., -1., 1.]))
1234
+ >>> numpy.allclose(q, [0, 1, 0, 0]) or numpy.allclose(q, [0, -1, 0, 0])
1235
+ True
1236
+ >>> R = rotation_matrix(0.123, (1, 2, 3))
1237
+ >>> q = quaternion_from_matrix(R, True)
1238
+ >>> numpy.allclose(q, [0.9981095, 0.0164262, 0.0328524, 0.0492786])
1239
+ True
1240
+ >>> R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0],
1241
+ ... [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]]
1242
+ >>> q = quaternion_from_matrix(R)
1243
+ >>> numpy.allclose(q, [0.19069, 0.43736, 0.87485, -0.083611])
1244
+ True
1245
+ >>> R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0],
1246
+ ... [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]]
1247
+ >>> q = quaternion_from_matrix(R)
1248
+ >>> numpy.allclose(q, [0.82336615, -0.13610694, 0.46344705, -0.29792603])
1249
+ True
1250
+ >>> R = random_rotation_matrix()
1251
+ >>> q = quaternion_from_matrix(R)
1252
+ >>> is_same_transform(R, quaternion_matrix(q))
1253
+ True
1254
+
1255
+ """
1256
+ M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:4, :4]
1257
+ if isprecise:
1258
+ q = numpy.empty((4,), dtype=numpy.float64)
1259
+ t = numpy.trace(M)
1260
+ if t > M[3, 3]:
1261
+ q[0] = t
1262
+ q[3] = M[1, 0] - M[0, 1]
1263
+ q[2] = M[0, 2] - M[2, 0]
1264
+ q[1] = M[2, 1] - M[1, 2]
1265
+ else:
1266
+ i, j, k = 1, 2, 3
1267
+ if M[1, 1] > M[0, 0]:
1268
+ i, j, k = 2, 3, 1
1269
+ if M[2, 2] > M[i, i]:
1270
+ i, j, k = 3, 1, 2
1271
+ t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3]
1272
+ q[i] = t
1273
+ q[j] = M[i, j] + M[j, i]
1274
+ q[k] = M[k, i] + M[i, k]
1275
+ q[3] = M[k, j] - M[j, k]
1276
+ q *= 0.5 / math.sqrt(t * M[3, 3])
1277
+ else:
1278
+ m00 = M[0, 0]
1279
+ m01 = M[0, 1]
1280
+ m02 = M[0, 2]
1281
+ m10 = M[1, 0]
1282
+ m11 = M[1, 1]
1283
+ m12 = M[1, 2]
1284
+ m20 = M[2, 0]
1285
+ m21 = M[2, 1]
1286
+ m22 = M[2, 2]
1287
+ # symmetric matrix K
1288
+ K = numpy.array(
1289
+ (
1290
+ (m00 - m11 - m22, 0.0, 0.0, 0.0),
1291
+ (m01 + m10, m11 - m00 - m22, 0.0, 0.0),
1292
+ (m02 + m20, m12 + m21, m22 - m00 - m11, 0.0),
1293
+ (m21 - m12, m02 - m20, m10 - m01, m00 + m11 + m22),
1294
+ )
1295
+ )
1296
+ K /= 3.0
1297
+ # quaternion is eigenvector of K that corresponds to largest eigenvalue
1298
+ l, V = numpy.linalg.eigh(K)
1299
+ q = V[[3, 0, 1, 2], numpy.argmax(l)]
1300
+
1301
+ if q[0] < 0.0:
1302
+ q *= -1.0
1303
+ return q
1304
+
1305
+
1306
+ def quaternion_multiply(quaternion1, quaternion0):
1307
+ """Return multiplication of two quaternions.
1308
+
1309
+ >>> q = quaternion_multiply([4, 1, -2, 3], [8, -5, 6, 7])
1310
+ >>> numpy.allclose(q, [28, -44, -14, 48])
1311
+ True
1312
+
1313
+ """
1314
+ w0, x0, y0, z0 = quaternion0
1315
+ w1, x1, y1, z1 = quaternion1
1316
+ return numpy.array(
1317
+ (
1318
+ -x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
1319
+ x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
1320
+ -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
1321
+ x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0,
1322
+ ),
1323
+ dtype=numpy.float64,
1324
+ )
1325
+
1326
+
1327
+ def quaternion_conjugate(quaternion):
1328
+ """Return conjugate of quaternion.
1329
+
1330
+ >>> q0 = random_quaternion()
1331
+ >>> q1 = quaternion_conjugate(q0)
1332
+ >>> q1[0] == q0[0] and all(q1[1:] == -q0[1:])
1333
+ True
1334
+
1335
+ """
1336
+ return numpy.array((quaternion[0], -quaternion[1], -quaternion[2], -quaternion[3]), dtype=numpy.float64)
1337
+
1338
+
1339
+ def quaternion_inverse(quaternion):
1340
+ """Return inverse of quaternion.
1341
+
1342
+ >>> q0 = random_quaternion()
1343
+ >>> q1 = quaternion_inverse(q0)
1344
+ >>> numpy.allclose(quaternion_multiply(q0, q1), [1, 0, 0, 0])
1345
+ True
1346
+
1347
+ """
1348
+ return quaternion_conjugate(quaternion) / numpy.dot(quaternion, quaternion)
1349
+
1350
+
1351
+ def quaternion_real(quaternion):
1352
+ """Return real part of quaternion.
1353
+
1354
+ >>> quaternion_real([3.0, 0.0, 1.0, 2.0])
1355
+ 3.0
1356
+
1357
+ """
1358
+ return quaternion[0]
1359
+
1360
+
1361
+ def quaternion_imag(quaternion):
1362
+ """Return imaginary part of quaternion.
1363
+
1364
+ >>> quaternion_imag([3.0, 0.0, 1.0, 2.0])
1365
+ [0.0, 1.0, 2.0]
1366
+
1367
+ """
1368
+ return quaternion[1:4]
1369
+
1370
+
1371
+ def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True):
1372
+ """Return spherical linear interpolation between two quaternions.
1373
+
1374
+ >>> q0 = random_quaternion()
1375
+ >>> q1 = random_quaternion()
1376
+ >>> q = quaternion_slerp(q0, q1, 0.0)
1377
+ >>> numpy.allclose(q, q0)
1378
+ True
1379
+ >>> q = quaternion_slerp(q0, q1, 1.0, 1)
1380
+ >>> numpy.allclose(q, q1)
1381
+ True
1382
+ >>> q = quaternion_slerp(q0, q1, 0.5)
1383
+ >>> angle = math.acos(numpy.dot(q0, q))
1384
+ >>> numpy.allclose(2.0, math.acos(numpy.dot(q0, q1)) / angle) or \
1385
+ numpy.allclose(2.0, math.acos(-numpy.dot(q0, q1)) / angle)
1386
+ True
1387
+
1388
+ """
1389
+ q0 = unit_vector(quat0[:4])
1390
+ q1 = unit_vector(quat1[:4])
1391
+ if fraction == 0.0:
1392
+ return q0
1393
+ elif fraction == 1.0:
1394
+ return q1
1395
+ d = numpy.dot(q0, q1)
1396
+ if abs(abs(d) - 1.0) < _EPS:
1397
+ return q0
1398
+ if shortestpath and d < 0.0:
1399
+ # invert rotation
1400
+ d = -d
1401
+ q1 *= -1.0
1402
+ angle = math.acos(d) + spin * math.pi
1403
+ if abs(angle) < _EPS:
1404
+ return q0
1405
+ isin = 1.0 / math.sin(angle)
1406
+ q0 *= math.sin((1.0 - fraction) * angle) * isin
1407
+ q1 *= math.sin(fraction * angle) * isin
1408
+ q0 += q1
1409
+ return q0
1410
+
1411
+
1412
+ def random_quaternion(rand=None):
1413
+ """Return uniform random unit quaternion.
1414
+
1415
+ rand: array like or None
1416
+ Three independent random variables that are uniformly distributed
1417
+ between 0 and 1.
1418
+
1419
+ >>> q = random_quaternion()
1420
+ >>> numpy.allclose(1.0, vector_norm(q))
1421
+ True
1422
+ >>> q = random_quaternion(numpy.random.random(3))
1423
+ >>> len(q.shape), q.shape[0]==4
1424
+ (1, True)
1425
+
1426
+ """
1427
+ if rand is None:
1428
+ rand = numpy.random.rand(3)
1429
+ else:
1430
+ assert len(rand) == 3
1431
+ r1 = numpy.sqrt(1.0 - rand[0])
1432
+ r2 = numpy.sqrt(rand[0])
1433
+ pi2 = math.pi * 2.0
1434
+ t1 = pi2 * rand[1]
1435
+ t2 = pi2 * rand[2]
1436
+ return numpy.array(
1437
+ (numpy.cos(t2) * r2, numpy.sin(t1) * r1, numpy.cos(t1) * r1, numpy.sin(t2) * r2), dtype=numpy.float64
1438
+ )
1439
+
1440
+
1441
+ def random_rotation_matrix(rand=None):
1442
+ """Return uniform random rotation matrix.
1443
+
1444
+ rnd: array like
1445
+ Three independent random variables that are uniformly distributed
1446
+ between 0 and 1 for each returned quaternion.
1447
+
1448
+ >>> R = random_rotation_matrix()
1449
+ >>> numpy.allclose(numpy.dot(R.T, R), numpy.identity(4))
1450
+ True
1451
+
1452
+ """
1453
+ return quaternion_matrix(random_quaternion(rand))
1454
+
1455
+
1456
+ class Arcball:
1457
+ """Virtual Trackball Control.
1458
+
1459
+ >>> ball = Arcball()
1460
+ >>> ball = Arcball(initial=numpy.identity(4))
1461
+ >>> ball.place([320, 320], 320)
1462
+ >>> ball.down([500, 250])
1463
+ >>> ball.drag([475, 275])
1464
+ >>> R = ball.matrix()
1465
+ >>> numpy.allclose(numpy.sum(R), 3.90583455)
1466
+ True
1467
+ >>> ball = Arcball(initial=[1, 0, 0, 0])
1468
+ >>> ball.place([320, 320], 320)
1469
+ >>> ball.setaxes([1,1,0], [-1, 1, 0])
1470
+ >>> ball.setconstrain(True)
1471
+ >>> ball.down([400, 200])
1472
+ >>> ball.drag([200, 400])
1473
+ >>> R = ball.matrix()
1474
+ >>> numpy.allclose(numpy.sum(R), 0.2055924)
1475
+ True
1476
+ >>> ball.next()
1477
+
1478
+ """
1479
+
1480
+ def __init__(self, initial=None):
1481
+ """Initialize virtual trackball control.
1482
+
1483
+ initial : quaternion or rotation matrix
1484
+
1485
+ """
1486
+ self._axis = None
1487
+ self._axes = None
1488
+ self._radius = 1.0
1489
+ self._center = [0.0, 0.0]
1490
+ self._vdown = numpy.array([0, 0, 1], dtype=numpy.float64)
1491
+ self._constrain = False
1492
+
1493
+ if initial is None:
1494
+ self._qdown = numpy.array([1, 0, 0, 0], dtype=numpy.float64)
1495
+ else:
1496
+ initial = numpy.array(initial, dtype=numpy.float64)
1497
+ if initial.shape == (4, 4):
1498
+ self._qdown = quaternion_from_matrix(initial)
1499
+ elif initial.shape == (4,):
1500
+ initial /= vector_norm(initial)
1501
+ self._qdown = initial
1502
+ else:
1503
+ raise ValueError("initial not a quaternion or matrix")
1504
+
1505
+ self._qnow = self._qpre = self._qdown
1506
+
1507
+ def place(self, center, radius):
1508
+ """Place Arcball, e.g. when window size changes.
1509
+
1510
+ center : sequence[2]
1511
+ Window coordinates of trackball center.
1512
+ radius : float
1513
+ Radius of trackball in window coordinates.
1514
+
1515
+ """
1516
+ self._radius = float(radius)
1517
+ self._center[0] = center[0]
1518
+ self._center[1] = center[1]
1519
+
1520
+ def setaxes(self, *axes):
1521
+ """Set axes to constrain rotations."""
1522
+ if axes is None:
1523
+ self._axes = None
1524
+ else:
1525
+ self._axes = [unit_vector(axis) for axis in axes]
1526
+
1527
+ def setconstrain(self, constrain):
1528
+ """Set state of constrain to axis mode."""
1529
+ self._constrain = constrain == True
1530
+
1531
+ def getconstrain(self):
1532
+ """Return state of constrain to axis mode."""
1533
+ return self._constrain
1534
+
1535
+ def down(self, point):
1536
+ """Set initial cursor window coordinates and pick constrain-axis."""
1537
+ self._vdown = arcball_map_to_sphere(point, self._center, self._radius)
1538
+ self._qdown = self._qpre = self._qnow
1539
+
1540
+ if self._constrain and self._axes is not None:
1541
+ self._axis = arcball_nearest_axis(self._vdown, self._axes)
1542
+ self._vdown = arcball_constrain_to_axis(self._vdown, self._axis)
1543
+ else:
1544
+ self._axis = None
1545
+
1546
+ def drag(self, point):
1547
+ """Update current cursor window coordinates."""
1548
+ vnow = arcball_map_to_sphere(point, self._center, self._radius)
1549
+
1550
+ if self._axis is not None:
1551
+ vnow = arcball_constrain_to_axis(vnow, self._axis)
1552
+
1553
+ self._qpre = self._qnow
1554
+
1555
+ t = numpy.cross(self._vdown, vnow)
1556
+ if numpy.dot(t, t) < _EPS:
1557
+ self._qnow = self._qdown
1558
+ else:
1559
+ q = [numpy.dot(self._vdown, vnow), t[0], t[1], t[2]]
1560
+ self._qnow = quaternion_multiply(q, self._qdown)
1561
+
1562
+ def next(self, acceleration=0.0):
1563
+ """Continue rotation in direction of last drag."""
1564
+ q = quaternion_slerp(self._qpre, self._qnow, 2.0 + acceleration, False)
1565
+ self._qpre, self._qnow = self._qnow, q
1566
+
1567
+ def matrix(self):
1568
+ """Return homogeneous rotation matrix."""
1569
+ return quaternion_matrix(self._qnow)
1570
+
1571
+
1572
+ def arcball_map_to_sphere(point, center, radius):
1573
+ """Return unit sphere coordinates from window coordinates."""
1574
+ v = numpy.array(((point[0] - center[0]) / radius, (center[1] - point[1]) / radius, 0.0), dtype=numpy.float64)
1575
+ n = v[0] * v[0] + v[1] * v[1]
1576
+ if n > 1.0:
1577
+ v /= math.sqrt(n) # position outside of sphere
1578
+ else:
1579
+ v[2] = math.sqrt(1.0 - n)
1580
+ return v
1581
+
1582
+
1583
+ def arcball_constrain_to_axis(point, axis):
1584
+ """Return sphere point perpendicular to axis."""
1585
+ v = numpy.array(point, dtype=numpy.float64, copy=True)
1586
+ a = numpy.array(axis, dtype=numpy.float64, copy=True)
1587
+ v -= a * numpy.dot(a, v) # on plane
1588
+ n = vector_norm(v)
1589
+ if n > _EPS:
1590
+ if v[2] < 0.0:
1591
+ v *= -1.0
1592
+ v /= n
1593
+ return v
1594
+ if a[2] == 1.0:
1595
+ return numpy.array([1, 0, 0], dtype=numpy.float64)
1596
+ return unit_vector([-a[1], a[0], 0])
1597
+
1598
+
1599
+ def arcball_nearest_axis(point, axes):
1600
+ """Return axis, which arc is nearest to point."""
1601
+ point = numpy.array(point, dtype=numpy.float64, copy=False)
1602
+ nearest = None
1603
+ mx = -1.0
1604
+ for axis in axes:
1605
+ t = numpy.dot(arcball_constrain_to_axis(point, axis), point)
1606
+ if t > mx:
1607
+ nearest = axis
1608
+ mx = t
1609
+ return nearest
1610
+
1611
+
1612
+ # epsilon for testing whether a number is close to zero
1613
+ _EPS = numpy.finfo(float).eps * 4.0
1614
+
1615
+ # axis sequences for Euler angles
1616
+ _NEXT_AXIS = [1, 2, 0, 1]
1617
+
1618
+ # map axes strings to/from tuples of inner axis, parity, repetition, frame
1619
+ _AXES2TUPLE = {
1620
+ "sxyz": (0, 0, 0, 0),
1621
+ "sxyx": (0, 0, 1, 0),
1622
+ "sxzy": (0, 1, 0, 0),
1623
+ "sxzx": (0, 1, 1, 0),
1624
+ "syzx": (1, 0, 0, 0),
1625
+ "syzy": (1, 0, 1, 0),
1626
+ "syxz": (1, 1, 0, 0),
1627
+ "syxy": (1, 1, 1, 0),
1628
+ "szxy": (2, 0, 0, 0),
1629
+ "szxz": (2, 0, 1, 0),
1630
+ "szyx": (2, 1, 0, 0),
1631
+ "szyz": (2, 1, 1, 0),
1632
+ "rzyx": (0, 0, 0, 1),
1633
+ "rxyx": (0, 0, 1, 1),
1634
+ "ryzx": (0, 1, 0, 1),
1635
+ "rxzx": (0, 1, 1, 1),
1636
+ "rxzy": (1, 0, 0, 1),
1637
+ "ryzy": (1, 0, 1, 1),
1638
+ "rzxy": (1, 1, 0, 1),
1639
+ "ryxy": (1, 1, 1, 1),
1640
+ "ryxz": (2, 0, 0, 1),
1641
+ "rzxz": (2, 0, 1, 1),
1642
+ "rxyz": (2, 1, 0, 1),
1643
+ "rzyz": (2, 1, 1, 1),
1644
+ }
1645
+
1646
+ _TUPLE2AXES = {v: k for k, v in _AXES2TUPLE.items()}
1647
+
1648
+
1649
+ def vector_norm(data, axis=None, out=None):
1650
+ """Return length, i.e. eucledian norm, of ndarray along axis.
1651
+
1652
+ >>> v = numpy.random.random(3)
1653
+ >>> n = vector_norm(v)
1654
+ >>> numpy.allclose(n, numpy.linalg.norm(v))
1655
+ True
1656
+ >>> v = numpy.random.rand(6, 5, 3)
1657
+ >>> n = vector_norm(v, axis=-1)
1658
+ >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2)))
1659
+ True
1660
+ >>> n = vector_norm(v, axis=1)
1661
+ >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
1662
+ True
1663
+ >>> v = numpy.random.rand(5, 4, 3)
1664
+ >>> n = numpy.empty((5, 3), dtype=numpy.float64)
1665
+ >>> vector_norm(v, axis=1, out=n)
1666
+ >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
1667
+ True
1668
+ >>> vector_norm([])
1669
+ 0.0
1670
+ >>> vector_norm([1.0])
1671
+ 1.0
1672
+
1673
+ """
1674
+ data = numpy.array(data, dtype=numpy.float64, copy=True)
1675
+ if out is None:
1676
+ if data.ndim == 1:
1677
+ return math.sqrt(numpy.dot(data, data))
1678
+ data *= data
1679
+ out = numpy.atleast_1d(numpy.sum(data, axis=axis))
1680
+ numpy.sqrt(out, out)
1681
+ return out
1682
+ else:
1683
+ data *= data
1684
+ numpy.sum(data, axis=axis, out=out)
1685
+ numpy.sqrt(out, out)
1686
+
1687
+
1688
+ def unit_vector(data, axis=None, out=None):
1689
+ """Return ndarray normalized by length, i.e. eucledian norm, along axis.
1690
+
1691
+ >>> v0 = numpy.random.random(3)
1692
+ >>> v1 = unit_vector(v0)
1693
+ >>> numpy.allclose(v1, v0 / numpy.linalg.norm(v0))
1694
+ True
1695
+ >>> v0 = numpy.random.rand(5, 4, 3)
1696
+ >>> v1 = unit_vector(v0, axis=-1)
1697
+ >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=2)), 2)
1698
+ >>> numpy.allclose(v1, v2)
1699
+ True
1700
+ >>> v1 = unit_vector(v0, axis=1)
1701
+ >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=1)), 1)
1702
+ >>> numpy.allclose(v1, v2)
1703
+ True
1704
+ >>> v1 = numpy.empty((5, 4, 3), dtype=numpy.float64)
1705
+ >>> unit_vector(v0, axis=1, out=v1)
1706
+ >>> numpy.allclose(v1, v2)
1707
+ True
1708
+ >>> list(unit_vector([]))
1709
+ []
1710
+ >>> list(unit_vector([1.0]))
1711
+ [1.0]
1712
+
1713
+ """
1714
+ if out is None:
1715
+ data = numpy.array(data, dtype=numpy.float64, copy=True)
1716
+ if data.ndim == 1:
1717
+ data /= math.sqrt(numpy.dot(data, data))
1718
+ return data
1719
+ else:
1720
+ if out is not data:
1721
+ out[:] = numpy.array(data, copy=False)
1722
+ data = out
1723
+ length = numpy.atleast_1d(numpy.sum(data * data, axis))
1724
+ numpy.sqrt(length, length)
1725
+ if axis is not None:
1726
+ length = numpy.expand_dims(length, axis)
1727
+ data /= length
1728
+ if out is None:
1729
+ return data
1730
+
1731
+
1732
+ def random_vector(size):
1733
+ """Return array of random doubles in the half-open interval [0.0, 1.0).
1734
+
1735
+ >>> v = random_vector(10000)
1736
+ >>> numpy.all(v >= 0.0) and numpy.all(v < 1.0)
1737
+ True
1738
+ >>> v0 = random_vector(10)
1739
+ >>> v1 = random_vector(10)
1740
+ >>> numpy.any(v0 == v1)
1741
+ False
1742
+
1743
+ """
1744
+ return numpy.random.random(size)
1745
+
1746
+
1747
+ def inverse_matrix(matrix):
1748
+ """Return inverse of square transformation matrix.
1749
+
1750
+ >>> M0 = random_rotation_matrix()
1751
+ >>> M1 = inverse_matrix(M0.T)
1752
+ >>> numpy.allclose(M1, numpy.linalg.inv(M0.T))
1753
+ True
1754
+ >>> for size in range(1, 7):
1755
+ ... M0 = numpy.random.rand(size, size)
1756
+ ... M1 = inverse_matrix(M0)
1757
+ ... if not numpy.allclose(M1, numpy.linalg.inv(M0)): print(size)
1758
+
1759
+ """
1760
+ return numpy.linalg.inv(matrix)
1761
+
1762
+
1763
+ def concatenate_matrices(*matrices):
1764
+ """Return concatenation of series of transformation matrices.
1765
+
1766
+ >>> M = numpy.random.rand(16).reshape((4, 4)) - 0.5
1767
+ >>> numpy.allclose(M, concatenate_matrices(M))
1768
+ True
1769
+ >>> numpy.allclose(numpy.dot(M, M.T), concatenate_matrices(M, M.T))
1770
+ True
1771
+
1772
+ """
1773
+ M = numpy.identity(4)
1774
+ for i in matrices:
1775
+ M = numpy.dot(M, i)
1776
+ return M
1777
+
1778
+
1779
+ def is_same_transform(matrix0, matrix1):
1780
+ """Return True if two matrices perform same transformation.
1781
+
1782
+ >>> is_same_transform(numpy.identity(4), numpy.identity(4))
1783
+ True
1784
+ >>> is_same_transform(numpy.identity(4), random_rotation_matrix())
1785
+ False
1786
+
1787
+ """
1788
+ matrix0 = numpy.array(matrix0, dtype=numpy.float64, copy=True)
1789
+ matrix0 /= matrix0[3, 3]
1790
+ matrix1 = numpy.array(matrix1, dtype=numpy.float64, copy=True)
1791
+ matrix1 /= matrix1[3, 3]
1792
+ return numpy.allclose(matrix0, matrix1)
1793
+
1794
+
1795
+ def _import_module(module_name, warn=True, prefix="_py_", ignore="_"):
1796
+ """Try import all public attributes from module into global namespace.
1797
+
1798
+ Existing attributes with name clashes are renamed with prefix.
1799
+ Attributes starting with underscore are ignored by default.
1800
+
1801
+ Return True on successful import.
1802
+
1803
+ """
1804
+ try:
1805
+ module = __import__(module_name)
1806
+ except ImportError:
1807
+ if warn:
1808
+ warnings.warn("failed to import module " + module_name)
1809
+ else:
1810
+ for attr in dir(module):
1811
+ if ignore and attr.startswith(ignore):
1812
+ continue
1813
+ if prefix:
1814
+ if attr in globals():
1815
+ globals()[prefix + attr] = globals()[attr]
1816
+ elif warn:
1817
+ warnings.warn("no Python implementation of " + attr)
1818
+ globals()[attr] = getattr(module, attr)
1819
+ return True
1820
+
1821
+
1822
+ # _import_module('_transformations')
1823
+
1824
+ # Documentation in HTML format can be generated with Epydoc
1825
+ __docformat__ = "restructuredtext en"
1826
+
1827
+
1828
+ if __name__ == "__main__":
1829
+ import doctest
1830
+
1831
+ doctest.testmod()