Day 3

Inverse kinematics, Jacobians, singularities

LECTURE & READING

Glossary primer (12 min)

  • Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. — Given a desired EE pose T_des, find Movement, Mechanics & Robot BodyJointA movable connection between robot parts. angles q such that FK(q) = T_des.
  • Jacobian J(q) — A 6×n matrix linking Movement, Mechanics & Robot BodyJointA movable connection between robot parts. velocities to EE velocities: [ω_ee; v_ee] = J(q) · q̇.
  • Geometric / spatial Jacobian — Two flavors of Jacobian, expressed in different frames. Spatial frame for PoE in space form; body frame for PoE in body form.
  • Singularity — A configuration where det(J · J.T) = 0; the Core ConceptsRobotA physical system with sensors and actuators that can observe the world and take actions. can't move in some EE direction.
  • Damped Least Squares (DLS) Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. — Iterative Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. that survives singularities: q̇ = J.T · (J J.T + λ² I)⁻¹ · ẋ_des.
  • Newton-Raphson Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. — Iterative Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. without damping: q̇ = J⁺ · ẋ_des (pseudoinverse). Fast when far from singularities, explodes near them.
  • Null-space — Directions in joint-space that don't move the EE. Redundant arms (n > 6) have a non-trivial null-space; you can use it to satisfy secondary objectives like "stay away from Movement, Mechanics & Robot BodyJointA movable connection between robot parts. limits".
  • Manipulability ellipsoid — Geometric visualization of how easily the EE can move in different directions; axes are the singular values of J.

Real-world analogy

The Jacobian is the gear ratio between Movement, Mechanics & Robot BodyJointA movable connection between robot parts. motion and EE motion. A high gear ratio means small Movement, Mechanics & Robot BodyJointA movable connection between robot parts. moves cause large EE moves (the arm is dexterous in that direction). A singular Jacobian is a car with a locked differential — you can press the accelerator (Movement, Mechanics & Robot BodyJointA movable connection between robot parts. Movement, Mechanics & Robot BodyTorqueA rotational force around a joint or axis.) but the wheels (EE) won't move in the desired direction.

Hour 1 — Robot Academy primer

The manipulability-ellipsoid animation is one of the things Corke is genuinely best in the world at explaining.

Hour 2 — Math

  • Modern Robotics Ch. 5 exact sequence, ~25 min:
  • Movement, Mechanics & Robot BodyVelocityHow fast something moves. Movement, Mechanics & Robot BodyKinematicsThe study of motion without considering forces. and Statics

    Video

    Movement, Mechanics & Robot BodyVelocityHow fast something moves. Movement, Mechanics & Robot BodyKinematicsThe study of motion without considering forces. and Statics

    Open source
  • 5.1.1 Space Jacobian
  • 5.1.2 Body Jacobian
  • 5.2 Statics of Open Chains
  • 5.3 Singularities
  • 5.4 Manipulability
  • Lynch & Park Ch. 6 (Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose., numerical Newton-Raphson)
  • Stanford CS223A Lec 6 "Instantaneous Movement, Mechanics & Robot BodyKinematicsThe study of motion without considering forces."

    Video

    Stanford CS223A Lec 6 "Instantaneous Movement, Mechanics & Robot BodyKinematicsThe study of motion without considering forces.

    Open source

LAB

Hour 3 — Lab: keyboard-teleop a Panda with DLS IK (75 min)

What you're building. A Python program that runs a MuJoCo Simulation & Sim-to-RealSimulationA virtual environment where robots can be trained or tested. of the Franka Panda at 60 Hz, accepts keyboard input (W/A/S/D for X/Y, Q/E for Z, arrow keys for orientation), and uses Damped Least Squares Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. to drive the EE toward your commanded pose. You will deliberately steer it toward a singularity, observe the Jacobian's condition number spike, and plot it.

What success looks like at the end. You have: 1. A 30-second screen recording videos/day3_teleop.mp4 showing you piloting the Panda's Movement, Mechanics & Robot BodyGripperA common end-effector used to grasp objects. smoothly through space using keyboard. 2. A plot figures/day3_jacobian_condition.png showing condition number cond(J) over time, with one or two clear spikes (>500) marking moments you got close to a singularity. 3. src/day3_dls_ik.py containing the runnable program.

Step 1 — Install keyboard handling and verify viewer works (10 min)

cd ~/robo47
source .venv/bin/activate
uv pip install pynput

Quick sanity test that the viewer runs in passive mode (you'll embed it in a Python loop instead of using the standalone viewer):

python -c "
import mujoco, mujoco.viewer, time
m = mujoco.MjModel.from_xml_path('mujoco_menagerie/franka_emika_panda/scene.xml')
d = mujoco.MjData(m)
with mujoco.viewer.launch_passive(m, d) as v:
    for _ in range(180):  # 3 seconds at 60 fps
        mujoco.mj_step(m, d)
        v.sync()
        time.sleep(1/60)
print('passive viewer ok')
"

Expected: Window opens for ~3 s, Panda visible. If you're on Nebius headless, do this lab on your laptop instead — keyboard Imitation & Reinforcement LearningTeleoperation (teleop)A human remotely controlling the robot, often to collect demonstrations. needs a display.

Step 2 — Write the teleop loop (40 min)

Create w1-foundations/src/day3_dls_ik.py:

"""Day 3: Keyboard-teleop the Franka Panda using Damped Least Squares IK.
Press WASD/QE for translation; arrow keys for orientation; ESC to quit.
"""
from __future__ import annotations
import time
import numpy as np
import mujoco
import mujoco.viewer
import matplotlib.pyplot as plt
from pynput import keyboard

# --- Config ------------------------------------------------------------------
DT = 1.0 / 60.0
LIN_SPEED = 0.15   # m/s
ANG_SPEED = 0.5    # rad/s
DAMPING = 0.05     # DLS lambda
TIMEOUT_SEC = 60.0

MODEL_PATH = "../mujoco_menagerie/franka_emika_panda/scene.xml"

# --- Keyboard state (shared across threads) ---------------------------------
keys_held: set = set()

def on_press(key):
    try:
        keys_held.add(key.char)
    except AttributeError:
        keys_held.add(key)

def on_release(key):
    try:
        keys_held.discard(key.char)
    except AttributeError:
        keys_held.discard(key)
    if key == keyboard.Key.esc:
        return False  # stop listener

# --- DLS IK ------------------------------------------------------------------

def dls_step(jacp: np.ndarray, jacr: np.ndarray,
             dx: np.ndarray, dr: np.ndarray, lam: float = DAMPING) -> np.ndarray:
    """One step of damped least-squares IK. Returns delta-q for the 7 arm joints."""
    J = np.vstack([jacp, jacr])         # 6 x n_dof
    e = np.concatenate([dx, dr])        # 6
    JJt = J @ J.T
    dq = J.T @ np.linalg.solve(JJt + lam**2 * np.eye(6), e)
    return dq

# --- Main --------------------------------------------------------------------

def main():
    model = mujoco.MjModel.from_xml_path(MODEL_PATH)
    data = mujoco.MjData(model)

    # Home pose for the arm
    data.qpos[:7] = [0, -0.785, 0, -2.356, 0, 1.571, 0.785]
    mujoco.mj_forward(model, data)

    ee_site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "attachment_site")

    listener = keyboard.Listener(on_press=on_press, on_release=on_release)
    listener.start()

    print("Teleop ready. WASD = X/Y, QE = Z, arrows = orientation, ESC = quit.")

    cond_history: list = []
    t_history: list = []
    t_start = time.time()

    with mujoco.viewer.launch_passive(model, data) as viewer:
        while viewer.is_running() and (time.time() - t_start) < TIMEOUT_SEC:
            t = time.time() - t_start
            # --- Build commanded EE velocity from keyboard
            v = np.zeros(3)
            w = np.zeros(3)
            if 'w' in keys_held: v[0] += LIN_SPEED
            if 's' in keys_held: v[0] -= LIN_SPEED
            if 'a' in keys_held: v[1] += LIN_SPEED
            if 'd' in keys_held: v[1] -= LIN_SPEED
            if 'q' in keys_held: v[2] += LIN_SPEED
            if 'e' in keys_held: v[2] -= LIN_SPEED
            if keyboard.Key.up    in keys_held: w[0] += ANG_SPEED
            if keyboard.Key.down  in keys_held: w[0] -= ANG_SPEED
            if keyboard.Key.left  in keys_held: w[1] += ANG_SPEED
            if keyboard.Key.right in keys_held: w[1] -= ANG_SPEED

            # Desired EE motion this step
            dx = v * DT
            dr = w * DT

            # --- Jacobian at current state
            jacp = np.zeros((3, model.nv))
            jacr = np.zeros((3, model.nv))
            mujoco.mj_jacSite(model, data, jacp, jacr, ee_site_id)

            # Restrict to the 7 arm joints (slice columns)
            J_arm = np.vstack([jacp[:, :7], jacr[:, :7]])

            # --- DLS step
            dq = dls_step(jacp[:, :7], jacr[:, :7], dx, dr)
            data.qpos[:7] += dq

            # --- Step physics
            mujoco.mj_forward(model, data)
            viewer.sync()

            # --- Log condition number
            cond = np.linalg.cond(J_arm)
            cond_history.append(cond)
            t_history.append(t)

            time.sleep(max(0, DT - (time.time() - t_start - t)))

    listener.stop()

    # --- Save the condition-number plot
    cond = np.array(cond_history)
    t = np.array(t_history)
    fig, ax = plt.subplots(figsize=(10, 4))
    ax.plot(t, cond)
    ax.axhline(500, color='r', linestyle='--', label='singularity threshold')
    ax.set_xlabel("Time (s)")
    ax.set_ylabel("cond(J_arm)")
    ax.set_title("Jacobian condition number during teleop")
    ax.set_yscale('log')
    ax.legend()
    ax.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig("../figures/day3_jacobian_condition.png", dpi=120)
    print(f"\nWrote figures/day3_jacobian_condition.png")
    print(f"Max condition number reached: {cond.max():.1f}")
    print(f"Time spent above threshold (cond > 500): {((cond > 500).sum() * DT):.1f} s")

if __name__ == "__main__":
    main()

Run it:

cd ~/robo47/w1-foundations
python src/day3_dls_ik.py
  • What you should experience:
  • An interactive Panda window opens.
  • Press W: arm advances in +X. Press S: retreats in −X. A/D: left/right. Q/E: up/down.
  • Move the arm in a smooth circle in front of the Core ConceptsRobotA physical system with sensors and actuators that can observe the world and take actions. for ~10 seconds. The motion should feel responsive.
  • Now deliberately push the arm fully extended forward (hold W). At some point the arm will reach the edge of its Manipulation & TasksWorkspaceThe region of space the robot can reach. and the motion will become jittery — that's a singularity.
  • Press ESC to exit.

What success looks like in console output:

Teleop ready. WASD = X/Y, QE = Z, arrows = orientation, ESC = quit.
Wrote figures/day3_jacobian_condition.png
Max condition number reached: 1247.3
Time spent above threshold (cond > 500): 2.4 s

Expected figure: A line plot of cond(J) over ~30 s on a log y-axis. Most of the trace sits between 10 and 100 (well-conditioned). One or two visible spikes climb above the red cond=500 line — those are your singularity moments. If the plot has no spikes, you didn't push the arm to a singular pose; do another run and deliberately fully extend it.

Step 3 — Record a screen video (10 min)

Use any screen recorder (macOS Cmd-Shift-5; Linux simplescreenrecorder; Windows Game Bar). Capture a 30-second clip showing fluid Imitation & Reinforcement LearningTeleoperation (teleop)A human remotely controlling the robot, often to collect demonstrations. including one deliberate singularity push. Save to videos/day3_teleop.mp4. Trim to ≤30 s.

Step 4 — Common failures and fixes (10 min)

  • `pynput` doesn't capture keys — On macOS, you need to grant terminal accessibility permissions in System Settings → Privacy → Accessibility.
  • Arm wiggles wildlyLIN_SPEED is too high. Reduce to 0.05 and retry.
  • DLS doesn't help near singularity (still jittery) — Increase DAMPING to 0.1. With more damping, the EE responds more sluggishly but never explodes.
  • No condition-number spike at all — The Panda's home pose is well-conditioned. To force a singularity, hold W long enough that the arm fully extends, then press an orientation key. The fully-stretched arm has a wrist singularity.
  • `mj_jacSite` segfault — You used the wrong site name. Print [mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_SITE, i) for i in range(model.nsite)] to list all sites.

Step 5 — Log and commit (5 min)

echo "day3_dls_teleop,$(date -I),3,dls_ik_teleop,$(cd ~/robo47/w1-foundations && git rev-parse --short HEAD),0,Panda,dls_ik,1800,1,max_cond_J,1247,manual_teleop_30s" >> ~/robo47/metrics/metrics.csv

cd ~/robo47/w1-foundations
git add src/day3_dls_ik.py figures/day3_jacobian_condition.png videos/day3_teleop.mp4
git commit -m "Day 3: DLS IK teleop, observed singularity cond ~1200"

Deliverable checklist

Why this lab matters

Every modern Core ConceptsPolicyThe rule or model that maps observations or states to actions. (Modern Robot LearningDiffusion policyA robot policy that generates actions using diffusion-model techniques., ACT, π0, GR00T) eventually outputs an EE delta or Movement, Mechanics & Robot BodyVelocityHow fast something moves.. Internally or externally, something is solving Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. to convert that to Movement, Mechanics & Robot BodyJointA movable connection between robot parts. commands. When a learned Core ConceptsPolicyThe rule or model that maps observations or states to actions. "fails" at a particular pose, 30% of the time it's because that pose is near a singularity and the Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. is unstable. You now understand what that feels like in your fingers.

Side quest (optional, 30 min)

Add null-space damping: when cond(J) > 200, add a secondary objective q̇_null = -k · (q - q_home) projected onto the null-space (I - J⁺ J). The arm will gracefully bias toward its home pose when far from a Core ConceptsGoalThe desired outcome or target state for a robot task. — a trick used by every modern Manipulation & TasksManipulationUsing a robot arm or hand to move or interact with objects. Control & PlanningControllerThe algorithm or system that turns desired behavior into motor commands..

---

Deliverable checklist

Optional: Submit your repo