🦾 Inverse Kinematics (IK)¶
Inverse Kinematics converts the target pose of an end-effector into joint angles.
For complete runnable code, see OrcaPlayground examples/euler/06_jacobian/.
What is IK?¶
Forward Kinematics (FK): joint angles → end-effector pose (unique solution)
Inverse Kinematics (IK): end-effector pose → joint angles (may have multiple or no solutions)
IK presents three major challenges: 1. Redundancy: end-effector has 6 DOF, joints may have more → infinitely many solutions 2. Singularities: Jacobian degenerates in certain poses → tiny end-effector displacement requires infinite joint velocity 3. Joint Limits: solutions must satisfy physical limits
Complete Example: The Big Picture¶
Below is a complete IK example that lifts the G1 left foot by approximately 10cm. It uses damped least squares + joint limit clamping + a two-phase strategy.
"""Damped Least Squares IK: G1 left foot lifted ~10cm"""
import numpy as np
# G1's 29 revolute joint suffixes
G1_ROT_JOINT_SUFFIXES = [
"left_hip_pitch_joint", "left_hip_roll_joint", "left_hip_yaw_joint",
"left_knee_joint", "left_ankle_pitch_joint", "left_ankle_roll_joint",
"right_hip_pitch_joint", "right_hip_roll_joint", "right_hip_yaw_joint",
"right_knee_joint", "right_ankle_pitch_joint", "right_ankle_roll_joint",
"waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint",
"left_shoulder_pitch_joint", "left_shoulder_roll_joint", "left_shoulder_yaw_joint",
"left_elbow_joint", "left_wrist_roll_joint", "left_wrist_pitch_joint", "left_wrist_yaw_joint",
"right_shoulder_pitch_joint", "right_shoulder_roll_joint", "right_shoulder_yaw_joint",
"right_elbow_joint", "right_wrist_roll_joint", "right_wrist_pitch_joint", "right_wrist_yaw_joint",
]
def damped_least_squares_ik(env, foot_suffix="left_ankle_roll_link",
offset=np.array([0.0, 0.05, 0.10]),
damping=0.05, step=0.05, iters=80, atol=0.02):
"""Damped least squares IK: move the foot body to a target offset position.
Two phases:
1. Preset slight-crouch pose — avoid paths that violate the joint's natural direction from a fully extended state
2. IK iteration — damped least squares + joint limit clamping
Args:
env: OrcaGymEulerEnv instance
foot_suffix: foot body suffix (without agent prefix)
offset: target offset [dx, dy, dz] (world frame, meters)
damping: damping coefficient (larger = more stable, smaller = faster convergence)
step: maximum joint change per step (radians)
iters: maximum number of iterations
atol: convergence threshold (meters)
Returns:
Final foot position (3,) np.ndarray
"""
agent = env._agent_names[0]
foot_body = f"{agent}_{foot_suffix}"
# ── Prepare G1 joint info ──
joint_names = [f"{agent}_{s}" for s in G1_ROT_JOINT_SUFFIXES]
dof_adrs = [env.jnt_dofadr(jn) for jn in joint_names]
qpos_adrs = [env.jnt_qposadr(jn) for jn in joint_names]
# Column range of G1 joints in the global DOF array (must NOT use [7:] in multi-body scenes!)
v_min, v_max = min(dof_adrs), max(dof_adrs)
g1_joint_cols = slice(v_min, v_max + 1)
# Joint limits
jdict = env.model.get_joint_dict()
jnt_lo = np.array([
jdict[jn]["Range"][0] if jdict[jn]["Limited"] else -np.inf
for jn in joint_names
])
jnt_hi = np.array([
jdict[jn]["Range"][1] if jdict[jn]["Limited"] else np.inf
for jn in joint_names
])
# ═══════════════════════════════════════════
# Phase 1: Preset slight-crouch pose
# ═══════════════════════════════════════════
# Starting from a fully extended state, pure DLS may take a path that
# violates the joint's natural direction (e.g., knee bending backward).
# Preset forward knee bend + hip flexion + ankle dorsiflexion so IK
# starts from a reasonable pose.
preset = {
f"{agent}_left_knee_joint": 0.6,
f"{agent}_left_hip_pitch_joint": -0.3,
f"{agent}_left_ankle_pitch_joint": -0.3,
f"{agent}_right_knee_joint": 0.6,
f"{agent}_right_hip_pitch_joint": -0.3,
f"{agent}_right_ankle_pitch_joint": -0.3,
}
qpos = env.data.qpos.copy()
for jn, val in preset.items():
qpos[env.jnt_qposadr(jn)] = val
env.set_joint_qpos(qpos)
env.mj_forward()
print(f" Phase 1 complete: preset slight-crouch pose")
# ═══════════════════════════════════════════
# Phase 2: Damped least squares IK iteration
# ═══════════════════════════════════════════
foot_pos = env.get_body_xpos_xmat_xquat([foot_body])[foot_body]["xpos"]
target = foot_pos + offset
print(f" Phase 2 start: target={target}, initial={foot_pos}")
jacr = np.zeros((3, env.model.nv))
for i in range(iters):
# (a) Compute foot Jacobian
jacp_foot = np.zeros((3, env.model.nv))
env.mj_jacBody(jacp_foot, jacr, body_name=foot_body)
# (b) Current error
cur = env.get_body_xpos_xmat_xquat([foot_body])[foot_body]["xpos"]
delta = target - cur
# (c) Damped least squares: dq = J^T (J J^T + lambda^2 I)^(-1) delta_x
jac_leg = jacp_foot[:, g1_joint_cols]
dq = jac_leg.T @ np.linalg.inv(
jac_leg @ jac_leg.T + damping**2 * np.eye(3)
) @ delta
# (d) Write compliantly + limit clamping
qpos = env.data.qpos.copy()
for j, qadr in enumerate(qpos_adrs):
qpos[qadr] = np.clip(
qpos[qadr] + dq[j] * step, jnt_lo[j], jnt_hi[j]
)
env.set_joint_qpos(qpos)
env.mj_forward()
# (e) Convergence check
err = np.linalg.norm(delta)
if err < atol:
print(f" IK converged at iteration {i + 1}, error {err:.4f}m")
break
else:
print(f" IK did not converge, final error {err:.4f}m")
return env.get_body_xpos_xmat_xquat([foot_body])[foot_body]["xpos"]
# ============================================================
# Usage Example
# ============================================================
if __name__ == "__main__":
# env is your OrcaGymEulerEnv instance (already reset)
final_pos = damped_least_squares_ik(
env,
foot_suffix="left_ankle_roll_link",
offset=np.array([0.0, 0.05, 0.10]), # y+5cm, z+10cm
)
print(f"Left foot final position: {final_pos}")
Step-by-Step Explanation¶
Damped Least Squares Principle¶
The standard Jacobian pseudoinverse J^+ = J^T(J J^T)^(-1) diverges near singularities.
Damped Least Squares (DLS) adds a regularization term lambda^2 I:
| lambda value | Behavior |
|---|---|
| lambda = 0 | Degrades to standard pseudoinverse, unstable near singularities |
| lambda small | Fast convergence, but may oscillate near singularities |
| lambda large | Smoother, more stable solution, but slower convergence |
IK Iteration Workflow¶
Each iteration:
1. mj_jacBody → foot Jacobian jacp_foot (3, nv)
2. jac_leg = jacp_foot[:, g1_dof_min:g1_dof_max+1] ← only take G1 joint columns
3. delta_x = target - current_xpos
4. dq = J^T (J J^T + lambda^2 I)^(-1) delta_x ← damped least squares
5. q ← clip(q + dq * step, jnt_lo, jnt_hi) ← limit clamping
6. set_joint_qpos(q) + mj_forward()
7. Check ||delta_x|| < ATOL?
Why Two Phases?¶
At G1's default qpos=0, the knees are fully extended. Starting from an extended state,
a pure mathematical DLS solution may cause the knees to bend backward
(opposite to the joint's natural direction) — this is mathematically correct but physically infeasible.
Phase 1 — Preset Slight Crouch: - Knee forward bend +0.6 rad (~34 degrees) - Hip flexion -0.3 rad - Ankle dorsiflexion -0.3 rad (compensates to keep foot sole level) - Applied symmetrically to both legs
This way, IK starts from an already-bent state, and the solution naturally continues bending forward along the joint's positive direction to lift the foot.
Phase 2 — IK Iteration to Lift Foot: Damped least squares + limit clamping, 80 iterations converging to ~2cm precision.
DOF Columns in Multi-Body Scenarios¶
The column range of G1 joints in the global DOF array is NOT [7:]! In multi-body scenes
(G1 + manipulated object + toy), other bodies' DOFs may be interleaved.
Correct approach: Get each joint's DOF address via jnt_dofadr, then construct the [min, max+1] range:
dof_adrs = [env.jnt_dofadr(jn) for jn in joint_names]
g1_joint_cols = slice(min(dof_adrs), max(dof_adrs) + 1)
Joint Limit Clamping¶
After each IK iteration, clamp to joint limits to prevent generating unreachable poses:
jdict = env.model.get_joint_dict()
for jn in joint_names:
lo, hi = jdict[jn]["Range"] # [lower, upper]
limited = jdict[jn]["Limited"] # True/False
# clamp
qpos[qadr] = np.clip(qpos[qadr] + dq * step, lo, hi)
Parameter Quick Reference¶
| Parameter | Function | Recommended | Tuning |
|---|---|---|---|
damping |
Numerical stability | 0.01–0.1 | Increase if not converging, decrease if too slow |
step |
Joint change per step | 0.02–0.1 | Decrease if oscillating |
iters |
Max iterations | 50–200 | Increase for distant targets |
atol |
Convergence threshold (m) | 0.01–0.05 | Decrease for higher precision |
Common Issues¶
| Symptom | Cause | Solution |
|---|---|---|
| IK does not converge | Damping too small / target too far | Increase damping, reduce offset |
| Joint bends in the wrong direction | Starting from fully extended state | Preset slight-crouch pose |
| Jacobian is all zeros | No forward call / wrong body name | Ensure mj_forward + agent prefix |
| Multi-body scene anomalies | Wrong DOF column range | Use jnt_dofadr for [min, max] |
Next Steps¶
Now that you've mastered IK, learn how to apply external forces and the complete force + IK workflow: 🔄 External Force Application and IK.