Skip to content

Commit 9873fac

Browse files
authored
[FEATURE] Add point-specific Jacobian support (Genesis-Embodied-AI#1353)
1 parent 3858da0 commit 9873fac

File tree

2 files changed

+77
-14
lines changed

2 files changed

+77
-14
lines changed

genesis/engine/entities/rigid_entity/rigid_entity.py

Lines changed: 34 additions & 14 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,31 +823,48 @@ 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+
self._kernel_get_jacobian_zero(link.idx)
828+
else:
829+
p_local = torch.as_tensor(local_point, dtype=gs.tc_float, device=gs.device)
830+
if p_local.shape != (3,):
831+
gs.raise_exception("Must be a vector of length 3")
832+
self._kernel_get_jacobian(link.idx, p_local)
824833

825834
jacobian = self._jacobian.to_torch(gs.device).permute(2, 0, 1)
826835
if self._solver.n_envs == 0:
827836
jacobian = jacobian.squeeze(0)
828837

829838
return jacobian
830839

840+
@ti.func
841+
def _impl_get_jacobian(self, tgt_link_idx, i_b, p_vec):
842+
self._func_get_jacobian(
843+
tgt_link_idx,
844+
i_b,
845+
p_vec,
846+
ti.Vector.one(gs.ti_int, 3),
847+
ti.Vector.one(gs.ti_int, 3),
848+
)
849+
831850
@ti.kernel
832-
def _kernel_get_jacobian(self, tgt_link_idx: ti.i32):
833-
ti.loop_config(serialize=self._solver._para_level < gs.PARA_LEVEL.ALL)
851+
def _kernel_get_jacobian(self, tgt_link_idx: ti.i32, p_local: ti.types.ndarray()):
852+
p_vec = ti.Vector([p_local[0], p_local[1], p_local[2]], dt=gs.ti_float)
834853
for i_b in range(self._solver._B):
835-
self._func_get_jacobian(
836-
tgt_link_idx,
837-
i_b,
838-
ti.Vector.one(gs.ti_int, 3),
839-
ti.Vector.one(gs.ti_int, 3),
840-
)
854+
self._impl_get_jacobian(tgt_link_idx, i_b, p_vec)
855+
856+
@ti.kernel
857+
def _kernel_get_jacobian_zero(self, tgt_link_idx: ti.i32):
858+
for i_b in range(self._solver._B):
859+
self._impl_get_jacobian(tgt_link_idx, i_b, ti.Vector.zero(gs.ti_float, 3))
841860

842861
@ti.func
843-
def _func_get_jacobian(self, tgt_link_idx, i_b, pos_mask, rot_mask):
862+
def _func_get_jacobian(self, tgt_link_idx, i_b, p_local, pos_mask, rot_mask):
844863
for i_row, i_d in ti.ndrange(6, self.n_dofs):
845864
self._jacobian[i_row, i_d, i_b] = 0.0
846865

847-
tgt_link_pos = self._solver.links_state[tgt_link_idx, i_b].pos
866+
tgt_link_state = self._solver.links_state[tgt_link_idx, i_b]
867+
tgt_link_pos = tgt_link_state.pos + gu.ti_transform_by_quat(p_local, tgt_link_state.quat)
848868
i_l = tgt_link_idx
849869
while i_l > -1:
850870
I_l = [i_l, i_b] if ti.static(self.solver._options.batch_links_info) else i_l
@@ -1320,7 +1340,7 @@ def _kernel_inverse_kinematics(
13201340
# update jacobian for ee link
13211341
i_l_ee = links_idx[i_ee]
13221342
self._func_get_jacobian(
1323-
i_l_ee, i_b, pos_mask, rot_mask
1343+
i_l_ee, i_b, ti.Vector.zero(gs.ti_float, 3), pos_mask, rot_mask
13241344
) # NOTE: we still compute jacobian for all dofs as we haven't found a clean way to implement this
13251345

13261346
# copy to multi-link jacobian (only for the effective n_dofs instead of self.n_dofs)

tests/test_rigid_physics.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,49 @@ def test_terrain_size(show_viewer, tol):
21692169
assert_allclose((height_ref * 2.0), height_test, tol=tol)
21702170

21712171

2172+
@pytest.mark.required
2173+
@pytest.mark.merge_fixed_links(False)
2174+
@pytest.mark.parametrize("model_name", ["pendulum"])
2175+
@pytest.mark.parametrize("gs_solver", [gs.constraint_solver.CG])
2176+
@pytest.mark.parametrize("gs_integrator", [gs.integrator.Euler])
2177+
def test_jacobian(gs_sim, tol):
2178+
pendulum = gs_sim.entities[0]
2179+
angle = 0.7
2180+
pendulum.set_qpos(np.array([angle], dtype=gs.np_float))
2181+
gs_sim.scene.step()
2182+
2183+
link = pendulum.get_link("PendulumArm_0")
2184+
2185+
p_local = np.array([0.05, -0.02, 0.12], dtype=gs.np_float)
2186+
J_o = tensor_to_array(pendulum.get_jacobian(link))
2187+
J_p = tensor_to_array(pendulum.get_jacobian(link, p_local))
2188+
2189+
c, s = np.cos(angle), np.sin(angle)
2190+
Rx = np.array(
2191+
[
2192+
[1, 0, 0],
2193+
[0, c, -s],
2194+
[0, s, c],
2195+
],
2196+
dtype=gs.np_float,
2197+
)
2198+
r_world = Rx @ p_local
2199+
r_cross = np.array(
2200+
[
2201+
[0, -r_world[2], r_world[1]],
2202+
[r_world[2], 0, -r_world[0]],
2203+
[-r_world[1], r_world[0], 0],
2204+
],
2205+
dtype=gs.np_float,
2206+
)
2207+
2208+
lin_o, ang_o = J_o[:3, 0], J_o[3:, 0]
2209+
lin_expected = lin_o - r_cross @ ang_o
2210+
2211+
assert_allclose(J_p[3:, 0], ang_o, tol=tol)
2212+
assert_allclose(J_p[:3, 0], lin_expected, tol=tol)
2213+
2214+
21722215
@pytest.mark.required
21732216
@pytest.mark.parametrize("backend", [gs.cpu])
21742217
def test_urdf_parsing(show_viewer, tol):

0 commit comments

Comments
 (0)