Skip to content

Commit c019c0e

Browse files
committed
jacobian for a given point on link body
1 parent 66011e6 commit c019c0e

File tree

2 files changed

+72
-8
lines changed

2 files changed

+72
-8
lines changed

genesis/engine/entities/rigid_entity/rigid_entity.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -798,14 +798,17 @@ def _add_equality(self, name, type, objs_name, data, sol_params):
798798
# ------------------------------------------------------------------------------------
799799

800800
@gs.assert_built
801-
def get_jacobian(self, link):
801+
def get_jacobian(self, link, local_point=None):
802802
"""
803-
Get the Jacobian matrix for a target link.
803+
Get the spatial Jacobian for a point on a target link.
804804
805805
Parameters
806806
----------
807807
link : RigidLink
808808
The target link.
809+
local_point : torch.Tensor or None, shape (3,)
810+
Coordinates of the point in the link’s *local* frame.
811+
If None, the link origin is used (back-compat).
809812
810813
Returns
811814
-------
@@ -820,7 +823,16 @@ def get_jacobian(self, link):
820823
if self.n_dofs == 0:
821824
gs.raise_exception("Entity has zero dofs.")
822825

823-
self._kernel_get_jacobian(link.idx)
826+
if local_point is None:
827+
local_point = torch.zeros(3, device=gs.device, dtype=torch.float32)
828+
elif isinstance(local_point, np.ndarray):
829+
local_point = torch.as_tensor(local_point, device=gs.device, dtype=torch.float32)
830+
831+
if local_point.shape != (3,):
832+
gs.raise_exception("`local_point` must be a (3,) vector in link space.")
833+
834+
p_local_ti = ti.Vector(local_point.tolist(), dt=ti.f32)
835+
self._kernel_get_jacobian(link.idx, p_local_ti)
824836

825837
jacobian = self._jacobian.to_torch(gs.device).permute(2, 0, 1)
826838
if self._solver.n_envs == 0:
@@ -829,22 +841,24 @@ def get_jacobian(self, link):
829841
return jacobian
830842

831843
@ti.kernel
832-
def _kernel_get_jacobian(self, tgt_link_idx: ti.i32):
844+
def _kernel_get_jacobian(self, tgt_link_idx: ti.i32, p_local: ti.types.vector(3, ti.f32)):
833845
ti.loop_config(serialize=self._solver._para_level < gs.PARA_LEVEL.ALL)
834846
for i_b in range(self._solver._B):
835847
self._func_get_jacobian(
836848
tgt_link_idx,
837849
i_b,
850+
p_local,
838851
ti.Vector.one(gs.ti_int, 3),
839852
ti.Vector.one(gs.ti_int, 3),
840853
)
841854

842855
@ti.func
843-
def _func_get_jacobian(self, tgt_link_idx, i_b, pos_mask, rot_mask):
856+
def _func_get_jacobian(self, tgt_link_idx, i_b, p_local, pos_mask, rot_mask):
844857
for i_row, i_d in ti.ndrange(6, self.n_dofs):
845858
self._jacobian[i_row, i_d, i_b] = 0.0
846859

847-
tgt_link_pos = self._solver.links_state[tgt_link_idx, i_b].pos
860+
tgt_link_state = self._solver.links_state[tgt_link_idx, i_b]
861+
tgt_link_pos = tgt_link_state.pos + gu.ti_transform_by_quat(p_local, tgt_link_state.quat)
848862
i_l = tgt_link_idx
849863
while i_l > -1:
850864
I_l = [i_l, i_b] if ti.static(self.solver._options.batch_links_info) else i_l
@@ -864,7 +878,7 @@ def _func_get_jacobian(self, tgt_link_idx, i_b, pos_mask, rot_mask):
864878
I_d = [i_d, i_b] if ti.static(self.solver._options.batch_dofs_info) else i_d
865879
i_d_jac = i_d + dof_offset - self._dof_start
866880
rotation = gu.ti_transform_by_quat(self._solver.dofs_info[I_d].motion_ang, l_state.quat)
867-
translation = rotation.cross(tgt_link_pos - l_state.pos)
881+
translation = (tgt_link_pos - l_state.pos).cross(rotation)
868882

869883
self._jacobian[0, i_d_jac, i_b] = translation[0] * pos_mask[0]
870884
self._jacobian[1, i_d_jac, i_b] = translation[1] * pos_mask[1]
@@ -897,7 +911,7 @@ def _func_get_jacobian(self, tgt_link_idx, i_b, pos_mask, rot_mask):
897911
i_d_jac = i_d + dof_offset - self._dof_start
898912
I_d = [i_d, i_b] if ti.static(self.solver._options.batch_dofs_info) else i_d
899913
rotation = self._solver.dofs_info[I_d].motion_ang
900-
translation = rotation.cross(tgt_link_pos - l_state.pos)
914+
translation = (tgt_link_pos - l_state.pos).cross(rotation)
901915

902916
self._jacobian[0, i_d_jac, i_b] = translation[0] * pos_mask[0]
903917
self._jacobian[1, i_d_jac, i_b] = translation[1] * pos_mask[1]

tests/test_rigid_physics.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,6 +2267,56 @@ def test_scene_saver_franka(show_viewer, tol):
22672267
assert_allclose(pose_ref, pose_loaded, tol=tol)
22682268

22692269

2270+
@pytest.mark.required
2271+
@pytest.mark.parametrize("backend", [gs.cpu])
2272+
def test_jacobian_arbitrary_point(tmp_path, show_viewer, tol):
2273+
urdf_path = tmp_path / "one_link.urdf"
2274+
urdf_path.write_text(
2275+
r"""
2276+
<robot name="one_link">
2277+
<link name="base"/>
2278+
<link name="tip"/>
2279+
<joint name="hinge" type="revolute">
2280+
<parent link="base"/>
2281+
<child link="tip"/>
2282+
<origin xyz="0 0 0" rpy="0 0 0"/>
2283+
<axis xyz="0 0 1"/>
2284+
<limit lower="-3.14" upper="3.14" effort="1" velocity="1"/>
2285+
</joint>
2286+
</robot>
2287+
"""
2288+
)
2289+
2290+
scene = gs.Scene(show_viewer=show_viewer, show_FPS=False)
2291+
ent = scene.add_entity(gs.morphs.URDF(file=str(urdf_path), fixed=True))
2292+
scene.build()
2293+
2294+
angle = 0.7 # rad
2295+
ent.set_qpos(np.array([angle], dtype=np.float32))
2296+
scene.step()
2297+
2298+
link_tip = ent.get_link("tip")
2299+
2300+
p_local = np.array([0.05, -0.02, 0.12], dtype=np.float32)
2301+
J_o = ent.get_jacobian(link_tip).cpu().numpy() # → np.ndarray
2302+
J_p = ent.get_jacobian(link_tip, p_local).cpu().numpy()
2303+
2304+
c, s = np.cos(angle), np.sin(angle)
2305+
Rz = np.array([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32)
2306+
r_world = Rz @ p_local
2307+
2308+
r_cross = np.array(
2309+
[[0, -r_world[2], r_world[1]], [r_world[2], 0, -r_world[0]], [-r_world[1], r_world[0], 0]],
2310+
dtype=np.float32,
2311+
)
2312+
2313+
lin_o, ang_o = J_o[:3, 0], J_o[3:, 0]
2314+
lin_expected = lin_o + r_cross @ ang_o
2315+
2316+
np.testing.assert_allclose(J_p[3:, 0], ang_o, tol=tol)
2317+
np.testing.assert_allclose(J_p[:3, 0], lin_expected, tol=tol)
2318+
2319+
22702320
@pytest.mark.required
22712321
@pytest.mark.parametrize("backend", [gs.cpu])
22722322
def test_drone_hover_same_with_and_without_substeps(show_viewer, tol):

0 commit comments

Comments
 (0)