Day 7
Trajectory generation + Week 1 integration + fresh-clone test
LECTURE & READING
Glossary primer (10 min)
- Core ConceptsTrajectoryA sequence of states or actions over time. — A time-parameterized configuration:
q(t), q̇(t), q̈(t)satisfying physical and kinematic constraints. - Path — The geometric route, time-independent.
- Trapezoidal Movement, Mechanics & Robot BodyVelocityHow fast something moves. profile — A Core ConceptsTrajectoryA sequence of states or actions over time. that ramps up to constant Movement, Mechanics & Robot BodyVelocityHow fast something moves., cruises, then ramps down. Has discontinuous Movement, Mechanics & Robot BodyAccelerationHow quickly velocity changes. (infinite jerk).
- S-curve / quintic profile — Smoother — bounded jerk. Standard for industrial arms and what Ruckig generates online.
- Ruckig — An online jerk-limited Core ConceptsTrajectoryA sequence of states or actions over time. generator written in C++. MoveIt 2's default time-parameterizer since 2023.
- Time-parameterization — Given a path and motion limits (vmax, amax, jmax), produce a time-stamped Core ConceptsTrajectoryA sequence of states or actions over time..
Hour 1 — Trajectories
- Ruckig docs ~30 min: https://docs.ruckig.com/
- Modern Robotics Ch. 9 exact sequence:
- 9.1 and 9.2 Point-to-Point Trajectories, Part 1
- 9.1 and 9.2 Point-to-Point Trajectories, Part 2
- 9.3 Polynomial Via Point Trajectories
- 9.4 Time-Optimal Time Scaling, Part 1
- 9.4 Time-Optimal Time Scaling, Part 2
- 9.4 Time-Optimal Time Scaling, Part 3
Hour 2 — Choose one
- Peter Corke RVC3 Ch. 7 (Time-Varying Coordinate Frames) skim: https://petercorke.com/rvc3-landing/
- or Russ Tedrake "Control & PlanningTrajectory optimizationFinding the best motion path while obeying constraints. I" intro lecture
Video
or Russ Tedrake "Control & PlanningTrajectory optimizationFinding the best motion path while obeying constraints. I" intro lecture
Open source
LAB
Hour 3 — Lab: integrate Week 1 into a pick-and-place pipeline (90 min)
What you're building. A single-script pipeline that combines everything from Days 1–6: (1) plan a smooth jerk-limited Cartesian Core ConceptsTrajectoryA sequence of states or actions over time. between two EE poses using Ruckig, (2) at every timestep, solve DLS Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. to get Movement, Mechanics & Robot BodyJointA movable connection between robot parts. targets, (3) execute via the impedance Control & PlanningControllerThe algorithm or system that turns desired behavior into motor commands. with gravity compensation, (4) wrap it in a make reproduce Makefile. Then run a fresh-clone test: clone the Week 1 repo to /tmp/test, run make reproduce, verify Day 5's settling time matches yours within 1%.
What success looks like at the end. You have:
1. src/day7_pick_and_place.py running the full pipeline.
2. A 30-second video videos/day7_pickplace.mp4 showing the Panda smoothly approaching a virtual pick pose, "Manipulation & TasksGraspingTaking hold of an object.," lifting, moving, "releasing".
3. A Makefile with a reproduce target.
4. Successful fresh-clone test: RETRO.md notes the reproduced settling time matches within 1%.
Step 1 — Install Ruckig (5 min)
cd ~/robo47
source .venv/bin/activate
uv pip install ruckig
python -c "import ruckig; print(ruckig.__version__)"Expected: 0.14.0 or later.
Step 2 — Write the pipeline (50 min)
Create src/day7_pick_and_place.py:
"""Day 7: integrated pick-and-place pipeline.
- Ruckig joint-space trajectory between two configs.
- DLS IK from Day 3 to convert task-space waypoints.
- Day 6 impedance + Day 4 grav comp for execution.
"""
from __future__ import annotations
import numpy as np
import matplotlib.pyplot as plt
import mujoco
import mujoco.viewer
import time
from ruckig import InputParameter, OutputParameter, Result, Ruckig
MODEL_PATH = "../mujoco_menagerie/franka_emika_panda/scene.xml"
ARM_DOF = 7
Q_HOME = np.array([0, -0.785, 0, -2.356, 0, 1.571, 0.785])
# --- Two task-space waypoints (in world frame) we'll move between
PICK_POS = np.array([0.40, 0.20, 0.40])
PLACE_POS = np.array([0.40, -0.20, 0.40])
# Conservative limits for Ruckig (rad, rad/s, rad/s^2, rad/s^3)
QD_MAX = np.array([2.0] * ARM_DOF)
QDD_MAX = np.array([3.0] * ARM_DOF)
QDDD_MAX = np.array([10.0] * ARM_DOF)
KP_LIN = 800
KD_LIN = 2 * np.sqrt(KP_LIN)
KQ_DAMP = np.array([5, 5, 5, 5, 2, 2, 2], dtype=float)
def dls_ik(model, data, target_pos, ee_site_id, n_iter=50, lam=0.05, tol=1e-4):
"""Solve IK for desired EE position. Returns the converged joint config."""
q = data.qpos[:ARM_DOF].copy()
for _ in range(n_iter):
data.qpos[:ARM_DOF] = q
mujoco.mj_forward(model, data)
x_now = data.site_xpos[ee_site_id].copy()
err = target_pos - x_now
if np.linalg.norm(err) < tol:
break
jacp = np.zeros((3, model.nv))
jacr = np.zeros((3, model.nv))
mujoco.mj_jacSite(model, data, jacp, jacr, ee_site_id)
J = jacp[:, :ARM_DOF]
dq = J.T @ np.linalg.solve(J @ J.T + lam**2 * np.eye(3), err)
q = q + dq
return q
def ruckig_trajectory(q_start, q_goal, dt):
"""Generate a jerk-limited joint-space trajectory from q_start to q_goal.
Returns three N×ARM_DOF arrays: qpos, qvel, qacc.
"""
rk = Ruckig(ARM_DOF, dt)
inp = InputParameter(ARM_DOF)
out = OutputParameter(ARM_DOF)
inp.current_position = q_start.tolist()
inp.current_velocity = [0.0] * ARM_DOF
inp.current_acceleration = [0.0] * ARM_DOF
inp.target_position = q_goal.tolist()
inp.target_velocity = [0.0] * ARM_DOF
inp.target_acceleration = [0.0] * ARM_DOF
inp.max_velocity = QD_MAX.tolist()
inp.max_acceleration = QDD_MAX.tolist()
inp.max_jerk = QDDD_MAX.tolist()
qs, qvs, qas = [], [], []
while True:
res = rk.update(inp, out)
qs.append(np.array(out.new_position))
qvs.append(np.array(out.new_velocity))
qas.append(np.array(out.new_acceleration))
if res == Result.Finished:
break
out.pass_to_input(inp)
return np.array(qs), np.array(qvs), np.array(qas)
def execute_trajectory(model, data, q_traj, qd_traj):
"""Track the trajectory using PD + gravity compensation in joint space."""
KP = np.array([200, 200, 150, 150, 80, 80, 40], dtype=float)
KD = np.array([20, 20, 15, 15, 8, 8, 4], dtype=float)
history = np.zeros((len(q_traj), 3)) # EE positions
ee_site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "attachment_site")
for k in range(len(q_traj)):
q_des = q_traj[k]
qd_des = qd_traj[k]
q = data.qpos[:ARM_DOF]
qd = data.qvel[:ARM_DOF]
tau_pd = KP * (q_des - q) + KD * (qd_des - qd)
tau_gc = data.qfrc_bias[:ARM_DOF]
data.qfrc_applied[:] = 0
data.qfrc_applied[:ARM_DOF] = tau_pd + tau_gc
mujoco.mj_step(model, data)
history[k] = data.site_xpos[ee_site_id]
return history
def main(visualize: bool = False):
model = mujoco.MjModel.from_xml_path(MODEL_PATH)
data = mujoco.MjData(model)
data.qpos[:ARM_DOF] = Q_HOME.copy()
mujoco.mj_forward(model, data)
ee_site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "attachment_site")
# 1. Solve IK for pick and place poses
q_pick = dls_ik(model, data, PICK_POS, ee_site_id)
print(f"q_pick: {np.round(q_pick, 3)}")
data.qpos[:ARM_DOF] = q_pick
mujoco.mj_forward(model, data)
print(f" resulting EE pos: {np.round(data.site_xpos[ee_site_id], 3)}, target {PICK_POS}")
q_place = dls_ik(model, data, PLACE_POS, ee_site_id)
print(f"q_place: {np.round(q_place, 3)}")
data.qpos[:ARM_DOF] = q_place
mujoco.mj_forward(model, data)
print(f" resulting EE pos: {np.round(data.site_xpos[ee_site_id], 3)}, target {PLACE_POS}")
# Reset to home
data.qpos[:ARM_DOF] = Q_HOME.copy()
data.qvel[:] = 0
mujoco.mj_forward(model, data)
dt = model.opt.timestep
# 2. Plan home -> pick
print("\nPlanning home -> pick ...")
q1, qd1, _ = ruckig_trajectory(Q_HOME, q_pick, dt)
print(f" trajectory length: {len(q1)} steps ({len(q1)*dt:.2f} s)")
# 3. Plan pick -> place (no real grasp, just move)
print("Planning pick -> place ...")
q2, qd2, _ = ruckig_trajectory(q_pick, q_place, dt)
print(f" trajectory length: {len(q2)} steps ({len(q2)*dt:.2f} s)")
# 4. Execute both
h1 = execute_trajectory(model, data, q1, qd1)
h2 = execute_trajectory(model, data, q2, qd2)
# 5. Plot the resulting EE path
full = np.vstack([h1, h2])
fig, ax = plt.subplots(figsize=(8, 6))
ax.plot(full[:, 0], full[:, 1], 'b-', label='EE path')
ax.scatter(*PICK_POS[:2], color='g', s=80, label='pick', zorder=5)
ax.scatter(*PLACE_POS[:2], color='r', s=80, label='place', zorder=5)
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.set_title("Pick-and-place EE path")
ax.set_aspect('equal')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("../figures/day7_pickplace_path.png", dpi=120)
print("Wrote figures/day7_pickplace_path.png")
# 6. Optional: visualize live
if visualize:
data.qpos[:ARM_DOF] = Q_HOME.copy()
mujoco.mj_forward(model, data)
with mujoco.viewer.launch_passive(model, data) as v:
for q_traj, qd_traj in [(q1, qd1), (q2, qd2)]:
for k in range(len(q_traj)):
q_des = q_traj[k]
qd_des = qd_traj[k]
q = data.qpos[:ARM_DOF]
qd = data.qvel[:ARM_DOF]
KP = np.array([200, 200, 150, 150, 80, 80, 40], dtype=float)
KD = np.array([20, 20, 15, 15, 8, 8, 4], dtype=float)
tau = KP * (q_des - q) + KD * (qd_des - qd) + data.qfrc_bias[:ARM_DOF]
data.qfrc_applied[:] = 0
data.qfrc_applied[:ARM_DOF] = tau
mujoco.mj_step(model, data)
v.sync()
time.sleep(dt)
if __name__ == "__main__":
import sys
main(visualize=("--viz" in sys.argv))Test without viewer first:
cd ~/robo47/w1-foundations
python src/day7_pick_and_place.pyExpected output:
q_pick: [ 0.452 -0.193 -0.025 -2.05 -0.012 1.86 0.785]
resulting EE pos: [0.4 0.2 0.4], target [0.4 0.2 0.4]
q_place: [-0.452 -0.193 0.025 -2.05 0.012 1.86 0.785]
resulting EE pos: [ 0.4 -0.2 0.4], target [ 0.4 -0.2 0.4]
Planning home -> pick ...
trajectory length: 612 steps (1.22 s)
Planning pick -> place ...
trajectory length: 478 steps (0.96 s)
Wrote figures/day7_pickplace_path.pngThen with viewer:
python src/day7_pick_and_place.py --vizScreen-record 30 s. Save to videos/day7_pickplace.mp4.
Step 3 — Write the Makefile (10 min)
Create w1-foundations/Makefile:
.PHONY: install reproduce clean
install:
uv venv --python 3.12 .venv
. .venv/bin/activate && uv pip install -r requirements.txt
reproduce:
. ../.venv/bin/activate && \
python src/day1_transforms.py && \
python src/day2_fk.py && \
python src/day4_grav_comp.py && \
python src/day5_cartpole_lqr.py && \
python src/day7_pick_and_place.py
clean:
rm -rf figures/*.png videos/*.mp4 __pycache__ src/__pycache__And requirements.txt:
mujoco==3.7.*
numpy
scipy
matplotlib
ruckig
pynputStep 4 — Fresh-clone test (15 min)
# Push your work
cd ~/robo47/w1-foundations
git add -A && git commit -m "Day 7: integrated pick-place pipeline + Makefile"
git push origin main # adjust branch name if needed
# Now do the fresh-clone test
cd /tmp
rm -rf w1-test
git clone <your-github-url-for-w1-foundations> w1-test
cd w1-test
cp -r ~/robo47/mujoco_menagerie . # menagerie not in repo (gitignored)
# Make a fresh venv
uv venv --python 3.12 .venv
source .venv/bin/activate
uv pip install -r requirements.txt
# Run the headless pipeline
python src/day5_cartpole_lqr.py 2>&1 | tee fresh_run.logCompare the "Mean settling time" line to your earlier value. Expected: within ±2%.
Write RETRO.md:
# Week 1 retro
## Reproducibility
- Original mean settling time: 1.84 s
- Fresh-clone mean settling time: 1.83 s
- Delta: 0.5%, within ±1% target.
## What I learned
1. Frame ordering: `T_a @ T_b` and `T_b @ T_a` are different; the difference IS the geometry.
2. PoE is cleaner than DH but the screw-axis sign convention is still where bugs hide.
3. Singular Jacobians don't usually crash code — they make controllers oscillate. Watch the condition number.
4. Gravity compensation is worth ~18× tracking improvement. It's not optional on a 7-DoF arm.
5. LQR linearization happens about an equilibrium; the controller works in *delta-state*, not absolute state.
## What still confuses me
- Why does Ruckig need `max_jerk`? In what units, and how was 10 rad/s³ chosen?Step 5 — Log and commit (5 min)
cd ~/robo47/w1-foundations
git add Makefile requirements.txt RETRO.md src/day7_pick_and_place.py figures/day7_pickplace_path.png videos/day7_pickplace.mp4
git commit -m "Day 7: pick-place pipeline + Makefile + Week 1 retro"
git pushDeliverable checklist
Why this lab matters
Today's pipeline is the Evaluation & ResearchBaselineA reference method used for comparison. against which every learned Core ConceptsPolicyThe rule or model that maps observations or states to actions. in Weeks 3–7 will be compared. The classical stack — Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. + Ruckig + impedance + gravity comp — is a strong opponent: highly tuned, reliable, predictable. When OpenVLA does Manipulation & TasksPick-and-placePicking up an object from one location and placing it somewhere else. at 90% Simulation & Sim-to-RealSuccess rateHow often the robot completes a task correctly., it's beating this at things this can't do (semantic Modern Robot LearningGeneralizationThe robot’s ability to work in new situations it has not seen before. to new objects), not at the Core ConceptsTrajectoryA sequence of states or actions over time. itself.
Side quest (optional, 60 min)
Replace your DLS Movement, Mechanics & Robot BodyInverse kinematics (IK)Calculating the joint values needed to reach a desired pose. with Drake's InverseKinematics solver (uv pip install drake). It uses SNOPT/IPOPT and handles Movement, Mechanics & Robot BodyJointA movable connection between robot parts. limits and Control & PlanningCollision avoidancePreventing the robot from hitting obstacles or itself. natively. Compare Simulation & Sim-to-RealSuccess rateHow often the robot completes a task correctly. and runtime against your DLS for 100 random target poses.
---
[End of Week 1. Document continues with Week 2 — Perception & SensingPerceptionThe process of turning raw sensor data into useful understanding of the world., ROS 2 Jazzy, URDF, MoveIt 2, Nav2 — in the next chunk.]