Chapter 4 · Classical action models: planning and inverse dynamics

§4.x Hands-on exercise + chapter references

0:00/15:40
AI-narrated by Kokoro

Chapter 4 was the classical-lineage chapter; the exercise is the one where you build a small version of the three-layer cake on your laptop and stack one block on top of another with no neural network anywhere in the loop. The four drills below take a combined three hours on a laptop CPU. The point is to leave Chapter 4 with a working symbolic-to-geometric-to-dynamic pipeline on disk, so that when Chapters 11 through 14 start replacing the top and middle of the cake with learned components, you have something to replace into.

Exercise 4.x.1 — A five-block PDDL domain and a planner call

Create a directory drills_ch4/ with two files: blocksworld.pddl (the domain) and problem_5.pddl (the instance). The domain encodes the standard Blocksworld formulation from §4.1 — predicates (on ?x ?y), (on-table ?x), (clear ?x), (holding ?x), (arm-empty) — and four action schemas pick-up, put-down, stack, unstack. Write the domain from memory and check it against §4.1 afterwards; if you cannot reproduce the stack schema’s add and delete lists without peeking, you have not yet internalized what an action schema is.

The problem file describes five blocks a through e arranged as two towers — (a on b), (b on c), (c on-table), (d on e), (e on-table), plus the matching clear and arm-empty predicates — and a goal that stacks all five into a single column with a on top and e on the bottom: (on a b) (on b c) (on c d) (on d e) (on-table e). Fast Downward should return one of several plans of length 12.

Install Fast Downward (Helmert, 2006) — from source, via the dockerized variant, or via the pyperplan wrapper for a simpler alternative — and run it on the two files. Save the returned plan to plan.txt. The deliverable is the plan plus a one-line note recording which heuristic the planner used (seq-opt-lmcut and lama-first are the two defaults worth knowing). Runtime should be well under a second. If the planner reports unsolvable, your goal or initial state is inconsistent — most often a missing clear predicate — and §4.1 on the closed-world assumption is the place to re-read.

Wall clock: about thirty minutes including the Fast Downward install.

Exercise 4.x.2 — Wire the plan to a PyBullet pick-and-place executor

Copy the plan from Exercise 4.x.1 into a new Python file execute_plan.py. Its job is to take the plan and produce joint-space motion on a simulated Franka Panda in PyBullet. The architecture is the §4.5 three-layer cake in code form:

  1. Symbolic layer (already done): the plan from 4.x.1, read in as a list of (action_name, arguments) tuples.
  2. Geometric layer (this exercise): a function solve_action(action, world_state) that, for each symbolic action, computes a sequence of Cartesian waypoints (pre-grasp pose, grasp pose, lift pose, transport pose, place pose, retract pose), passes each waypoint through TRAC-IK to obtain joint targets, and concatenates the joint targets into a trajectory. Use the TRAC-IK Python bindings (Beeson and Ames, 2015) if installed; otherwise fall back to the damped-least-squares IK from §4.2 with a damping factor of 0.05 and a maximum of 200 iterations. RRT-Connect (Kuffner and LaValle, 2000) is overkill for tabletop blocksworld — straight-line motion in joint space between IK solutions is sufficient — but feel free to drop OMPL in if you have it.
  3. Dynamic layer (PyBullet provides it): set joint targets with setJointMotorControlArray in position-control mode. PyBullet’s internal PD-plus-gravity controller is doing the §4.3 work for you; the point of this exercise is to not implement computed-torque from scratch and to notice that the simulator is silently providing the bottom layer of the cake.

The world is a flat table at z=0, five colored 4 cm cubes whose initial positions match problem_5.pddl, and a Franka mounted at the table edge. PyBullet ships the URDF at pybullet_data/franka_panda/panda.urdf. A skeleton for the execution loop, illustrative not exhaustive:

plan = read_plan("plan.txt")
world = init_pybullet_world(blocks=["a", "b", "c", "d", "e"])
arm = load_panda(world)
ik = TracIKSolver(arm.urdf, "panda_link0", "panda_hand")

for action_name, args in plan:
    waypoints = waypoints_for(action_name, args, world)
    for pose in waypoints:
        q_target = ik.solve(pose, q_seed=arm.current_q())
        if q_target is None:
            raise RuntimeError(f"IK failed for {action_name} at {pose}")
        arm.move_to(q_target, duration=1.0)
    world.apply_symbolic_effects(action_name, args)

Run the script and watch the arm execute the twelve-action plan in about thirty seconds of wall clock simulation time. Save a screenshot of the final state — a single tower with a on top — to final_state.png. Save the joint trajectory to trajectory.npz; you will reuse it in Exercise 4.x.3.

The most common failure is that IK returns None for the pre-grasp pose of c because the dexterous workspace of the Panda does not quite reach a block at table height directly below the base; the §4.2 fix is to lower the table by 5 cm or mount the arm 10 cm higher. If the IK succeeds but the gripper passes through an adjacent block on approach, you have rediscovered the problem PDDLStream (Garrett, Lozano-Pérez, and Kaelbling, 2020) was written to solve — the symbolic planner does not know about geometric infeasibility, and a true TAMP system would interleave the two searches. We do not implement that here; leaving the bug visible is the lesson.

Wall clock: about ninety minutes including TRAC-IK installation.

Exercise 4.x.3 — Replace the bottom layer with explicit computed-torque

Copy execute_plan.py to execute_plan_torque.py and change one thing: instead of using PyBullet’s position controller, drive the arm in pure torque mode (setJointMotorControlArray(... , controlMode=p.TORQUE_CONTROL, forces=tau)) and compute tau explicitly from the §4.3 computed-torque law

τ=M(q)(q¨d+Kd(q˙dq˙)+Kp(qdq))+C(q,q˙)q˙+g(q).\begin{aligned} \tau &= M(q)(\ddot{q}_d + K_d (\dot{q}_d - \dot{q}) + K_p (q_d - q)) \\ &\quad + C(q,\dot{q})\dot{q} + g(q). \end{aligned}

PyBullet exposes the inertia matrix and the bias terms through calculateMassMatrix and calculateInverseDynamics; you do not have to derive the Panda’s dynamics from URDF by hand. Choose K_p = 100 and K_d = 20 on every joint as a starting point. The reference trajectory q_d(t) is the trajectory you saved in 4.x.2; numerically differentiate it once for q̇_d and twice for q̈_d with a simple central difference, or fit a cubic spline first if the differentiated signal is too noisy.

The arm executes the plan again, with modest overshoot at each waypoint that decays in under half a second. Plot q(t), q_d(t), and tau(t) for joint 4 (the elbow, which carries the largest gravity load) and save as joint4_torque.png. The plot should make three things visible: tau(t) is not zero when the arm is stationary (the static gravity term), peaks in tau(t) line up with the lift-and-transport phases (dynamic torques scale with acceleration), and the tracking error grows when K_d is reduced — try K_d = 5 and watch the arm wobble. That wobble is what §4.3 cited as the motivation for matching damping coefficients to the inertia matrix, and the residual-learning pattern from §4.4 is what a research group reaches for when the wobble persists even with well-tuned gains.

This is the exercise where the three-layer cake stops being a metaphor. You have written the geometric and dynamic layers explicitly; the symbolic layer is the text file from 4.x.1; the learned layer is the empty slot the rest of the book will fill.

Wall clock: about forty-five minutes.

Exercise 4.x.4 — A PDDL diff against a SayCan transcript

Open the SayCan paper (Ahn et al., 2022, arXiv:2204.01691) to one of the appendices that lists a full kitchen-task transcript — the “bring me a snack” examples typically run several pages. Pick one transcript and, on a piece of paper or in a text file, transcribe the language-model output into PDDL. Each numbered step the model produces is one ground action; figure out which predicates would have to hold in the precondition and which would be added or deleted.

You will discover three things. SayCan plans are short — four to eight steps, well within Fast Downward’s reach. The predicate set is much larger than blocksworld’s (in-hand, at-location, is-open, contains) because a real kitchen has more state than five blocks. And several SayCan steps will be hard to express as STRIPS schemas at all without admitting that the language model is doing implicit hierarchical decomposition that the symbolic formalism would express with HTN-style methods, not pure STRIPS.

The deliverable is the transcribed PDDL plus a half-page note: which steps mapped cleanly, which did not, and what would have been needed to make them clean. The §4.5 claim — “the interface of a symbolic action model survives, only the engine has moved” — is true in a limited sense, and you should have first-hand experience with where the limit is. Chapter 14 returns to this when it dissects the high-level system of a dual-system architecture.

Wall clock: about thirty minutes.

Chapter 4 reading list

The works below are the ones cited in §4.1–§4.5. They are grouped by purpose. Full bibliographic entries for everything cited in the whole book live in Appendix E.2; this list is the chapter-local subset.

Symbolic planning (§4.1)

Geometric actions: IK and motion planning (§4.2)

Inverse dynamics and control (§4.3)

Where classical methods still live in modern stacks (§4.4)

Chapter summary

Chapter 4 was the classical-action chapter, and it sits at the hinge between Part 1 (the foundations) and Part 2 (the lineage that produced VLAs). You can now write a small PDDL domain and run Fast Downward on it; you can stand up a tabletop IK-plus-motion-planning layer that takes a symbolic plan and turns it into joint-space motion on a simulated Franka; you can write the manipulator equation and the computed-torque law without looking them up, and explain which terms PyBullet, the manufacturer’s onboard firmware, and the application code are each responsible for; and you can read any modern VLA paper and draw its three-layer cake, labeling each layer classical or learned. Chapter 5 begins the next chapter of Part 2 — reinforcement learning, the family that fills the training-signal slot the classical models leave empty — and the controllers from §4.3 will return there as the policies that an MDP optimizes over.

This section has been read times.

References

  1. Fikes & Nilsson (1971). STRIPS — A New Approach to the Application of Theorem Proving to Problem Solving. Artificial Intelligence 2(3–4).
  2. McDermott et al. (1998). PDDL — The Planning Domain Definition Language. AIPS-98.
  3. Helmert (2006). The Fast Downward Planning System. JAIR 26.
  4. Garrett, Lozano-Pérez, Kaelbling (2020). PDDLStream. ICAPS.
  5. Kuffner & LaValle (2000). RRT-Connect. ICRA.
  6. Beeson & Ames (2015). TRAC-IK — An Improved Inverse Kinematics Solver. Humanoids.
  7. Ahn et al. (2022). Do As I Can, Not As I Say (SayCan). arXiv:2204.01691.
  8. Liang et al. (2022). Code as Policies. arXiv:2209.07753.
  9. Kim et al. (2024). OpenVLA. arXiv:2406.09246.
  10. Black et al. (2024). π0. arXiv:2410.24164.