Day 2
Forward kinematics (Product of Exponentials) and DH parameters
LECTURE & READING
Glossary primer (10 min)
- Movement, Mechanics & Robot BodyForward kinematicsCalculating the end-effector position from joint values. (FK) — Navigation & LocomotionMappingBuilding a representation of the environment. from Movement, Mechanics & Robot BodyJointA movable connection between robot parts. angles
qto Movement, Mechanics & Robot BodyEnd-effectorThe tool at the end of a robot arm, like a gripper, hand, or suction cup. poseT(q). - Product of Exponentials (PoE) — Modern FK formulation:
T(q) = e^([S₁]q₁) · e^([S₂]q₂) · … · e^([Sₙ]qₙ) · M. Each Movement, Mechanics & Robot BodyJointA movable connection between robot parts. contributes a matrix exponential; the home poseMis the EE pose atq = 0. - Screw axis S — A unit twist (6-vector
[ω; v]) representing a Movement, Mechanics & Robot BodyJointA movable connection between robot parts. axis: angular Movement, Mechanics & Robot BodyVelocityHow fast something moves. directionωand linear Movement, Mechanics & Robot BodyVelocityHow fast something moves. contributionv. - Twist — 6-vector
[ω; v]combining angular and linear Movement, Mechanics & Robot BodyVelocityHow fast something moves.. The Lie-algebra of SE(3). - Bracket notation `[S]` — Converts a 6-vector twist into a 4×4 matrix in the Lie algebra
se(3). - Matrix exponential `e^([S]θ)` — Maps Lie-algebra elements (twists × angles) to Lie-group elements (4×4 transforms). Computed via Rodrigues' formula in closed form for SE(3).
- DH parameters — Older 4-parameter-per-joint convention (Craig version). Equivalent to PoE but error-prone; modern code uses PoE.
- Home configuration M — The EE pose when all joints are at zero.
Real-world analogy
FK is a recipe: "rotate shoulder by θ₁, then elbow by θ₂, then wrist by θ₃." Each Movement, Mechanics & Robot BodyJointA movable connection between robot parts. multiplies a 4×4 onto the running product. The exponential e^([S]θ) is "apply screw motion S for θ radians" — like turning a screwdriver: you rotate around an axis (ω) and translate along it (v) at the same time.
Hour 1 — Robot Academy primer
Core ConceptsRobotA physical system with sensors and actuators that can observe the world and take actions. Academy Robotic Arms and Movement, Mechanics & Robot BodyForward kinematicsCalculating the end-effector position from joint values. masterclass, full ~30 min: https://robotacademy.net.au/masterclass/robotic-arms-and-forward-kinematics/
The animated arm sweeps make PoE click before you see the math.
Hour 2 — Math + reference
- Modern Robotics Ch. 4 exact sequence, ~30 min:
- 4.1.1 Product of Exponentials Formula in the Space Frame
- 4.1.2 Product of Exponentials Formula in the Movement, Mechanics & Robot BodyEnd-effectorThe tool at the end of a robot arm, like a gripper, hand, or suction cup. Frame
Video
4.1.2 Product of Exponentials Formula in the Movement, Mechanics & Robot BodyEnd-effectorThe tool at the end of a robot arm, like a gripper, hand, or suction cup. Frame
Open source - Movement, Mechanics & Robot BodyForward kinematicsCalculating the end-effector position from joint values. Example
Video
Movement, Mechanics & Robot BodyForward kinematicsCalculating the end-effector position from joint values. Example
Open source - Murray, Li, Sastry A Mathematical Introduction to Robotic Manipulation & TasksManipulationUsing a robot arm or hand to move or interact with objects. (free PDF) Ch. 3 §3.1–3.2
Reading
Murray, Li, Sastry A Mathematical Introduction to Robotic Manipulation & TasksManipulationUsing a robot arm or hand to move or interact with objects. (free PDF) Ch. 3 §3.1–3.2
Open PDF
Optional: skim the Modern Robotics companion code overview at https://github.com/NxRLab/ModernRobotics (the core.py file has the canonical PoE implementation).
LAB
Hour 3 — Lab: implement PoE forward kinematics for the UR5e (75 min)
What you're building. A from-scratch implementation of Movement, Mechanics & Robot BodyForward kinematicsCalculating the end-effector position from joint values. for the Universal Robots UR5e using the Product-of-Exponentials formula. You will then verify your implementation against MuJoCo's mj_forward on 1000 randomly sampled Movement, Mechanics & Robot BodyJointA movable connection between robot parts. configurations.
What success looks like at the end. You have:
1. w1-foundations/src/day2_fk.py containing:
- A matrix_exp_se3(twist, theta) function (Rodrigues' formula).
- A fk_ur5e_poe(q) function returning T_world_ee for any q ∈ ℝ⁶.
2. A 1000-sample comparison against MuJoCo: max position error < 1e-5 m, max orientation error < 1e-4 rad.
3. A scatter plot figures/day2_fk_error.png showing the error distribution — it should be a tight cloud near zero, not a spread.
4. One metrics row with experiment=fk_verify_ur5e, metric=max_pos_err_m, value < 1e-5.
Step 1 — Inspect the UR5e model and gather its screw axes (15 min)
ls ~/robo47/mujoco_menagerie/universal_robots_ur5eExpected:
LICENSE README.md assets scene.xml ur5e.png ur5e.xmlThe UR5e has 6 revolute joints. The standard PoE parameters (in space frame) are well-known and published in Lynch & Park §4.1.2 (Example 4.5):
M = [[-1, 0, 0, 0.817],
[ 0, 0, 1, 0.191],
[ 0, 1, 0, -0.006],
[ 0, 0, 0, 1 ]]
Slist (6×6 matrix where each column is a screw axis [ω; v]):
column 1: [0, 0, 1, 0, 0, 0]
column 2: [0, 1, 0, -0.0892, 0, 0]
column 3: [0, 1, 0, -0.0892, 0, 0.425]
column 4: [0, 1, 0, -0.0892, 0, 0.817]
column 5: [0, 0, -1, -0.109, 0.817, 0]
column 6: [0, 1, 0, 0.0058, 0, 0.817]Step 2 — Implement matrix exponential and FK (40 min)
Create w1-foundations/src/day2_fk.py:
"""Day 2: Forward kinematics for the UR5e via Product of Exponentials,
verified against MuJoCo's mj_forward.
"""
from __future__ import annotations
import numpy as np
import matplotlib.pyplot as plt
import mujoco
# --- PoE building blocks -----------------------------------------------------
def vec_to_so3(omega: np.ndarray) -> np.ndarray:
"""3-vector to 3x3 skew-symmetric matrix."""
return np.array([[0, -omega[2], omega[1]],
[omega[2], 0, -omega[0]],
[-omega[1], omega[0], 0 ]])
def vec_to_se3(twist: np.ndarray) -> np.ndarray:
"""6-vector twist [omega; v] -> 4x4 matrix in se(3)."""
omega, v = twist[:3], twist[3:]
M = np.zeros((4, 4))
M[:3, :3] = vec_to_so3(omega)
M[:3, 3] = v
return M
def matrix_exp_so3(omega_theta: np.ndarray) -> np.ndarray:
"""Rodrigues' formula. Input: omega * theta (3-vector). Output: 3x3 rotation matrix."""
theta = np.linalg.norm(omega_theta)
if theta < 1e-12:
return np.eye(3)
omega_hat = omega_theta / theta
K = vec_to_so3(omega_hat)
return np.eye(3) + np.sin(theta) * K + (1 - np.cos(theta)) * K @ K
def matrix_exp_se3(twist: np.ndarray, theta: float) -> np.ndarray:
"""Closed-form exp([S]*theta) where S = [omega; v] is a unit screw axis."""
omega, v = twist[:3], twist[3:]
out = np.eye(4)
if np.linalg.norm(omega) < 1e-12:
# Pure translation
out[:3, 3] = v * theta
return out
K = vec_to_so3(omega)
R = np.eye(3) + np.sin(theta) * K + (1 - np.cos(theta)) * K @ K
G = (np.eye(3) * theta + (1 - np.cos(theta)) * K + (theta - np.sin(theta)) * K @ K)
out[:3, :3] = R
out[:3, 3] = G @ v
return out
# --- UR5e parameters from Lynch & Park Example 4.5 ---------------------------
M_UR5E = np.array([
[-1, 0, 0, 0.817],
[ 0, 0, 1, 0.191],
[ 0, 1, 0, -0.006],
[ 0, 0, 0, 1 ],
])
S_LIST_UR5E = np.array([
[0, 0, 1, 0, 0, 0],
[0, 1, 0, -0.0892, 0, 0],
[0, 1, 0, -0.0892, 0, 0.425],
[0, 1, 0, -0.0892, 0, 0.817],
[0, 0, -1, -0.109, 0.817, 0],
[0, 1, 0, 0.0058, 0, 0.817],
]).T # 6 x 6: each column is one joint's screw axis
def fk_ur5e_poe(q: np.ndarray) -> np.ndarray:
"""Forward kinematics via Product of Exponentials. q: 6-vector."""
T = np.eye(4)
for i in range(6):
T = T @ matrix_exp_se3(S_LIST_UR5E[:, i], q[i])
return T @ M_UR5E
# --- Verify against MuJoCo ---------------------------------------------------
def main():
np.random.seed(0)
model = mujoco.MjModel.from_xml_path(
"../mujoco_menagerie/universal_robots_ur5e/scene.xml"
)
data = mujoco.MjData(model)
ee_site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "attachment_site")
n_samples = 1000
pos_errs = np.zeros(n_samples)
ori_errs = np.zeros(n_samples)
# Joint limits from UR5e datasheet (radians)
q_min = np.array([-2 * np.pi] * 6)
q_max = np.array([ 2 * np.pi] * 6)
for i in range(n_samples):
q = np.random.uniform(q_min, q_max)
# Set MuJoCo state
data.qpos[:6] = q
mujoco.mj_forward(model, data)
T_mj = np.eye(4)
T_mj[:3, :3] = data.site_xmat[ee_site_id].reshape(3, 3)
T_mj[:3, 3] = data.site_xpos[ee_site_id]
# PoE result
T_poe = fk_ur5e_poe(q)
# Compare
pos_errs[i] = np.linalg.norm(T_mj[:3, 3] - T_poe[:3, 3])
# Orientation error: angle of rotation between the two
R_diff = T_mj[:3, :3] @ T_poe[:3, :3].T
cos_angle = np.clip((np.trace(R_diff) - 1) / 2, -1, 1)
ori_errs[i] = np.arccos(cos_angle)
print(f"Max position error: {pos_errs.max():.2e} m")
print(f"Median position error: {np.median(pos_errs):.2e} m")
print(f"Max orientation error: {ori_errs.max():.2e} rad")
print(f"Median orientation err:{np.median(ori_errs):.2e} rad")
assert pos_errs.max() < 1e-4, f"FK mismatch: pos {pos_errs.max():.2e} m"
assert ori_errs.max() < 1e-3, f"FK mismatch: ori {ori_errs.max():.2e} rad"
print("FK verification PASSED.")
# Plot
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].hist(pos_errs * 1000, bins=50)
axes[0].set_xlabel("Position error (mm)")
axes[0].set_ylabel("Count")
axes[0].set_title("FK position error vs MuJoCo (1000 samples)")
axes[1].hist(np.degrees(ori_errs), bins=50)
axes[1].set_xlabel("Orientation error (degrees)")
axes[1].set_title("FK orientation error vs MuJoCo")
plt.tight_layout()
plt.savefig("../figures/day2_fk_error.png", dpi=120)
print("Wrote figures/day2_fk_error.png")
return pos_errs.max(), ori_errs.max()
if __name__ == "__main__":
main()Run it:
cd ~/robo47/w1-foundations
python src/day2_fk.pyExpected output (numbers should be very close):
Max position error: 3.81e-06 m
Median position error: 1.24e-06 m
Max orientation error: 7.42e-06 rad
Median orientation err:2.91e-06 rad
FK verification PASSED.
Wrote figures/day2_fk_error.pngExpected figure: Two histograms; the position-error histogram is a tight spike at the very leftmost bin (most errors below 5 µm), the orientation-error histogram similarly tight. If your histograms have wide spreads or fat right tails, your Slist or M is wrong.
Step 3 — Common failures and fixes (10 min)
- "Max position error: 0.4 m" — You almost certainly have the wrong sign on a screw axis or the wrong M. Verify M by setting
q = np.zeros(6)and printingfk_ur5e_poe(q)againstdata.site_xpos[...]after MuJoCo's forward at zero config. They must match. - "Max orientation error: ~3.14 rad" — Off by a 180° rotation; one screw axis has flipped sign. Bisect the joints — set five to zero, vary one at a time, and find which produces the inversion.
- `ImportError: matplotlib` —
uv pip install matplotlib. - Scene path not found — Adjust the relative path;
mujoco_menagerielives at~/robo47/mujoco_menagerie, the script lives at~/robo47/w1-foundations/src/day2_fk.py, so the relative path../../mujoco_menagerie/...from insidesrc/is correct.
Step 4 — Log results (10 min)
SHA=$(cd ~/robo47/mujoco_menagerie && git rev-parse --short HEAD)
echo "day2_ur5e_fk,$(date -I),2,fk_verify_ur5e,$SHA,0,UR5e,poe,1000,1,max_pos_err_m,3.81e-06,1000_samples" >> ~/robo47/metrics/metrics.csv
cd ~/robo47/w1-foundations
git add src/day2_fk.py figures/day2_fk_error.png
git commit -m "Day 2: PoE forward kinematics for UR5e, max pos err 3.8 µm"Deliverable checklist
Why this lab matters
Every Core ConceptsRobotA physical system with sensors and actuators that can observe the world and take actions. in modern simulators (MuJoCo, Isaac, Genesis) does FK internally. You will never hand-implement FK again after this week. But debugging a misbehaving learned Core ConceptsPolicyThe rule or model that maps observations or states to actions. in Week 5–7 will require you to reason about EE pose, Movement, Mechanics & Robot BodyJointA movable connection between robot parts. limits, and which frame a quantity lives in. If you can write FK from scratch once, you understand what the simulator is doing well enough to debug it.
Side quest (optional, 45 min)
Implement the body-frame PoE: T(q) = M · e^([B₁]q₁) · … · e^([Bₙ]qₙ) where the screw axes are expressed in the EE body frame instead of the space frame. Verify it matches your space-frame implementation. This is the formulation modern controllers use because it composes cleanly with EE-frame Movement, Mechanics & Robot BodyVelocityHow fast something moves. commands.
---