Grocery Fulfillment#
Note
Requirements
A CUDA GPU (MuJoCo EGL rendering + CuRobo planning + perception weights), the
grocery dependency extra, and an LLM credential for graph generation. The
configs pin Vertex with gemini-3.1-pro-preview; the default openrouter
provider works by switching the config’s llm: block.
This is GaP’s flagship acceptance family — and its release gate. The task:
pick a described grocery item (“the alphabet soup can with a blue and yellow
label”) and place it in the basket, under the benchmark’s baked pose,
permutation, and basket-swap variations. Every graph is generated per task
by gap.agent.generate (the llm_generation benchmark mode); nothing is
hand-written. The full config doubles as the release gate: --gate makes
gap benchmark exit non-zero when a cell falls below the config’s
gate_threshold.
Each instance re-samples the object arrangement and basket placement — the generated graph must re-perceive and adapt every episode.#
The example lives at examples/grocery_fulfillment; the configs it runs live in examples/benchmark (see Benchmark Grids).
Run it#
CUDA_HOME=/usr/local/cuda uv sync --extra grocery # quickstart set + CuRobo planning
uv run gap skills check --download
# the 20-trial smoke (tasks 0-1)
MUJOCO_GL=egl uv run gap benchmark examples/benchmark/grocery_acceptance_smoke.yaml --gate
# the full acceptance gate (interruptible; --resume continues)
MUJOCO_GL=egl uv run gap benchmark examples/benchmark/grocery_acceptance.yaml --gate --resume
gap skills check --download verifies the skill bundles and runs any
per-bundle prefetch hooks; the current bundles do not define one, so model
weights download lazily on the first model call instead. Expect the first
trial of a fresh checkout to be slow for that reason.
Config anatomy#
grocery_acceptance.yaml
has no benchmark: block, so it runs in suites mode: each suites: entry is
one independent cell, and all cells launch concurrently — one suite per task:
task: "auto" # per-task prompts resolved from LIBERO metadata
llm:
provider: vertex
model: gemini-3.1-pro-preview
project_id: bc-y7-06
gate_threshold: 0.90
suites:
- suite_name: libero_object_all_variance
task_ids: [0]
task_prompts:
0: "Pick up the alphabet soup can and put it in the basket."
objects:
target: "alphabet soup can with a blue and yellow label"
expected_label: "Alphabet Soup"
shape_hint: "metal cylinder about 7 cm wide, wrapped in a blue-and-yellow label"
num_workers: 1
# ... 9 more suites, one per task
trials:
trials_per_generation: 50
task_ids: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
num_workers: 25
regenerate_code_per_trial: false
record_video: true
task_timeout_secs: 900
enable_tracing: false
output_dir: ../../benchmark_runs/grocery_acceptance
The parts worth knowing:
objects:hints are load-bearing. Each task’starget/expected_label/shape_hintblock is appended to the code generation prompt so the generated perception scripts get an unambiguous description of the LIBERO grocery asset. Drop them and the generated graphs misidentify look-alike items.task_timeout_secs: 900, not lower. Flat-box grasp planning legitimately runs long when the goalset planner falls back to per-pose iteration — a tight timeout kills trials that are on track to succeed.regenerate_code_per_trial: falsegenerates one graph per task and reuses it across that task’s trials; seeds select the suite’s baked initial-state variations.skills:is optional. Omit it to auto-discover the open-robot-skills checkout ($GAP_SKILLS_PATH, or a checkout next to the GaP repo); explicit relative paths resolve against the config file’s directory.
grocery_acceptance_smoke.yaml
is the same recipe shrunk to tasks 0–1 × 10 trials — same prompts and
objects: blocks, smaller worker budget. Run it before committing to the
full gate.
What gets generated#
sample_generated_graph/
is a gap.agent.generate output for task 0 (“Pick the blue
and yellow alphabet soup can and place it in the basket”). The coordinator
decomposed the task into four subgraphs:
Subgraph |
Skill |
What it does |
|---|---|---|
|
|
observe → DINO + VLM-tournament + SAM3 perception script → |
|
|
same pipeline aimed at the basket |
|
|
open gripper → top-down grasp candidates → axis-locked straight-Z descend (collision-aware planner fallback) → observe → close |
|
|
compute drop pose → waypoint move above basket → descend and release |
The subgraph agents authored the inner state machines plus the helper scripts
under scripts/ (perception, grasp descend, drop-pose computation), and a
checkpoint agent attached validate=True postconditions that
checkpoint enforcement checks against
simulator ground truth at every subgraph exit.
The grasp script (scripts/grasp_sg/grasp_descend_linear.py) carries the same
tuned recipe as Grocery Packing’s grasp_move.py,
distilled to the single-object case: rise, translate in XY over the object,
then a pure-vertical, orientation-locked descend via
curobo.plan_directed_linear (allowed_axes=["Z"],
orientation_mode="LOCK") — with a hand-off to the collision-aware CuRobo
planner when the straight-line solve is infeasible. Unlike the planner’s
goalset, the straight-Z fast path also grips a flat object (the cream-cheese
box) by simply lowering onto it, and its grip depth is floored above the
object base so the fingers never strike the table.
Run the sample graph directly — no generation needed (still needs the perception VLM credential):
MUJOCO_GL=egl uv run gap run examples/grocery_fulfillment/sample_generated_graph \
--sim libero_object_all_variance/0 --checkpoints warn
libero_object_all_variance/0:
perceive the described can and the basket, straight-Z grasp, transport,
release — recorded by gap run's default video capture.Checkpoint sidecars#
Each subgraph gets a generated sidecar under checkpoints/ (“Do not edit” —
the harness re-executes the original builder block to capture the predicate
lambdas with their closures). The perception subgraphs verify their OBBs
against the privileged object poses; from
checkpoints/grasp_sg.py:
sg.add_checkpoint('grasp_pose_above_table',
predicate=lambda w, o: o['grasp_pose']['position']['z'] > 0.01,
rationale='planned grasp z-height is above the tabletop', validate=True)
sg.add_checkpoint('target_held',
predicate=lambda w: w.body('alphabet soup').is_grasped(),
rationale='the alphabet soup can is held by the robot gripper', validate=True)
and from checkpoints/transport_sg.py:
sg.add_checkpoint('target_in_container',
predicate=lambda w: w.body('alphabet soup').is_in(w.body('basket')),
rationale='alphabet soup settled inside the basket after release', validate=True)
Predicates take the privileged world snapshot w
(w.body(name).position / .is_grasped() / .is_in() / .interior_lower /
.interior_upper) and optionally the subgraph’s bound outputs o.
gap.execute(..., checkpoints="warn") logs violations;
checkpoints="raise" turns them into failures. The sidecars also double as a
readable record of the gap.builder API the generator emits — see
Builder.
Next steps#
Grocery Packing — the pack-everything loop whose grasp/transport recipes this family shares.
Benchmark Grids — all benchmark configs, gate semantics, and resume behavior.
Generate a Graph — the generation pipeline this benchmark drives at grid scale, run once interactively.
Benchmarking — the full benchmarking guide.
Checkpoints — how
validate=Truepostconditions are enforced at runtime.