File size: 14,070 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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
"""YAM BIMANUAL: take the lid off the pot, then put food inside.

The LEFT arm lifts the lid off the pot and parks it on the table; only then can the RIGHT arm
drop a piece of food into the open pot. The ordering is the point -- the food cannot go in while
the lid is on, so the two arms have to take turns on a shared workspace.

The pot is built from primitive walls and the lid is a separate rigid body, because RoboTwin's
060_kitchenpot is a single fused mesh whose lid cannot be removed.

    python scripts/yam_lid_food.py --headless --food apple --video outputs/tasks/lid_food.mp4
"""
import argparse, sys, os
from isaaclab.app import AppLauncher

parser = argparse.ArgumentParser()
parser.add_argument("--food", default="apple", help="the item to put in the pot")
parser.add_argument("--food_xy", default="0.06,0.03", help="food start x,y (right side, env-local)")
parser.add_argument("--pot_xy", default="0.00,0.13", help="pot centre x,y (left side, env-local)")
parser.add_argument("--lid_park_xy", default="-0.10,0.20", help="where the left arm parks the lid")
parser.add_argument("--episode", type=int, default=-1)
parser.add_argument("--video", default="outputs/tasks/yam_lid_food.mp4")
AppLauncher.add_app_launcher_args(parser)
args = parser.parse_args(); args.headless = True; args.enable_cameras = True
app = AppLauncher(args).app

import numpy as np, torch, gymnasium as gym
import imageio.v2 as imageio
from PIL import Image, ImageDraw
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, "source")); sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bimanual.tasks.manager_based.yam  # noqa
from isaaclab_tasks.utils import parse_env_cfg

TASK = "Template-YAM-Play-v0"; dev = "cuda:0"
_cfg = parse_env_cfg(TASK, device=dev, num_envs=1)
_cfg.episode_length_s = 1.0e6
try:
    _cfg.terminations.time_out = None
except Exception as _e:
    print("[lf] time_out disable failed:", _e)
try:
    _cfg.viewer.eye = (0.95, -0.95, 1.15); _cfg.viewer.lookat = (0.02, 0.05, 0.5)
    _cfg.viewer.resolution = (720, 540)
except Exception as _e:
    print("viewer cfg:", _e)
env = gym.make(TASK, cfg=_cfg, render_mode="rgb_array"); u = env.unwrapped; env.reset()


def Rq(q):
    w, x, y, z = q
    return np.array([[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
                     [2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
                     [2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)]])


def qR(m):
    t = m[0, 0]+m[1, 1]+m[2, 2]
    if t > 0:
        s = np.sqrt(t+1)*2; w = .25*s; x = (m[2, 1]-m[1, 2])/s; y = (m[0, 2]-m[2, 0])/s; z = (m[1, 0]-m[0, 1])/s
    elif m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]:
        s = np.sqrt(1+m[0, 0]-m[1, 1]-m[2, 2])*2; w = (m[2, 1]-m[1, 2])/s; x = .25*s; y = (m[0, 1]+m[1, 0])/s; z = (m[0, 2]+m[2, 0])/s
    elif m[1, 1] > m[2, 2]:
        s = np.sqrt(1+m[1, 1]-m[0, 0]-m[2, 2])*2; w = (m[0, 2]-m[2, 0])/s; x = (m[0, 1]+m[1, 0])/s; y = .25*s; z = (m[1, 2]+m[2, 1])/s
    else:
        s = np.sqrt(1+m[2, 2]-m[0, 0]-m[1, 1])*2; w = (m[1, 0]-m[0, 1])/s; x = (m[0, 2]+m[2, 0])/s; y = (m[1, 2]+m[2, 1])/s; z = .25*s
    q = np.array([w, x, y, z]); q /= np.linalg.norm(q)+1e-9
    return q if q[0] >= 0 else -q


origin = u.scene.env_origins[0].cpu().numpy()
R = u.scene["right_robot"]; Rbn = list(R.data.body_names)
L = u.scene["left_robot"]; Lbn = list(L.data.body_names)
rroot = R.data.root_pos_w[0].cpu().numpy()-origin; rrootq = R.data.root_quat_w[0].cpu().numpy()
lroot = L.data.root_pos_w[0].cpu().numpy()-origin; lrootq = L.data.root_quat_w[0].cpu().numpy()
OFF = np.array([0, 0, 0.13]); TABLE_TOP = 0.45
OPEN, CLOSE = 1.0, -1.0
PXY = [float(v) for v in args.pot_xy.split(",")]
FXY = [float(v) for v in args.food_xy.split(",")]
KXY = [float(v) for v in args.lid_park_xy.split(",")]
for _n in ("lid", args.food):
    if _n not in u.scene.rigid_objects:
        raise SystemExit(f"[lf] {_n!r} not in scene")
LID = u.scene.rigid_objects["lid"]; FOOD = u.scene.rigid_objects[args.food]

# ---- the pot: four primitive walls leaving a rectangular mouth the lid sits on ----
import isaaclab.sim as sim_utils
MOUTH_X, MOUTH_Y, WALL_H, WALL_T = 0.135, 0.048, 0.085, 0.012


def _wall(name, size, off, color=(0.40, 0.42, 0.48)):
    c = sim_utils.CuboidCfg(size=tuple(size),
                            visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=color),
                            collision_props=sim_utils.CollisionPropertiesCfg())
    c.func(f"/World/envs/env_0/pot_{name}", c,
           translation=tuple((origin+np.array([PXY[0]+off[0], PXY[1]+off[1], TABLE_TOP+off[2]],
                                              np.float32)).astype(float).tolist()))


_wall("floor", (MOUTH_X+2*WALL_T, MOUTH_Y+2*WALL_T, 0.010), (0, 0, 0.005))
_wall("xp", (WALL_T, MOUTH_Y+2*WALL_T, WALL_H), (MOUTH_X/2+WALL_T/2, 0, WALL_H/2))
_wall("xn", (WALL_T, MOUTH_Y+2*WALL_T, WALL_H), (-MOUTH_X/2-WALL_T/2, 0, WALL_H/2))
_wall("yp", (MOUTH_X+2*WALL_T, WALL_T, WALL_H), (0, MOUTH_Y/2+WALL_T/2, WALL_H/2))
_wall("yn", (MOUTH_X+2*WALL_T, WALL_T, WALL_H), (0, -MOUTH_Y/2-WALL_T/2, WALL_H/2))
print(f"[lf] pot at ({PXY[0]},{PXY[1]}) mouth={MOUTH_X}x{MOUTH_Y} wall_h={WALL_H}", flush=True)


def eef_root(a, bn, root, rootq):
    i = bn.index("link_6"); p = a.data.body_pos_w[0, i].cpu().numpy()-origin
    q = a.data.body_quat_w[0, i].cpu().numpy()
    return Rq(rootq).T@((p+Rq(q)@OFF)-root), q


lp0, _ = eef_root(L, Lbn, lroot, lrootq)
rp0, _ = eef_root(R, Rbn, rroot, rrootq)
JAW_X = qR(np.stack([np.array([1., 0., 0.]), np.array([0., -1., 0.]), np.array([0., 0., -1.])], axis=1))
JAW_Y = qR(np.stack([np.array([0., 1., 0.]), np.array([1., 0., 0.]), np.array([0., 0., -1.])], axis=1))


def act2(lp, lq, lg, rp, rq, rg):
    return torch.tensor(np.concatenate([lp, lq, [lg], rp, rq, [rg]]),
                        dtype=torch.float32, device=dev).view(1, -1)


LID_T = 0.016
LID.write_root_pose_to_sim(torch.tensor(
    np.concatenate([origin+np.array([PXY[0], PXY[1], TABLE_TOP+WALL_H+LID_T/2+0.002]), [1, 0, 0, 0]]),
    dtype=torch.float32, device=dev).view(1, 7))
LID.write_root_velocity_to_sim(torch.zeros((1, 6), device=dev))
FOOD.write_root_pose_to_sim(torch.tensor(
    np.concatenate([origin+np.array([FXY[0], FXY[1], 0.52]), [1, 0, 0, 0]]),
    dtype=torch.float32, device=dev).view(1, 7))
FOOD.write_root_velocity_to_sim(torch.zeros((1, 6), device=dev))
for _ in range(90):
    env.step(act2(lp0, JAW_X, OPEN, rp0, JAW_Y, OPEN))


def _boost(view, tag, s=1.7, d=1.5):
    try:
        m = view.get_material_properties().clone(); m[..., 0] = s; m[..., 1] = d
        view.set_material_properties(m, torch.arange(m.shape[0], dtype=torch.int32, device=m.device))
    except Exception as e:
        print(f"[lf] friction failed {tag}:", e, flush=True)


_boost(R.root_physx_view, "right"); _boost(L.root_physx_view, "left")
_boost(LID.root_physx_view, "lid"); _boost(FOOD.root_physx_view, args.food)

import omni.usd
from pxr import UsdGeom, Usd
stage = omni.usd.get_context().get_stage()
bbc = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_, UsdGeom.Tokens.render])
frng = bbc.ComputeWorldBound(stage.GetPrimAtPath(FOOD.root_physx_view.prim_paths[0])).ComputeAlignedRange()
fext = np.array(frng.GetMax())-np.array(frng.GetMin())
print(f"[lf] food {args.food} size={np.round(fext,3)}", flush=True)


def lidw():
    return LID.data.root_pos_w[0].cpu().numpy()-origin


def foodw():
    return FOOD.data.root_pos_w[0].cpu().numpy()-origin


def eefL():
    p, _ = eef_root(L, Lbn, lroot, lrootq); return p


def eefR():
    p, _ = eef_root(R, Rbn, rroot, rrootq); return p


def fsep(a):
    jn = list(a.data.joint_names)
    return (float(a.data.joint_pos[0, jn.index("left_finger")].item())
            + float(a.data.joint_pos[0, jn.index("right_finger")].item()))/2


frames = []; _phase = {"v": "start"}; _RESULT = {"v": ""}; _G = {"l": "OPEN", "r": "OPEN"}


def capture():
    img = env.render()
    if img is None:
        return
    im = Image.fromarray(np.asarray(img)[..., :3].copy()); d = ImageDraw.Draw(im)
    lw, fw = lidw(), foodw()
    lines = ["=== LID OFF THE POT, THEN FOOD IN ===" + (f"  EP {args.episode}" if args.episode >= 0 else "")]
    if _RESULT["v"]:
        lines.append(f"RESULT: {_RESULT['v']}")
    lines += [f"ACTION: {_phase['v']}",
              f"left={_G['l']}  right={_G['r']}",
              f"lid=({lw[0]:+.2f},{lw[1]:+.2f},{lw[2]:.2f})  food=({fw[0]:+.2f},{fw[1]:+.2f},{fw[2]:.2f})"]
    d.rectangle([0, 0, 470, 18*len(lines)+6], fill=(0, 0, 0))
    y = 3
    for ln in lines:
        d.text((6, y), ln, fill=(255, 235, 60)); y += 18
    frames.append(np.array(im))


_CL = {"v": np.zeros(3, np.float32)}; _CR = {"v": np.zeros(3, np.float32)}
_CMD = {"l": None, "r": None}
_Q = {"l": JAW_X, "r": JAW_Y}


def _ease(a):
    return float(0.5-0.5*np.cos(np.pi*min(max(a, 0.0), 1.0)))


def drive(lt, rt, lg, rg, n):
    ls = _CMD["l"].copy() if _CMD["l"] is not None else eefL().astype(np.float32)
    rs = _CMD["r"].copy() if _CMD["r"] is not None else eefR().astype(np.float32)
    lt = ls if lt is None else np.asarray(lt, np.float32)
    rt = rs if rt is None else np.asarray(rt, np.float32)
    _G["l"] = "CLOSE" if lg < 0 else "OPEN"; _G["r"] = "CLOSE" if rg < 0 else "OPEN"
    cl, cr = _CL["v"], _CR["v"]
    for k in range(n):
        a = _ease((k+1)/float(n))
        lc = (1-a)*ls+a*lt; rc = (1-a)*rs+a*rt
        _CMD["l"], _CMD["r"] = lc, rc
        env.step(act2((lc+cl).astype(np.float32), _Q["l"], lg, (rc+cr).astype(np.float32), _Q["r"], rg))
        el = lc-eefL(); el = np.where(np.abs(el) > 0.008, el, 0.0)
        er = rc-eefR(); er = np.where(np.abs(er) > 0.008, er, 0.0)
        cl = np.clip(cl+0.08*el, -0.10, 0.10); cl[2] = max(float(cl[2]), -0.06)
        cr = np.clip(cr+0.08*er, -0.10, 0.10); cr[2] = max(float(cr[2]), -0.06)
        _CL["v"], _CR["v"] = cl, cr
        if k % 3 == 0:
            capture()


def clamp(arm, lg, rg, n=150):
    prev = fsep(arm); stall = 0
    for k in range(n):
        env.step(act2((_CMD["l"]+_CL["v"]).astype(np.float32), _Q["l"], lg,
                      (_CMD["r"]+_CR["v"]).astype(np.float32), _Q["r"], rg))
        if k % 5 == 0:
            capture()
        cur = fsep(arm)
        stall = stall+1 if abs(cur-prev) < 0.0002 else 0
        prev = cur
        if stall >= 8 and cur < -0.002:
            print(f"[lf] jaw stalled at fsep={cur:.4f} after {k}", flush=True)
            return True
    print(f"[lf] jaw did NOT stall (fsep={fsep(arm):.4f})", flush=True)
    return False


def to_L(w):
    return (Rq(lrootq).T@(np.asarray(w, np.float32)-lroot)).astype(np.float32)


def to_R(w):
    return (Rq(rrootq).T@(np.asarray(w, np.float32)-rroot)).astype(np.float32)


# ---------------- phase 1: LEFT arm takes the lid off ----------------
lid0 = lidw()
lid_grip = np.array([lid0[0], lid0[1], TABLE_TOP+WALL_H+LID_T*0.4], np.float32)
print(f"[lf] lid at ({lid0[0]:.3f},{lid0[1]:.3f},{lid0[2]:.3f}) grip_z={lid_grip[2]:.3f}", flush=True)
_phase["v"] = "1. LEFT approach the lid"
drive(to_L(lid_grip+np.array([0, 0, 0.13], np.float32)), None, OPEN, OPEN, 130)
_phase["v"] = "2. LEFT descend onto the lid"
drive(to_L(lid_grip), None, OPEN, OPEN, 110)
got_lid = clamp(L, CLOSE, OPEN)
_phase["v"] = "3. LEFT lift the lid clear"
drive(to_L(lid_grip+np.array([0, 0, 0.15], np.float32)), None, CLOSE, OPEN, 130)
z_lid = float(lidw()[2])
_phase["v"] = "4. LEFT park the lid aside"
park = np.array([KXY[0], KXY[1], TABLE_TOP+0.15], np.float32)
drive(to_L(park), None, CLOSE, OPEN, 150)
drive(to_L(np.array([KXY[0], KXY[1], TABLE_TOP+LID_T/2+0.02], np.float32)), None, CLOSE, OPEN, 110)
_phase["v"] = "5. LEFT release the lid"
drive(None, None, OPEN, OPEN, 45)
drive(to_L(park+np.array([0, 0, 0.06], np.float32)), None, OPEN, OPEN, 90)
lid_off = float(np.hypot(lidw()[0]-PXY[0], lidw()[1]-PXY[1])) > 0.08
print(f"[lf] lid removed={lid_off} lifted_to={z_lid:.3f} now=({lidw()[0]:.3f},{lidw()[1]:.3f})", flush=True)

# ---------------- phase 2: RIGHT arm puts the food in ----------------
fw = foodw()
food_grip = np.array([fw[0], fw[1], TABLE_TOP+float(fext[2])/2.0], np.float32)
_phase["v"] = "6. RIGHT approach the food"
drive(None, to_R(food_grip+np.array([0, 0, 0.13], np.float32)), OPEN, OPEN, 130)
_phase["v"] = "7. RIGHT descend onto the food"
drive(None, to_R(food_grip), OPEN, OPEN, 120)
got_food = clamp(R, OPEN, CLOSE)
f_z0 = float(foodw()[2])
_phase["v"] = "8. RIGHT lift the food"
drive(None, to_R(food_grip+np.array([0, 0, 0.17], np.float32)), OPEN, CLOSE, 130)
_phase["v"] = "9. RIGHT carry over the open pot"
over = np.array([PXY[0], PXY[1], TABLE_TOP+WALL_H+float(fext[2])/2.0+0.05], np.float32)
drive(None, to_R(over+np.array([0, 0, 0.06], np.float32)), OPEN, CLOSE, 160)
_phase["v"] = "10. RIGHT lower into the pot"
drive(None, to_R(over), OPEN, CLOSE, 110)
_phase["v"] = "11. RIGHT release the food"
drive(None, None, OPEN, OPEN, 45)
_phase["v"] = "12. RIGHT retreat"
drive(None, to_R(over+np.array([0, 0, 0.14], np.float32)), OPEN, OPEN, 100)
for _ in range(70):
    env.step(act2((_CMD["l"]+_CL["v"]).astype(np.float32), _Q["l"], OPEN,
                  (_CMD["r"]+_CR["v"]).astype(np.float32), _Q["r"], OPEN))

lf = lidw(); ff = foodw()
food_in = (abs(ff[0]-PXY[0]) < MOUTH_X/2+0.02 and abs(ff[1]-PXY[1]) < MOUTH_Y/2+0.03
           and ff[2] < TABLE_TOP+WALL_H)
_RESULT["v"] = "SUCCESS" if (lid_off and food_in) else "FAIL"
_phase["v"] = "DONE"
print(f"[lf] EPISODE_RESULT: {_RESULT['v']} lid_off={lid_off} food_in_pot={food_in} "
      f"lid=({lf[0]:.3f},{lf[1]:.3f},{lf[2]:.3f}) food=({ff[0]:.3f},{ff[1]:.3f},{ff[2]:.3f})", flush=True)
for _ in range(16):
    capture()

os.makedirs(os.path.dirname(args.video), exist_ok=True)
# Drop the warm-up frames: before the renderer settles they come out with the wrong camera
# pose, unresolved textures and missing geometry.
if len(frames) > 6:
    frames = frames[2:]
if frames:
    imageio.mimsave(args.video, frames, fps=14)
print(f"[lf] video -> {args.video} ({len(frames)} frames)", flush=True)
env.close(); app.close(); print("YAM_LID_FOOD_OK", flush=True)