Inverse Kinematics
Inverse Kinematics (IK) calculates the required joint angles to reach a specific target position. For simple systems like a 2-bone planar arm, we can use exact analytical geometry (Law of Cosines) to find the solution instantly.
Analytical IK:
1# Analytical IK for a 2-bone planar arm2import math34def ik_2_bone(target_x, target_y, L1, L2):5 # Distance to target6 d_sq = target_x**2 + target_y**27 d = math.sqrt(d_sq)89 # Check if reachable10 if d > L1 + L2 or d < abs(L1 - L2):11 return None # Unreachable1213 # Law of Cosines for Joint 214 cos_q2 = (d_sq - L1**2 - L2**2) / (2 * L1 * L2)15 q2 = math.acos(cos_q2)1617 # Angles for Joint 118 alpha = math.atan2(target_y, target_x)19 beta = math.acos((d_sq + L1**2 - L2**2) / (2 * L1 * d))20 q1 = alpha - beta2122 return q1, q2
For arms with many DOFs, analytical solutions become too complex. We switch to numerical methods. The Jacobian matrix maps how changes in each joint angle (velocities) affect the position of the End Effector.
Jacobian Matrix:
1# Jacobian Matrix Construction2# The Jacobian relates joint velocities to end-effector velocities.34def build_jacobian(joint_positions, joint_axes, end_effector_pos):5 """Computes the 3xN Jacobian for a 3D robot arm."""6 import numpy as np78 J = np.zeros((3, len(joint_positions)))910 for i in range(len(joint_positions)):11 # Vector from joint to end effector12 r_i = end_effector_pos - joint_positions[i]1314 # Cross product of rotation axis and vector r_i15 # gives the linear velocity induced by this joint16 J[:, i] = np.cross(joint_axes[i], r_i)1718 return J
To move the arm towards the target, we need the inverse of the Jacobian to map spatial error back to joint angle changes. A fast and stable approximation is the Jacobian Transpose.
Jacobian Transpose:
1# Jacobian Transpose IK Solver2def step_jacobian_transpose(joints, target_pos, step_size=0.05):3 """Updates joint angles using the Jacobian transpose."""4 import numpy as np56 # 1. Forward Kinematics to get end effector7 end_effector = calc_fk(joints)89 # 2. Compute error vector to target10 error_vector = target_pos - end_effector1112 # 3. Build Jacobian matrix J13 J = build_jacobian(joints, ...)1415 # 4. Transpose J and multiply by error to get angle deltas16 delta_angles = np.dot(J.T, error_vector) * step_size1718 # 5. Apply update19 for i in range(len(joints)):20 joints[i] += delta_angles[i]
Cyclic Coordinate Descent (CCD) is a heuristic IK method. It solves the problem by adjusting one joint at a time. For a given joint, it calculates the angle required to point the end effector directly at the target.
CCD Step:
1# Cyclic Coordinate Descent (CCD) Single Step2def ccd_step(joint_pos, effector_pos, target_pos):3 """Adjusts a single joint to point the effector at the target."""4 import math5 import numpy as np67 # Vector from joint to effector8 v_effector = effector_pos - joint_pos9 v_effector /= np.linalg.norm(v_effector)1011 # Vector from joint to target12 v_target = target_pos - joint_pos13 v_target /= np.linalg.norm(v_target)1415 # Calculate angle between them using dot product16 cos_angle = np.dot(v_effector, v_target)17 angle = math.acos(max(-1.0, min(1.0, cos_angle)))1819 # Cross product gives the rotation axis and direction20 axis = np.cross(v_effector, v_target)2122 return angle, axis
The full CCD algorithm iterates over all joints, usually starting from the one closest to the end effector and working backwards to the base. This process is repeated until the end effector reaches the target or a maximum number of iterations is met.
CCD Loop:
1# Full CCD Iterative Loop2def ccd_solve(joints, target_pos, iterations=10, tolerance=0.01):3 """Iterates backward from effector to base repeatedly."""4 for _ in range(iterations):5 # Iterate from the last joint (closest to effector) to the first6 for i in range(len(joints) - 1, -1, -1):7 effector_pos = calc_fk(joints)[-1]89 # Check convergence10 if distance(effector_pos, target_pos) < tolerance:11 return True1213 # Perform single step on joint i14 angle, axis = ccd_step(joints[i].pos, effector_pos, target_pos)15 joints[i].rotate(angle, axis)1617 return False
Pure IK algorithms can generate mathematically correct solutions that are physically impossible (e.g. arms bending backwards through themselves). By clamping joint angles at each step of the solver, we ensure realistic poses, though it may take more iterations to converge (or prevent reaching some targets entirely).
Constrained IK:
1# IK with Joint Constraints2def ccd_step_constrained(joint_idx, joints, target_pos):3 """Applies constraints after calculating the raw CCD update."""45 # 1. Calculate raw update angle6 raw_angle, axis = ccd_step(joints[joint_idx], target_pos)78 # 2. Add to current angle9 new_angle = joints[joint_idx].angle + raw_angle1011 # 3. Clamp against physical limits12 clamped_angle = clamp(new_angle, joints[joint_idx].min, joints[joint_idx].max)1314 # 4. Apply clamped angle15 joints[joint_idx].angle = clamped_angle
When a robot arm is fully extended (a kinematic singularity), numerical solvers like the pure Jacobian pseudo-inverse fail because the matrix becomes non-invertible (divide by zero). Methods like Damped Least Squares (DLS) add a small damping factor to maintain stability at the cost of slight accuracy.
Damped Least Squares:
1# Damped Least Squares (DLS)2def solve_dls(J, error, damping=0.1):3 """Avoids singularities by adding a damping factor."""4 import numpy as np56 # Normally we want J_inverse = J^T * (J * J^T)^-17 # Near singularities, det(J*J^T) approaches 0.89 # DLS adds damping^2 * Identity to the inverted part10 I = np.eye(J.shape[0])11 J_T = J.T1213 # (J*J^T + lambda^2 * I)^-114 inv_term = np.linalg.inv(np.dot(J, J_T) + (damping**2) * I)1516 # J^T * inv_term * error17 pseudo_inverse = np.dot(J_T, inv_term)1819 return np.dot(pseudo_inverse, error)
In interactive applications (like games or VR), IK solvers run continuously in the background, often distributing iterations across multiple frames. This creates a fluid, organic movement as the arm smoothly tracks a dynamic target without stalling the simulation.
Continuous Tracking:
1# Interactive IK Tracking2def update_loop(mouse_pos, dt):3 """Continuously solves IK towards a moving target."""45 # 1. Update target position from input6 target = get_mouse_world_position()78 # 2. Run a few IK iterations per frame9 # We don't need full convergence every frame if the target is moving,10 # distributing the solver load creates a smooth tracking effect.11 ccd_solve(joints, target, iterations=2)1213 # 3. Render14 draw_robot(joints)