88
99from mink .exceptions import InvalidMocapBody
1010from mink .lie .base import MatrixLieGroup
11- from mink .lie .se3 import SE3
11+ from mink .lie .se3 import SE3 , _getQ
1212from mink .lie .so3 import SO3
1313
1414from .utils import assert_transforms_close
@@ -142,6 +142,12 @@ def test_so3_apply(self):
142142 rotated_vec = rot .apply (vec )
143143 np .testing .assert_allclose (rotated_vec , rot .as_matrix () @ vec )
144144
145+ def test_so3_matmul_with_vector_calls_apply (self ):
146+ """Using @ with an ndarray should delegate to .apply(...)."""
147+ vec = np .random .rand (3 )
148+ rot = SO3 .sample_uniform ()
149+ np .testing .assert_allclose (rot @ vec , rot .apply (vec ))
150+
145151 def test_so3_apply_throws_assertion_error_if_wrong_shape (self ):
146152 rot = SO3 .sample_uniform ()
147153 vec = np .random .rand (2 )
@@ -178,6 +184,10 @@ def test_so3_clamp(self):
178184
179185 # SE3.
180186
187+ def test_se3_raises_error_if_invalid_shape (self ):
188+ with self .assertRaises (ValueError ):
189+ SE3 (wxyz_xyz = np .random .rand (2 ))
190+
181191 def test_se3_equality (self ):
182192 pose_1 = SE3 .identity ()
183193 pose_2 = SE3 .identity ()
@@ -201,6 +211,12 @@ def test_se3_apply(self):
201211 T .apply (v ), T .as_matrix ()[:3 , :3 ] @ v + T .translation ()
202212 )
203213
214+ def test_se3_matmul_with_vector_calls_apply (self ):
215+ """Using @ with an ndarray should delegate to .apply(...)."""
216+ vec = np .random .rand (3 )
217+ T = SE3 .sample_uniform ()
218+ np .testing .assert_allclose (T @ vec , T .apply (vec ))
219+
204220 def test_se3_from_mocap_id (self ):
205221 xml_str = """
206222 <mujoco>
@@ -335,5 +351,57 @@ def test_se3_clamp(self):
335351 self .assertAlmostEqual (clamped_rpy .yaw , np .pi )
336352
337353
354+ class TestHashAndSetMembership (absltest .TestCase ):
355+ """Test that SO3 and SE3 objects can be hashed and used in sets."""
356+
357+ def test_so3_hash_and_set_membership (self ):
358+ a = SO3 .from_rpy_radians (0.1 , - 0.2 , 0.3 )
359+ b = SO3 (wxyz = a .wxyz .copy ())
360+ c = SO3 .from_rpy_radians (0.1 , - 0.2 , 0.31 )
361+ assert a == b
362+ assert hash (a ) == hash (b )
363+ s = {a , b , c }
364+ assert a in s and b in s and c in s
365+ assert len (s ) == 2
366+
367+ def test_se3_hash_and_set_membership (self ):
368+ R = SO3 .from_rpy_radians (0.05 , 0.02 , - 0.01 )
369+ t = np .array ([0.3 , - 0.1 , 0.2 ], dtype = np .float64 )
370+ a = SE3 .from_rotation_and_translation (R , t )
371+ b = SE3 .from_rotation_and_translation (SO3 (wxyz = R .wxyz .copy ()), t .copy ())
372+ c = SE3 .from_rotation_and_translation (R , t + np .array ([1e-3 , 0.0 , 0.0 ]))
373+ assert a == b
374+ assert hash (a ) == hash (b )
375+ s = {a , b , c }
376+ assert len (s ) == 2
377+
378+
379+ class TestSE3_getQ (absltest .TestCase ):
380+ """Covers both small-angle and general branches in mink.lie.se3._getQ."""
381+
382+ def test__getQ_small_angle_zero (self ):
383+ # theta == 0 (small-angle branch); with v=0 too, Q should be exactly zero.
384+ c_small = np .zeros (6 , dtype = np .float64 )
385+ Q = _getQ (c_small )
386+ np .testing .assert_allclose (Q , np .zeros ((3 , 3 ), dtype = np .float64 ))
387+
388+ def test__getQ_general_branch_nontrivial (self ):
389+ # Non-zero rotation (general branch).
390+ # Use non-zero v to avoid the trivial zero result from the small-angle test.
391+ c = np .zeros (6 , dtype = np .float64 )
392+ c [:3 ] = np .array ([0.3 , - 0.2 , 0.1 ], dtype = np .float64 ) # v
393+ c [3 :] = np .array ([0.4 , 0.0 , 0.0 ], dtype = np .float64 ) # omega (theta ≈ 0.4 > 0)
394+
395+ Q = _getQ (c )
396+ self .assertEqual (Q .shape , (3 , 3 ))
397+ self .assertTrue (np .isfinite (Q ).all ())
398+
399+ # Sanity: changing v (with same omega) should change Q.
400+ c_scaled = c .copy ()
401+ c_scaled [:3 ] *= 2.0
402+ Q_scaled = _getQ (c_scaled )
403+ self .assertGreater (np .linalg .norm (Q - Q_scaled ), 1e-9 )
404+
405+
338406if __name__ == "__main__" :
339407 absltest .main ()
0 commit comments