Code Walkthrough
The cmg planner one layer at a time: commands to run, the output they printed, why, and exercises with solutions.
cmg is the reference implementation of Planning Through Contact: a contact-formation search for the box-over-a-step task, built on JAX, Equinox, MuJoCo-MJX and MPAX. This walkthrough runs it layer by layer, from the scene file up to the search loop. Every page gives a command, the script (folded), the output it printed, what to notice in that output, where to read the code, and a few exercises with folded solutions.
It studies the code as it is, including its known problems; several pages end on a defect that the next stage of the project fixes. The last page, Where step 1 starts, collects them.
Code version. Angelo’s branch phase1/dev at commit 7d2d343 (2026-11-04). phase1/aykut matches it except for the search save/load added in the last five commits, which only step 8 uses.
Setup
Run everything in bash (type bash first if your shell is fish).
cd ~/repos/research/c-gnn # your clone of Symplectomorphism/c-gnn
git fetch origin
git switch --detach origin/phase1/dev # 7d2d343, the commit used here
cd cmg
uv venv --python 3.12 .venv
source .venv/bin/activate
uv pip install -e ".[dev]" matplotlibThe install takes a minute or two. Then fetch the walkthrough scripts into cmg/walkthrough/ and keep that folder out of git:
mkdir -p walkthrough && cd walkthrough
base=https://symplectomorphism.github.io/research/nonprehensile/code-walkthrough/scripts
for f in _common.py 01_scene.py 02_templates.py 03_formations.py 04_graph.py \
05_hold.py 06_candidates.py 07_mppi.py 08_search.py replay.py; do
curl -fsSLO "$base/$f"
done
cd ..
echo "walkthrough/" >> ../.git/info/excludeEvery command on the following pages runs from cmg/ with the virtual environment active. The scripts are also linked on each page.
Baseline: the test suite.
PYTHONPATH=tests python -m pytest -q tests...... [100%]
6 passed, 2 warnings in 97.70s (0:01:37)
PYTHONPATH=tests is needed because two tests import a helper module from tests/. Only four of the nine test files contain tests that pytest collects. test_relocate, test_markgraph_structure, test_gap_greedy_misses and test_horiz_support are placeholders. test_local_solver is a hand-run script (everything is under if __name__ == "__main__"), and it reads a gradient_warm_start attribute that MPPISolver no longer has. The suite writes plots and a video into tests/result/ (ignored by git); steps 3 and 5 show them.
Lines you can ignore. Every script first prints An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu. and two Failed to import warp lines. JAX runs on the CPU here, which is what the timings below assume. MuJoCo may also create an untracked MUJOCO_LOG.TXT in cmg/. The outputs on these pages omit those lines.
The steps
| Step | What it shows | Script | Time |
|---|---|---|---|
| 1. The scene | step.yaml and step.xml: bodies, features, the 24 templates, tolerances |
01_scene.py |
2 s |
| 2. Templates and gaps | Every template’s signed gap at one pose, and the band test | 02_templates.py |
3 s |
| 3. Formations | The band formation at seven named poses and along a 1,721-frame demo | 03_formations.py |
7 s |
| 4. The contact graph | Transfer-adjacent and shared-feature edges, and what is lost | 04_graph.py |
1 s |
| 5. The hold LP | Supported and EnvStable: the friction-cone LP behind the hold test | 05_hold.py |
23 s |
| 6. Candidate edits | Which makes and breaks are legal at a node, and why | 06_candidates.py |
26 s |
| 7. One solver call | MPPI realizing one make, and how its result is judged | 07_mppi.py |
2 min |
| 8. The search loop | Two batches of breadth-first search, saving and loading the graph | 08_search.py |
2 min |
| 9. Where step 1 starts | What the walkthrough exposed, mapped to the planar rewrite | none | none |
Times are wall-clock times on a 24-core CPU, including JAX compilation. _common.py holds shared helpers: the scene path, the named poses, and readable template names such as M:boundary-B:xn (body:feature on each side). replay.py loops a trajectory in MuJoCo’s viewer if you have a display.
What exists in the code
About 2,900 lines are implemented; the other 45 modules are stubs that hold only a docstring describing their intended role.
| Layer | Implemented | Stub only |
|---|---|---|
| Scene | scene/spec.py (schema and YAML loader) |
scene/loader.py |
| Geometry | geometry/primitives.py (3D gap kernels), geometry/features.py (faces, vertices, adjacency) |
none |
| Catalog | catalog/templates.py (templates, Catalog.gap, band), catalog/graph.py (partial) |
graph versioning |
| Marking | marking/candidate.py (Cand: legal edits) |
bitset.py, near.py, operators.py, statics.py |
| Physics | physics/world.py (MJX world), physics/hold.py (support LP) |
mjx_backend.py |
| Solver | solver/mppi.py, solver/gradient.py |
cem.py, success.py, cost/compiler.py |
| Search | search/__init__.py (Search, SearchState, Discretizer, BFS ranker) |
engine.py, keys.py, node.py, all four policies |
| Learning | none | features/, model/, diffusion/, training/, calibration/ |
| Orchestration | none | orchestration/ |
The roles of the stubbed marking modules are covered in practice by Cand (step 6) and physics/hold.py (step 5). The learning and orchestration layers are frozen by Decision 5, so this walkthrough does not visit them.