Skip to content

Commit 9eb1fe1

Browse files
committed
support query of vel and pos for a point on a link
1 parent 91cac2a commit 9eb1fe1

File tree

3 files changed

+237
-3
lines changed

3 files changed

+237
-3
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import argparse
2+
3+
import numpy as np
4+
import torch
5+
6+
import genesis as gs
7+
from genesis.utils import geom as gu
8+
9+
10+
def main():
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument("-v", "--vis", action="store_true", default=False)
13+
args = parser.parse_args()
14+
15+
gs.init(backend=gs.cpu)
16+
17+
viewer_options = gs.options.ViewerOptions(
18+
camera_pos=(0, -3.5, 2.5),
19+
camera_lookat=(0.0, 0.0, 0.5),
20+
camera_fov=40,
21+
max_FPS=60,
22+
)
23+
24+
scene = gs.Scene(
25+
viewer_options=viewer_options,
26+
show_viewer=args.vis,
27+
)
28+
29+
scene.add_entity(morph=gs.morphs.Plane())
30+
31+
robot = scene.add_entity(
32+
morph=gs.morphs.MJCF(file="xml/franka_emika_panda/panda.xml"),
33+
material=gs.materials.Rigid(),
34+
)
35+
36+
scene.build()
37+
38+
link4 = robot.get_link("link4")
39+
right_finger = robot.get_link("right_finger")
40+
hand_link = robot.get_link("hand")
41+
42+
local_point_offsets = torch.tensor(
43+
[[0.05, 0.0, 0.1], [0.02, 0.0, 0.01], [0.08, 0.0, 0.05], [0.0, 0.03, 0.08]], dtype=gs.tc_float, device=gs.device
44+
)
45+
46+
point_links_idx_local = torch.tensor(
47+
[link4.idx_local, right_finger.idx_local, hand_link.idx_local, hand_link.idx_local],
48+
dtype=gs.tc_int,
49+
device=gs.device,
50+
)
51+
52+
all_points_info = [
53+
{"link_name": "link4", "point_idx": 0},
54+
{"link_name": "right_finger", "point_idx": 1},
55+
{"link_name": "hand", "point_idx": 2},
56+
{"link_name": "hand", "point_idx": 3},
57+
]
58+
59+
center = torch.tensor([0.5, 0.0, 0.4])
60+
radius = 0.15
61+
frequency = 0.3
62+
63+
point_colors = [
64+
(0, 0, 1, 0.8),
65+
(1, 0, 0, 0.6),
66+
(0, 1, 0, 0.6),
67+
(1, 0, 1, 0.6),
68+
]
69+
70+
n_steps = 1000
71+
for step in range(n_steps):
72+
t = step * scene.dt
73+
74+
target_pos = center + torch.tensor(
75+
[radius * np.cos(2 * np.pi * frequency * t), radius * np.sin(2 * np.pi * frequency * t), 0.0]
76+
)
77+
78+
qpos_target = robot.inverse_kinematics(
79+
link=hand_link,
80+
pos=target_pos,
81+
max_solver_iters=20,
82+
damping=0.1,
83+
pos_tol=1e-3,
84+
)
85+
robot.control_dofs_position(qpos_target)
86+
87+
scene.step()
88+
89+
world_positions, world_velocities = robot.get_link_point_position_velocity(
90+
local_offsets=local_point_offsets, links_idx_local=point_links_idx_local
91+
)
92+
93+
scene.clear_debug_objects()
94+
95+
for i, point_info in enumerate(all_points_info):
96+
point_idx = point_info["point_idx"]
97+
98+
point_pos = world_positions[point_idx].numpy()
99+
100+
scene.draw_debug_sphere(pos=point_pos, radius=0.012, color=point_colors[i % len(point_colors)])
101+
102+
for i, point_info in enumerate(all_points_info):
103+
point_idx = point_info["point_idx"]
104+
105+
point_vel = world_velocities[point_idx]
106+
point_pos = world_positions[point_idx].numpy()
107+
108+
scene.draw_debug_arrow(
109+
pos=point_pos, vec=point_vel.numpy() * 0.3, radius=0.005, color=point_colors[i % len(point_colors)]
110+
)
111+
112+
113+
if __name__ == "__main__":
114+
main()

genesis/engine/entities/rigid_entity/rigid_entity.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ def get_jacobian(self, link, local_point=None):
821821
link : RigidLink
822822
The target link.
823823
local_point : torch.Tensor or None, shape (3,)
824-
Coordinates of the point in the links *local* frame.
824+
Coordinates of the point in the link's *local* frame.
825825
If None, the link origin is used (back-compat).
826826
827827
Returns
@@ -2882,3 +2882,57 @@ def is_free(self):
28822882
def is_local_collision_mask(self):
28832883
"""Whether the contype and conaffinity bitmasks of this entity only applies to self-collision."""
28842884
return self._is_local_collision_mask
2885+
2886+
@gs.assert_built
2887+
def get_link_point_position_velocity(self, local_offsets, links_idx_local, envs_idx=None, *, unsafe=False):
2888+
"""
2889+
Compute the world position and velocity of points given their local offsets from link origins.
2890+
2891+
Parameters
2892+
----------
2893+
local_offsets : torch.Tensor or array_like
2894+
Local offsets from link origins. Shape should be:
2895+
- (n_points, 3) for multiple points
2896+
- (n_envs, n_points, 3) for batched environments
2897+
links_idx_local : torch.Tensor or array_like
2898+
The link indices for each point. Shape should be:
2899+
- (n_points,) for multiple points
2900+
- (n_envs, n_points) for batched environments
2901+
envs_idx : None | array_like, optional
2902+
The indices of the environments. If None, all environments will be considered. Defaults to None.
2903+
unsafe : bool, optional
2904+
Whether to skip safety checks. Defaults to False.
2905+
2906+
Returns
2907+
-------
2908+
positions : torch.Tensor
2909+
World positions of the points. Shape: (n_points, 3) or (n_envs, n_points, 3)
2910+
velocities : torch.Tensor
2911+
World velocities of the points. Shape matches positions.
2912+
2913+
Examples
2914+
--------
2915+
# Multiple points on different links
2916+
local_offsets = torch.tensor([
2917+
[0.1, 0.0, 0.05], # Point 1 offset
2918+
[0.02, 0.0, 0.01], # Point 2 offset
2919+
[0.08, 0.0, 0.05], # Point 3 offset
2920+
[0.0, 0.03, 0.08] # Point 4 offset
2921+
])
2922+
links_idx_local = torch.tensor([0, 1, 2, 2]) # Link indices for each point
2923+
2924+
pos, vel = entity.get_link_point_position_velocity(
2925+
local_offsets=local_offsets,
2926+
links_idx_local=links_idx_local
2927+
)
2928+
"""
2929+
links_pos = self.get_links_pos(links_idx_local, envs_idx, unsafe=unsafe)
2930+
links_quat = self.get_links_quat(links_idx_local, envs_idx, unsafe=unsafe)
2931+
links_vel = self.get_links_vel(links_idx_local, envs_idx, ref="link_origin", unsafe=unsafe)
2932+
links_ang = self.get_links_ang(links_idx_local, envs_idx, unsafe=unsafe)
2933+
2934+
world_positions = gu.transform_by_trans_quat(local_offsets, links_pos, links_quat)
2935+
arm_vectors = world_positions - links_pos
2936+
rotational_vel = torch.cross(links_ang, arm_vectors, dim=-1)
2937+
world_velocities = links_vel + rotational_vel
2938+
return world_positions, world_velocities

tests/test_rigid_physics.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,18 @@ def _build_multi_pendulum(n):
274274

275275

276276
@pytest.fixture(scope="session")
277-
def pendulum(asset_tmp_path):
278-
return _build_multi_pendulum(n=1)
277+
def one_pendulum(asset_tmp_path):
278+
279+
mjcf = ET.Element("mujoco", model="one_stick_robot")
280+
# ET.SubElement(mjcf, "option", timestep="0.05")
281+
default = ET.SubElement(mjcf, "default")
282+
ET.SubElement(default, "geom", contype="1", conaffinity="1", condim="3")
283+
worldbody = ET.SubElement(mjcf, "worldbody")
284+
link1 = ET.SubElement(worldbody, "body", name="body1", pos="0.0 0.0 0.0")
285+
ET.SubElement(link1, "geom", type="capsule", fromto="0 0 0 0.0 0 1.0", size="0.05", rgba="0 0 1 0.3")
286+
ET.SubElement(link1, "joint", type="hinge", name="joint1", axis="0 1 0")
287+
288+
return mjcf
279289

280290

281291
@pytest.fixture(scope="session")
@@ -2877,3 +2887,59 @@ def test_contype_conaffinity(show_viewer, tol):
28772887
assert_allclose(box1.get_pos(), np.array([0.0, 0.0, 0.25]), atol=1e-3)
28782888
assert_allclose(box2.get_pos(), np.array([0.0, 0.0, 0.75]), atol=1e-3)
28792889
assert_allclose(box3.get_pos(), np.array([0.0, 0.0, 0.75]), atol=1e-3)
2890+
2891+
2892+
@pytest.mark.required
2893+
@pytest.mark.merge_fixed_links(False)
2894+
@pytest.mark.parametrize("model_name", ["one_pendulum"])
2895+
@pytest.mark.parametrize("backend", [gs.cpu])
2896+
@pytest.mark.parametrize("gs_solver", [gs.constraint_solver.CG])
2897+
@pytest.mark.parametrize("gs_integrator", [gs.integrator.Euler])
2898+
def test_point_position_velocity(gs_sim, backend, tol):
2899+
"""Test get_link_point_position_velocity against analytical pendulum motion."""
2900+
scene = gs_sim
2901+
pendulum = scene.entities[0]
2902+
2903+
arm_link = pendulum.get_link("body1")
2904+
r = 1.0
2905+
local_point_offsets = torch.tensor([[0.0, 0.0, 1.0]], dtype=gs.tc_float, device=gs.device)
2906+
point_links_idx_local = torch.tensor([arm_link.idx_local], dtype=gs.tc_int, device=gs.device)
2907+
2908+
pendulum.set_dofs_position(torch.tensor([np.pi / 6], dtype=gs.tc_float, device=gs.device))
2909+
pendulum.set_dofs_velocity(torch.tensor([0.0], dtype=gs.tc_float, device=gs.device))
2910+
2911+
n_steps = 50
2912+
2913+
for step in range(n_steps):
2914+
scene.step()
2915+
2916+
theta = pendulum.get_dofs_position()[0]
2917+
theta_dot = pendulum.get_dofs_velocity()[0]
2918+
2919+
world_positions, world_velocities = pendulum.get_link_point_position_velocity(
2920+
local_offsets=local_point_offsets, links_idx_local=point_links_idx_local
2921+
)
2922+
2923+
pos_analytical = torch.tensor([r * np.sin(theta), 0.0, r * np.cos(theta)], dtype=gs.tc_float, device=gs.device)
2924+
2925+
# Velocity: v = r * θ̇ * tangent_direction
2926+
# Tangent direction = [cos(θ), 0, sin(θ)]
2927+
vel_analytical = torch.tensor(
2928+
[r * theta_dot * np.cos(theta), 0.0, -r * theta_dot * np.sin(theta)], dtype=gs.tc_float, device=gs.device
2929+
)
2930+
2931+
# Compare with API results
2932+
pos_queried = world_positions[0]
2933+
vel_queried = world_velocities[0]
2934+
2935+
# Calculate errors
2936+
pos_error = torch.norm(pos_queried - pos_analytical).item()
2937+
vel_error = torch.norm(vel_queried - vel_analytical).item()
2938+
print(step, "pos", pos_error, pos_queried, pos_analytical)
2939+
print(step, "vel", vel_error, vel_queried, vel_analytical)
2940+
2941+
assert_allclose(pos_error, 0.0, atol=tol, err_msg="position error too large")
2942+
# why cannot be tol?
2943+
assert_allclose(vel_error, 0.0, atol=0.02, err_msg="velocity error too large")
2944+
2945+
# Use appropriate tolerances

0 commit comments

Comments
 (0)