16
16
from .. import eulerangles as nea
17
17
from .. import quaternions as nq
18
18
19
+
20
+ def norm (vec ):
21
+ # Return unit vector with same orientation as input vector
22
+ return vec / np .sqrt (vec @ vec )
23
+
24
+
25
+ def gen_vec (dtype ):
26
+ # Generate random 3-vector in [-1, 1]^3
27
+ rand = np .random .default_rng ()
28
+ return rand .uniform (low = - 1.0 , high = 1.0 , size = (3 ,)).astype (dtype )
29
+
30
+
19
31
# Example rotations
20
- eg_rots = []
21
- params = (- pi , pi , pi / 2 )
22
- zs = np .arange (* params )
23
- ys = np .arange (* params )
24
- xs = np .arange (* params )
25
- for z in zs :
26
- for y in ys :
27
- for x in xs :
28
- eg_rots .append (nea .euler2mat (z , y , x ))
32
+ eg_rots = [
33
+ nea .euler2mat (z , y , x )
34
+ for z in np .arange (- pi , pi , pi / 2 )
35
+ for y in np .arange (- pi , pi , pi / 2 )
36
+ for x in np .arange (- pi , pi , pi / 2 )
37
+ ]
38
+
29
39
# Example quaternions (from rotations)
30
- eg_quats = []
31
- for M in eg_rots :
32
- eg_quats .append (nq .mat2quat (M ))
40
+ eg_quats = [nq .mat2quat (M ) for M in eg_rots ]
33
41
# M, quaternion pairs
34
42
eg_pairs = list (zip (eg_rots , eg_quats ))
35
43
36
44
# Set of arbitrary unit quaternions
37
- unit_quats = set ()
38
- params = range (- 2 , 3 )
39
- for w in params :
40
- for x in params :
41
- for y in params :
42
- for z in params :
43
- q = (w , x , y , z )
44
- Nq = np .sqrt (np .dot (q , q ))
45
- if not Nq == 0 :
46
- q = tuple ([e / Nq for e in q ])
47
- unit_quats .add (q )
45
+ unit_quats = set (
46
+ tuple (norm (np .r_ [w , x , y , z ]))
47
+ for w in range (- 2 , 3 )
48
+ for x in range (- 2 , 3 )
49
+ for y in range (- 2 , 3 )
50
+ for z in range (- 2 , 3 )
51
+ if (w , x , y , z ) != (0 , 0 , 0 , 0 )
52
+ )
48
53
49
54
50
55
def test_fillpos ():
@@ -69,6 +74,51 @@ def test_fillpos():
69
74
assert wxyz [0 ] == 0.0
70
75
71
76
77
+ @pytest .mark .parametrize ('dtype' , ('f4' , 'f8' ))
78
+ def test_fillpositive_plus_minus_epsilon (dtype ):
79
+ # Deterministic test for fillpositive threshold
80
+ # We are trying to fill (x, y, z) with a w such that |(w, x, y, z)| == 1
81
+ # If |(x, y, z)| is slightly off one, w should still be 0
82
+ nptype = np .dtype (dtype ).type
83
+
84
+ # Obviously, |(x, y, z)| == 1
85
+ baseline = np .array ([0 , 0 , 1 ], dtype = dtype )
86
+
87
+ # Obviously, |(x, y, z)| ~ 1
88
+ plus = baseline * nptype (1 + np .finfo (dtype ).eps )
89
+ minus = baseline * nptype (1 - np .finfo (dtype ).eps )
90
+
91
+ assert nq .fillpositive (plus )[0 ] == 0.0
92
+ assert nq .fillpositive (minus )[0 ] == 0.0
93
+
94
+ # |(x, y, z)| > 1, no real solutions
95
+ plus = baseline * nptype (1 + 2 * np .finfo (dtype ).eps )
96
+ with pytest .raises (ValueError ):
97
+ nq .fillpositive (plus )
98
+
99
+ # |(x, y, z)| < 1, two real solutions, we choose positive
100
+ minus = baseline * nptype (1 - 2 * np .finfo (dtype ).eps )
101
+ assert nq .fillpositive (minus )[0 ] > 0.0
102
+
103
+
104
+ @pytest .mark .parametrize ('dtype' , ('f4' , 'f8' ))
105
+ def test_fillpositive_simulated_error (dtype ):
106
+ # Nondeterministic test for fillpositive threshold
107
+ # Create random vectors, normalize to unit length, and count on floating point
108
+ # error to result in magnitudes larger/smaller than one
109
+ # This is to simulate cases where a unit quaternion with w == 0 would be encoded
110
+ # as xyz with small error, and we want to recover the w of 0
111
+
112
+ # Permit 1 epsilon per value (default, but make explicit here)
113
+ w2_thresh = 3 * np .finfo (dtype ).eps
114
+
115
+ pos_error = neg_error = False
116
+ for _ in range (50 ):
117
+ xyz = norm (gen_vec (dtype ))
118
+
119
+ assert nq .fillpositive (xyz , w2_thresh )[0 ] == 0.0
120
+
121
+
72
122
def test_conjugate ():
73
123
# Takes sequence
74
124
cq = nq .conjugate ((1 , 0 , 0 , 0 ))
@@ -125,7 +175,7 @@ def test_norm():
125
175
def test_mult (M1 , q1 , M2 , q2 ):
126
176
# Test that quaternion * same as matrix *
127
177
q21 = nq .mult (q2 , q1 )
128
- assert_array_almost_equal , np . dot ( M2 , M1 ) , nq .quat2mat (q21 )
178
+ assert_array_almost_equal , M2 @ M1 , nq .quat2mat (q21 )
129
179
130
180
131
181
@pytest .mark .parametrize ('M, q' , eg_pairs )
@@ -146,7 +196,7 @@ def test_eye():
146
196
@pytest .mark .parametrize ('M, q' , eg_pairs )
147
197
def test_qrotate (vec , M , q ):
148
198
vdash = nq .rotate_vector (vec , q )
149
- vM = np . dot ( M , vec )
199
+ vM = M @ vec
150
200
assert_array_almost_equal (vdash , vM )
151
201
152
202
@@ -179,6 +229,6 @@ def test_angle_axis():
179
229
nq .nearly_equivalent (q , q2 )
180
230
aa_mat = nq .angle_axis2mat (theta , vec )
181
231
assert_array_almost_equal (aa_mat , M )
182
- unit_vec = vec / np . sqrt (vec . dot ( vec ) )
232
+ unit_vec = norm (vec )
183
233
aa_mat2 = nq .angle_axis2mat (theta , unit_vec , is_normalized = True )
184
234
assert_array_almost_equal (aa_mat2 , M )
0 commit comments