@@ -163,6 +163,25 @@ def _build_descendant_free_distance(device, joint_type) -> tuple[newton.Model, i
163163 return builder .finalize (device = device , requires_grad = True ), child
164164
165165
166+ def _build_root_free_distance (device , joint_type ) -> tuple [newton .Model , int ]:
167+ builder = newton .ModelBuilder ()
168+ body = builder .add_link (mass = 1.0 , inertia = wp .mat33 (1.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 , 0.0 , 0.0 , 1.0 ))
169+ builder .body_com [body ] = wp .vec3 (0.2 , - 0.1 , 0.3 )
170+ joint = _add_free_distance_joint (
171+ builder = builder ,
172+ joint_type = joint_type ,
173+ parent = - 1 ,
174+ child = body ,
175+ parent_xform = wp .transform (
176+ wp .vec3 (0.3 , - 0.4 , 0.6 ),
177+ wp .quat_from_axis_angle (wp .normalize (wp .vec3 (0.2 , 1.0 , - 0.3 )), 0.7 ),
178+ ),
179+ child_xform = wp .transform_identity (),
180+ )
181+ builder .add_articulation ([joint ])
182+ return builder .finalize (device = device , requires_grad = True ), body
183+
184+
166185# ----------------------------------------------------------------------------
167186# helpers - D6
168187# ----------------------------------------------------------------------------
@@ -711,6 +730,54 @@ def test_d6_jacobian_compare(test, device):
711730 _jacobian_compare (test , device , _d6_objective_builder )
712731
713732
733+ def test_free_distance_translated_jacobian_compare (test , device , joint_type , optimizer ):
734+ """Match analytic and autodiff Jacobians for a translated floating body."""
735+ with wp .ScopedDevice (device ):
736+ translations = (0.0 , 1.0 , 5.0 , 20.0 )
737+ model , body = _build_root_free_distance (device , joint_type )
738+ joint_q = np .zeros ((len (translations ), model .joint_coord_count ), dtype = np .float32 )
739+ joint_q [:, 0 ] = translations
740+ joint_q [:, 6 ] = 1.0
741+ target_positions = wp .zeros (len (translations ), dtype = wp .vec3 , device = device )
742+
743+ solver_auto = ik .IKSolver (
744+ model ,
745+ len (translations ),
746+ [ik .IKObjectivePosition (body , wp .vec3 (), target_positions )],
747+ optimizer = optimizer ,
748+ jacobian_mode = ik .IKJacobianType .AUTODIFF ,
749+ )
750+ solver_ana = ik .IKSolver (
751+ model ,
752+ len (translations ),
753+ [ik .IKObjectivePosition (body , wp .vec3 (), target_positions )],
754+ optimizer = optimizer ,
755+ jacobian_mode = ik .IKJacobianType .ANALYTIC ,
756+ )
757+ q_auto = wp .array (joint_q , device = device , requires_grad = True )
758+ q_ana = wp .array (joint_q , device = device )
759+
760+ solver_auto ._impl ._compute_residuals (q_auto )
761+ solver_ana ._impl ._compute_residuals (q_ana )
762+ if optimizer == ik .IKOptimizer .LM :
763+ jacobian_auto = solver_auto ._impl ._jacobian_at (solver_auto ._impl ._ctx_solver (q_auto )).numpy ()
764+ jacobian_ana = solver_ana ._impl ._jacobian_at (solver_ana ._impl ._ctx_solver (q_ana )).numpy ()
765+ for problem_idx , translation in enumerate (translations ):
766+ with test .subTest (translation = translation ):
767+ assert_np_equal (jacobian_auto [problem_idx ], jacobian_ana [problem_idx ], tol = 1e-4 )
768+ else :
769+ gradient_auto = wp .zeros ((len (translations ), model .joint_dof_count ), dtype = wp .float32 , device = device )
770+ gradient_ana = wp .zeros_like (gradient_auto )
771+ solver_auto ._impl ._gradient_at (solver_auto ._impl ._ctx_solver (q_auto ), gradient_auto )
772+ solver_ana ._impl ._gradient_at (solver_ana ._impl ._ctx_solver (q_ana ), gradient_ana )
773+ assert_np_equal (gradient_auto .numpy (), gradient_ana .numpy (), tol = 1e-4 )
774+
775+ motion_subspace = solver_ana ._impl .joint_S_s .numpy ()
776+ for problem_idx , translation in enumerate (translations [1 :], start = 1 ):
777+ with test .subTest (motion_subspace_translation = translation ):
778+ assert_np_equal (motion_subspace [0 ], motion_subspace [problem_idx ], tol = 1e-6 )
779+
780+
714781# ----------------------------------------------------------------------------
715782# 3. Test-class registration per device
716783# ----------------------------------------------------------------------------
@@ -761,6 +828,16 @@ class TestIKModes(unittest.TestCase):
761828add_function_test (TestIKModes , "test_rotation_jacobian_compare" , test_rotation_jacobian_compare , cuda_devices )
762829add_function_test (TestIKModes , "test_joint_limit_jacobian_compare" , test_joint_limit_jacobian_compare , devices )
763830add_function_test (TestIKModes , "test_d6_jacobian_compare" , test_d6_jacobian_compare , cuda_devices )
831+ for optimizer , optimizer_name in ((ik .IKOptimizer .LM , "lm" ), (ik .IKOptimizer .LBFGS , "lbfgs" )):
832+ for joint_type in (newton .JointType .FREE , newton .JointType .DISTANCE ):
833+ add_function_test (
834+ TestIKModes ,
835+ f"test_translated_{ _joint_type_name (joint_type )} _{ optimizer_name } _jacobian_compare" ,
836+ test_free_distance_translated_jacobian_compare ,
837+ devices ,
838+ joint_type = joint_type ,
839+ optimizer = optimizer ,
840+ )
764841
765842
766843if __name__ == "__main__" :
0 commit comments