File size: 4,754 Bytes
7399b6f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | # YAM task suite
Scripted manipulation tasks for the YAM bimanual arm in Isaac Lab, laid out the way ManiSkill
lays out its tasks: one registered class per task file, with the environment, the solvers and the
motion planner as separate layers.
```
yam/
motion/ how the robot MOVES arm.py · planner.py · recorder.py
envs/ the environment a task runs in base_env.py · scene.py
solvers/ scripted skills, one module each pick_place · multi_pick · insert · stack · dual_lift
tasks/ ONE FILE PER TASK, registered by name
registry.py · conditions.py
```
Run one:
```bash
export ROBOTWIN_USD=/home/yu/internship_yu/robotwin_usd
python scripts/yam_task.py --list
python scripts/yam_task.py --task grape_box --seed 3
python scripts/yam_task.py --task grape_box --no-randomize # nominal poses
```
## Writing a task
A task declares *what is in the scene* and *what counts as done*. It never touches action
vectors, IK or the video writer.
```python
@register_task("grape_box")
class GrapeBoxTask(YamTaskEnv):
title = "GRAPE -> BOX"
tags = ["pick-place"]
grape_spawn = (-0.03, 0.10) # task constants as class attrs
grape_spawn_jitter = (0.025, 0.015) # randomization range
gripper_effort, gripper_damping = 75, 85
def _load_scene(self):
self.scene.build_box({"name": "box", "xy": (-0.02, -0.26), "span": 0.26, "wall_h": 0.05})
self._placed = [self.scene.place_object(
{"name": "grape", "xy": self.grape_spawn, "xy_jitter": self.grape_spawn_jitter})]
def solve(self):
return pick_place.solve(self, obj="grape", target="box")
def evaluate(self):
return self.check(C.labelled("grape in box", C.object_in_region("grape", "box")),
C.labelled("grape lifted", C.object_lifted("grape", 0.05)))
```
Add the module to `tasks/__init__.py` so the registry sees it. Lifecycle is
`_load_scene()` -> `_initialize_episode()` -> `solve()` -> `evaluate()`.
Randomization: any object/container/marker accepts `xy_jitter` and `yaw_jitter`; `--seed`
reproduces an episode exactly. Nominal poses with `--no-randomize`.
## Asset gotchas this framework handles for you
Every one of these cost a debugging session; they are now enforced in `envs/scene.py` rather
than repeated per task.
| Symptom | Cause | Handled by |
|---|---|---|
| cup upside-down, basket on its side | RoboTwin GLBs are authored **Y-up** | pass `rpy: (90,0,0)`; the container default |
| container spawns 1.9 m wide | the GLB->USD converter reads `model_data` scale but does not bake it | pass `scale:` at spawn |
| tall object lands on its side | objects are written in above the table and drop | `reseat_objects()` seats each at its measured height |
| arm never moves, huge tracking error | a tall prop sits on the arm's home pose | height-aware warning at build time |
| container asset missing | the `_mesh` variant was never converted | explicit error printing the exact convert command |
| container swallows objects / they roll off it | convex decomposition fills the cavity | convert containers with `--collision none --suffix _mesh` |
## Grasping rules the solvers apply
* **Position from physics, size from the bbox.** `object_pos()` is live; `object_size()` is the
authored bbox and is only valid for extents — it does not follow the object once it moves or is
rotated.
* **Close across the narrow axis.** The jaw opens 9.4 cm. A bottle lying on its side is 18 cm long
and 5 cm across; `jaw="auto"` picks the axis that fits.
* **A stall is not a grasp.** `ArmController.grasp()` rejects a stall at an implausibly wide gap
(that is the jaw resting on the body) and then verifies the object actually rises. Without both
checks an episode mimes the whole sequence with an empty hand and still reports success.
* **Clamp force is object-dependent.** Thin-walled vessels get crushed through above ~50; heavy
solids slip below ~70. Set `gripper_effort` per task.
* **Two arms move on one profile.** `env.move_both()` commands both and steps once; driving them
in sequence parks the first arm in the second's path.
## Verification
The numeric success check only inspects final object poses, and it will happily pass an episode
where the cup landed upside-down or one gripper hung empty in the air. Render a contact sheet and
have a vision agent judge it:
```bash
python scripts/yam_agent_loop.py evaluate # contact sheets + review prompts
python scripts/yam_agent_loop.py update # verdicts -> concrete parameter fixes
python scripts/yam_agent_loop.py propose # uncovered skill axes from the asset library
```
Only `numeric=SUCCESS` **and** `visual=CONFIRMED` counts as done.
|