@@ -1575,6 +1575,150 @@ def test_multilink_inverse_kinematics(show_viewer):
15751575 assert_allclose (wrist .get_pos (envs_idx = (1 ,)), wrist_pos , tol = TOL )
15761576
15771577
1578+ @pytest .mark .required
1579+ @pytest .mark .parametrize ("n_envs" , [0 , 2 ])
1580+ @pytest .mark .parametrize ("backend" , [gs .cpu , gs .gpu ])
1581+ def test_inverse_kinematics_local_point (n_envs , show_viewer ):
1582+ """Test IK with local_point parameter - positions an offset point at the target instead of link origin."""
1583+
1584+ TOL = 2e-3 # 2mm tolerance for final position check
1585+
1586+ scene = gs .Scene (
1587+ viewer_options = gs .options .ViewerOptions (
1588+ camera_pos = (2.5 , 0.0 , 1.5 ),
1589+ camera_lookat = (0.0 , 0.0 , 0.5 ),
1590+ ),
1591+ show_viewer = show_viewer ,
1592+ )
1593+ robot = scene .add_entity (
1594+ morph = gs .morphs .MJCF (file = "xml/franka_emika_panda/panda.xml" ),
1595+ )
1596+ scene .build (n_envs = n_envs )
1597+
1598+ end_effector = robot .get_link ("hand" )
1599+
1600+ # Define a local offset point in the end-effector frame (e.g., 10cm along Z-axis)
1601+ local_offset = torch .tensor ([0.0 , 0.0 , 0.1 ], dtype = gs .tc_float , device = gs .device )
1602+
1603+ # Create different target positions and quaternions for each environment
1604+ num_envs = max (n_envs , 1 )
1605+ target_pos_base = torch .tensor (
1606+ [[0.5 , 0.2 , 0.4 ], [0.45 , 0.15 , 0.35 ], [0.55 , 0.25 , 0.45 ]], dtype = gs .tc_float , device = gs .device
1607+ )[:num_envs ]
1608+ target_quat_base = torch .tensor (
1609+ [[0.0 , 1.0 , 0.0 , 0.0 ], [0.0 , 0.9239 , 0.3827 , 0.0 ], [0.0 , 0.9239 , - 0.3827 , 0.0 ]],
1610+ dtype = gs .tc_float ,
1611+ device = gs .device ,
1612+ )[:num_envs ]
1613+
1614+ # Handle different shapes based on n_envs
1615+ if n_envs > 0 :
1616+ target_pos = target_pos_base
1617+ target_quat = target_quat_base
1618+ else :
1619+ target_pos = target_pos_base [0 ]
1620+ target_quat = target_quat_base [0 ]
1621+
1622+ # Solve IK with local_point (local_offset stays 1D - it gets broadcast internally)
1623+ qpos , err = robot .inverse_kinematics (
1624+ link = end_effector ,
1625+ pos = target_pos ,
1626+ quat = target_quat ,
1627+ local_point = local_offset ,
1628+ return_error = True ,
1629+ )
1630+
1631+ # Apply the solution
1632+ robot .set_qpos (qpos )
1633+ scene .step ()
1634+
1635+ # Verify the offset point is at the target position
1636+ link_pos = end_effector .get_pos ()
1637+ link_quat = end_effector .get_quat ()
1638+
1639+ # Transform local offset to world frame
1640+ world_offset = gu .transform_by_quat (local_offset , link_quat )
1641+ actual_point_pos = link_pos + world_offset
1642+
1643+ # Check that the offset point reached the target
1644+ assert_allclose (actual_point_pos , target_pos , tol = TOL )
1645+
1646+ # Also verify via forward kinematics
1647+ links_pos , links_quat = robot .forward_kinematics (qpos )
1648+
1649+ # Handle indexing based on n_envs
1650+ if n_envs > 0 :
1651+ fk_link_pos = links_pos [:, end_effector .idx_local ]
1652+ fk_link_quat = links_quat [:, end_effector .idx_local ]
1653+ else :
1654+ fk_link_pos = links_pos [end_effector .idx_local ]
1655+ fk_link_quat = links_quat [end_effector .idx_local ]
1656+
1657+ fk_world_offset = gu .transform_by_quat (local_offset , fk_link_quat )
1658+ fk_actual_point_pos = fk_link_pos + fk_world_offset
1659+ assert_allclose (fk_actual_point_pos , target_pos , tol = TOL )
1660+
1661+ if show_viewer :
1662+ scene .visualizer .update ()
1663+
1664+
1665+ @pytest .mark .required
1666+ @pytest .mark .parametrize ("backend" , [gs .cpu , gs .gpu ])
1667+ def test_inverse_kinematics_multilink_local_points (show_viewer ):
1668+ """Test multi-link IK with local_points parameter."""
1669+
1670+ TOL = 2e-3 # 2mm tolerance for final position check
1671+
1672+ scene = gs .Scene (
1673+ viewer_options = gs .options .ViewerOptions (
1674+ camera_pos = (2.5 , 0.0 , 1.5 ),
1675+ camera_lookat = (0.0 , 0.0 , 0.5 ),
1676+ ),
1677+ show_viewer = show_viewer ,
1678+ )
1679+ robot = scene .add_entity (
1680+ morph = gs .morphs .URDF (file = "urdf/shadow_hand/shadow_hand.urdf" ),
1681+ )
1682+ scene .build ()
1683+
1684+ index_finger = robot .get_link ("index_finger_distal" )
1685+ middle_finger = robot .get_link ("middle_finger_distal" )
1686+
1687+ # Different local offsets for each finger (e.g., fingertip positions)
1688+ index_local_offset = torch .tensor ([0.0 , 0.0 , 0.02 ], dtype = gs .tc_float , device = gs .device )
1689+ middle_local_offset = torch .tensor ([0.0 , 0.0 , 0.02 ], dtype = gs .tc_float , device = gs .device )
1690+
1691+ # Target positions for the fingertips
1692+ index_target = torch .tensor ([0.6 , 0.5 , 0.2 ], dtype = gs .tc_float , device = gs .device )
1693+ middle_target = torch .tensor ([0.63 , 0.5 , 0.2 ], dtype = gs .tc_float , device = gs .device )
1694+
1695+ # Solve multi-link IK with local_points
1696+ qpos , err = robot .inverse_kinematics_multilink (
1697+ links = [index_finger , middle_finger ],
1698+ poss = [index_target , middle_target ],
1699+ local_points = [index_local_offset , middle_local_offset ],
1700+ return_error = True ,
1701+ )
1702+
1703+ # Apply solution
1704+ robot .set_qpos (qpos )
1705+ scene .step ()
1706+
1707+ # Verify each offset point is at its target
1708+ for link , local_offset , target in [
1709+ (index_finger , index_local_offset , index_target ),
1710+ (middle_finger , middle_local_offset , middle_target ),
1711+ ]:
1712+ link_pos = link .get_pos ()
1713+ link_quat = link .get_quat ()
1714+ world_offset = gu .transform_by_quat (local_offset , link_quat )
1715+ actual_point_pos = link_pos + world_offset
1716+ assert_allclose (actual_point_pos , target , tol = TOL )
1717+
1718+ if show_viewer :
1719+ scene .visualizer .update ()
1720+
1721+
15781722@pytest .mark .slow # ~180s
15791723@pytest .mark .required
15801724@pytest .mark .parametrize ("n_envs" , [0 , 2 ])
0 commit comments