Skip to content

Commit ac3e755

Browse files
committed
fix(ik): non existing limits in mjcf are inf'ed
1 parent b9cc55d commit ac3e755

2 files changed

Lines changed: 72 additions & 26 deletions

File tree

‎python/tests/test_kinematics.py‎

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,93 @@
1414
common.RobotType("Yam"),
1515
]
1616

17+
# Robots with a genuine redundant DOF (7-DoF arms), where null-space biasing has room to act.
18+
REDUNDANT_ROBOTS = [
19+
common.RobotType.FR3,
20+
common.RobotType("XArm7"),
21+
]
22+
23+
# Identity pose / no TCP offset, reused across tests.
24+
NO_TCP_OFFSET = common.Pose()
25+
1726

1827
@pytest.mark.parametrize("robot_name", PIN_SUPPORTED_ROBOTS)
1928
def test_kinematics_identity(robot_name):
2029
robot = rcs.ROBOTS[robot_name]
21-
22-
# Determine model path and type
2330
model_path = robot.mjcf_model_path
24-
2531
frame_id = robot.attachment_site
32+
q_home = robot.q_home
2633

27-
# Initialize Pinocchio interface
34+
# Default Pin: limit-clamping on, no null-space bias.
2835
try:
2936
pin = common.Pin(model_path, frame_id, False)
3037
except Exception as e:
3138
pytest.fail(f"Failed to initialize Pin for {robot_name}: {e}")
3239

33-
q_home = robot.q_home
34-
35-
# Test 1: FK at home
36-
# Identity pose (no TCP offset)
37-
tcp_offset = common.Pose()
38-
39-
pose_home = pin.forward(q_home, tcp_offset)
40+
# Test 1: FK at home.
41+
pose_home = pin.forward(q_home, NO_TCP_OFFSET)
4042
assert isinstance(pose_home, common.Pose)
4143

42-
# Test 2: IK at home pose should return a solution (ideally close to q_home, but IK is redundant)
43-
# We use q_home as initial guess
44-
q_sol: np.ndarray | None = pin.inverse(pose_home, q_home, tcp_offset)
45-
44+
# Test 2: IK at the home pose returns a solution reaching that pose. The home
45+
# configuration is within the joint limits, so clamping does not interfere.
46+
q_sol: np.ndarray | None = pin.inverse(pose_home, q_home, NO_TCP_OFFSET)
4647
assert q_sol is not None, "IK failed for home pose"
4748

48-
# Verify the solution with FK
49-
pose_sol = pin.forward(q_sol, tcp_offset)
50-
51-
# Check if pose_sol is close to pose_home
49+
pose_sol = pin.forward(q_sol, NO_TCP_OFFSET)
5250
assert pose_sol.is_close(
5351
pose_home, eps_r=1e-4, eps_t=1e-4
5452
), f"FK(IK(pose)) does not match pose.\nOriginal: {pose_home}\nResult: {pose_sol}"
5553

56-
# Test 3: Perturbed configuration
57-
# Add small noise to q_home to test non-trivial pose
58-
# Ensure we stay within limits if possible, but for small noise it should be fine
54+
# Test 3: Perturbed configuration. We disable limit clamping here so that
55+
# reachability of FK(q_perturbed) does not depend on how close q_home sits to
56+
# a joint limit (e.g. SO101), keeping this a pure IK-convergence check.
57+
pin_free = common.Pin(model_path, frame_id, False, np.array([]), 0.0, False)
58+
5959
np.random.seed(42)
6060
q_perturbed = q_home + np.random.uniform(-0.1, 0.1, size=q_home.shape)
6161

62-
pose_perturbed = pin.forward(q_perturbed, tcp_offset) # type: ignore
63-
q_sol_perturbed: np.ndarray | None = pin.inverse(pose_perturbed, q_home, tcp_offset) # Use q_home as seed
64-
62+
pose_perturbed = pin_free.forward(q_perturbed, NO_TCP_OFFSET)
63+
q_sol_perturbed: np.ndarray | None = pin_free.inverse(pose_perturbed, q_home, NO_TCP_OFFSET)
6564
assert q_sol_perturbed is not None, "IK failed for perturbed pose"
6665

67-
pose_sol_perturbed = pin.forward(q_sol_perturbed, tcp_offset)
66+
pose_sol_perturbed = pin_free.forward(q_sol_perturbed, NO_TCP_OFFSET)
6867
assert pose_sol_perturbed.is_close(
6968
pose_perturbed, eps_r=1e-3, eps_t=1e-3
7069
), f"FK(IK(perturbed_pose)) does not match.\nOriginal: {pose_perturbed}\nResult: {pose_sol_perturbed}"
70+
71+
72+
@pytest.mark.parametrize("robot_name", REDUNDANT_ROBOTS)
73+
def test_kinematics_nullspace_bias(robot_name):
74+
"""A null-space target biases the redundant DOF toward the preferred posture
75+
without changing the achieved end-effector pose."""
76+
robot = rcs.ROBOTS[robot_name]
77+
model_path = robot.mjcf_model_path
78+
frame_id = robot.attachment_site
79+
q_home = robot.q_home
80+
81+
# Clamping off on both so the comparison isolates the null-space term.
82+
pin_plain = common.Pin(model_path, frame_id, False, np.array([]), 0.0, False)
83+
pin_ns = common.Pin(model_path, frame_id, False, q_home, 2.0, False) # bias toward home
84+
85+
# Target the home pose; a seed away from home exercises the redundancy so the
86+
# two solvers can settle on different configurations for the same pose.
87+
pose_home = pin_plain.forward(q_home, NO_TCP_OFFSET)
88+
q_seed = q_home.copy()
89+
q_seed[0] += 0.5
90+
q_seed[2] += 0.5
91+
q_seed[3] += 0.3
92+
93+
q_plain = pin_plain.inverse(pose_home, q_seed, NO_TCP_OFFSET)
94+
q_ns = pin_ns.inverse(pose_home, q_seed, NO_TCP_OFFSET)
95+
96+
assert q_plain is not None, "plain IK failed"
97+
assert q_ns is not None, "null-space IK failed"
98+
99+
# Both solutions must reach the same end-effector pose.
100+
assert pin_plain.forward(q_plain, NO_TCP_OFFSET).is_close(pose_home, eps_r=1e-3, eps_t=1e-3)
101+
assert pin_ns.forward(q_ns, NO_TCP_OFFSET).is_close(pose_home, eps_r=1e-3, eps_t=1e-3)
102+
103+
# The null-space-biased solution sits closer to the preferred (home) posture.
104+
d_plain = float(np.linalg.norm(q_plain - q_home))
105+
d_ns = float(np.linalg.norm(q_ns - q_home))
106+
assert d_ns < d_plain, f"null-space bias did not pull toward home: d_ns={d_ns} vs d_plain={d_plain}"

‎src/rcs/Kinematics.cpp‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "rcs/Kinematics.h"
22

3+
#include <cmath>
4+
#include <limits>
35
#include <pinocchio/algorithm/frames.hpp>
46
#include <pinocchio/algorithm/jacobian.hpp>
57
#include <pinocchio/algorithm/joint-configuration.hpp>
@@ -28,6 +30,14 @@ Pin::Pin(const std::string& path, const std::string& frame_id, bool urdf,
2830

2931
this->q_lower = this->model.lowerPositionLimit;
3032
this->q_upper = this->model.upperPositionLimit;
33+
const double inf = std::numeric_limits<double>::infinity();
34+
for (Eigen::Index i = 0; i < this->q_lower.size(); i++) {
35+
if (!std::isfinite(this->q_lower[i]) || !std::isfinite(this->q_upper[i]) ||
36+
this->q_lower[i] >= this->q_upper[i]) {
37+
this->q_lower[i] = -inf;
38+
this->q_upper[i] = inf;
39+
}
40+
}
3141
this->enforce_limits = enforce_limits;
3242

3343
this->nullspace_gain = nullspace_gain;

0 commit comments

Comments
 (0)