openpnm 1.0.0__zip

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