1616from .. import eulerangles as nea
1717from .. import quaternions as nq
1818
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+
1931# 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+
2939# 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 ]
3341# M, quaternion pairs
3442eg_pairs = list (zip (eg_rots , eg_quats ))
3543
3644# 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+ )
4853
4954
5055def test_fillpos ():
@@ -69,6 +74,51 @@ def test_fillpos():
6974 assert wxyz [0 ] == 0.0
7075
7176
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+
72122def test_conjugate ():
73123 # Takes sequence
74124 cq = nq .conjugate ((1 , 0 , 0 , 0 ))
@@ -125,7 +175,7 @@ def test_norm():
125175def test_mult (M1 , q1 , M2 , q2 ):
126176 # Test that quaternion * same as matrix *
127177 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 )
129179
130180
131181@pytest .mark .parametrize ('M, q' , eg_pairs )
@@ -146,7 +196,7 @@ def test_eye():
146196@pytest .mark .parametrize ('M, q' , eg_pairs )
147197def test_qrotate (vec , M , q ):
148198 vdash = nq .rotate_vector (vec , q )
149- vM = np . dot ( M , vec )
199+ vM = M @ vec
150200 assert_array_almost_equal (vdash , vM )
151201
152202
@@ -179,6 +229,6 @@ def test_angle_axis():
179229 nq .nearly_equivalent (q , q2 )
180230 aa_mat = nq .angle_axis2mat (theta , vec )
181231 assert_array_almost_equal (aa_mat , M )
182- unit_vec = vec / np . sqrt (vec . dot ( vec ) )
232+ unit_vec = norm (vec )
183233 aa_mat2 = nq .angle_axis2mat (theta , unit_vec , is_normalized = True )
184234 assert_array_almost_equal (aa_mat2 , M )
0 commit comments