In the previous labs we learned how to compute the position of the foot from the motor angles (forward kinematics) and how to go the other way around, from a desired foot position to the motor angles (inverse kinematics), and we made a single leg follow a triangular trajectory. In this lab we bring everything together: we will make the whole Pupper walk forward by implementing a trotting gait.
A gait is nothing more than a coordinated, periodic trajectory played on every leg with the right timing between them. We will build the reference trajectories for all four legs, interpolate between them in time, solve the inverse kinematics for each leg, and command the motors so that the robot trots in place and then forward.
Stanford Pupper trotting - demo video
There is nothing new to clone for this lab. We build directly on top of the code you already wrote in the previous lab, so keep working in your existing lab 6 project:
cd ~/lab_3_fall_2025 code .
You already have a working single-leg pipeline: forward kinematics, the gradient-descent inverse kinematics, and the triangular trajectory for one leg. In this lab you will extend that same code to drive all four legs and to produce a walking trot.
Docker users can keep using the same simulation image as before (no need to redownload or rebuild it) and run:
./run.sh 3
Open your solution and re-read the InverseKinematics class. Make sure the following, written in the previous labs, still work:
rotation_x, rotation_y, rotation_z and translation helpers (lab 5 forward kinematics);fr_leg_fk(theta) is your reference implementation;Everything below extends this existing code — you are not starting from a blank file.
So far your code only worked with one leg at a time. To walk, we need to extend it to all four legs, each attached to the body at a different position and orientation.
TODO 1: Starting from your working front-right implementation fr_leg_fk, add forward kinematics for the three remaining legs, using fr_leg_fk and your lab 5 diagrams as a reference:
fl_leg_fk(theta) — front-leftbr_leg_fk(theta) — back-rightbl_leg_fk(theta) — back-leftEach leg is mounted at a different body offset, so pay attention to the sign of the coordinates and to the orientation of each hip relative to the body frame:
(0.06, -0.09, 0)(0.06, 0.09, 0)(-0.11, -0.09, 0)(-0.11, 0.09, 0)Answer the following in your report:
fr_leg_fk with your single-leg forward kinematics from lab 5. What is different, and why were those modifications necessary once we care about the leg's position on the body?A trot is a symmetric gait in which the two diagonal leg pairs move together: the front-right and back-left legs swing forward while the front-left and back-right legs stay on the ground (stance), then the roles swap. This keeps the robot's support polygon balanced around the center of mass and is one of the most stable and energy-efficient gaits for a quadruped.
Each leg's foot (end-effector) follows a closed loop in space made of two phases:
We describe this loop with a small set of reference end-effector positions per leg. Instead of the three vertices you used in lab 6, we now use six reference positions per leg so we can shape both the stance (feet on the ground) and the swing (lifting and reaching forward):
touch_down (0.05, 0.0, -0.14) — foot lands in frontstand positions on the ground, moving backwardliftoff (-0.05, 0.0, -0.14) — foot leaves the ground at the backmid_swing (0.0, 0.0, -0.05) — foot at the top of the swing arcThe trot is obtained by playing this same per-leg loop with a phase offset of half a cycle between the two diagonal pairs: when FR+BL are at the start of their swing, FL+BR are halfway through, i.e. in the middle of their stance.
TODO 2: Fill in the reference end-effector positions for each leg. These arrays define the triangle/loop that each foot will track:
rf_ee_triangle_positionslf_ee_triangle_positionsrb_ee_triangle_positionslb_ee_triangle_positionsUse the six reference positions described in Part 3. Remember that the diagonal pairs (RF/LB and LF/RB) must be offset by half a period so the robot trots instead of hopping.
Answer the following in your report:
In lab 6 you interpolated a simple 3-vertex triangle for a single leg. That is not enough for a trot: we now need a richer loop with distinct stance and swing phases, played on all four legs with the correct diagonal timing.
TODO 3: Write a new function — do not overwrite your lab 6 triangle — that produces this alternative triangle/loop pattern. Give it a signature such as interpolate_triangle(t, leg_index): given a normalized time t and a leg index, it returns the interpolated end-effector target for that leg.
t.np.interp — write your own weighted-sum interpolation so you understand exactly what happens between the reference points.
The main loop then calls, at each control step: your new interpolation function → inverse_kinematics_single_leg (per leg) → send the resulting joint angles to the PD controller.
cache_target_joint_positions method precomputes the joint angles for a full gait cycle (the interpolation runs over t from 0 to 1 in steps of 0.02, i.e. 50 samples). Solving the IK for every leg at every control tick in real time would be too expensive on the Raspberry Pi 5, so we solve it once per cycle and replay the cached angles. Keep this in mind when you reason about performance later.
~/lab_3_fall_2025, launch the system:ros2 launch lab_3.launch.py
Answer the following in your report:
ik_timer_period and pd_timer_period and observe the trade-off between smoothness and CPU load.ee_positions offsets to shift the center of mass and see how it affects stability.Answer the following in your report:
Now optimize for speed. You can tune the timer frequencies, the stride length, and the end-effector positions defined in the InverseKinematics class after initialization.