| """YAM BIMANUAL pot lift: both arms grasp the pot by its two opposite handles and lift it |
| together, keeping it level. |
| |
| One arm cannot lift a wide pot -- the point of the task is the two-arm synchronisation: both |
| jaws close on opposite handles, then both wrists rise along the same profile so the pot stays |
| flat. The pot is a RoboTwin (MIT) asset converted GLB->USD; its handle axis is measured from |
| the mesh bbox at runtime and the pot is yawed so the handles face the two arms. |
| |
| ROBOTWIN_USD=/path/to/usd YAM_RW_OBJECTS="rw_pot=060_kitchenpot/base0.usd:0.05:0.30" \ |
| python scripts/yam_pot_lift.py --headless --video outputs/tasks/pot.mp4 |
| """ |
| import argparse, sys, os |
| from isaaclab.app import AppLauncher |
|
|
| parser = argparse.ArgumentParser() |
| parser.add_argument("--obj", default="rw_pot", help="pot object registered via YAM_RW_OBJECTS") |
| parser.add_argument("--pot_xy", default="0.04,0.00", help="pot centre x,y (env-local)") |
| parser.add_argument("--pot_rpy", default="0,0,0", |
| help="initial roll,pitch,yaw in DEGREES; RoboTwin meshes are not all authored " |
| "z-up, so a pot may need a roll to stand on its base") |
| parser.add_argument("--grip_tilt", type=float, default=0.0, |
| help="tilt each gripper this many DEGREES away from straight-down, leaning in " |
| "over the pot, so the fingers hook around an ear handle from outside " |
| "instead of pressing onto it from above") |
| parser.add_argument("--half_y", type=float, default=-1.0, |
| help="override the handle half-span (m). The USD bbox is the AUTHORED one and " |
| "does not follow the mesh once it is rotated, so measuring it is unreliable") |
| parser.add_argument("--grip_z", type=float, default=-1.0, help="override the grip height (world z)") |
| parser.add_argument("--grip_max", type=float, default=0.022, |
| help="a stall wider than this (per finger) means the jaw hit the body, not a " |
| "handle; treat it as no grasp rather than a successful clamp") |
| parser.add_argument("--half_y_l", type=float, default=-1.0, |
| help="per-arm handle offset for the LEFT arm; the mesh origin is not centred " |
| "between the two handles, so a single symmetric span misses one of them") |
| parser.add_argument("--half_y_r", type=float, default=-1.0, help="per-arm handle offset, RIGHT arm") |
| parser.add_argument("--jaw", default="y", choices=["x", "y"], |
| help="world axis each jaw closes along. 'y' grips a pot rim/handle from the " |
| "side; 'x' grips ACROSS a plank's width, which is how two arms carry a board") |
| parser.add_argument("--probe_only", action="store_true", |
| help="place the pot, let it settle, print its resting size/orientation, exit") |
| parser.add_argument("--lift", type=float, default=0.14, help="how high to lift the pot (m)") |
| parser.add_argument("--grip_inset", type=float, default=0.004, |
| help="close the jaws this far inside the handle tip (m)") |
| parser.add_argument("--episode", type=int, default=-1) |
| parser.add_argument("--video", default="outputs/tasks/yam_pot_lift.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 yam_prm |
| import bimanual.tasks.manager_based.yam |
| 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("[pot] time_out disable failed:", _e) |
| try: |
| _cfg.viewer.eye = (0.95, -0.95, 1.15); _cfg.viewer.lookat = (0.05, 0.0, 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 |
| if args.obj not in u.scene.rigid_objects: |
| raise SystemExit(f"[pot] object {args.obj!r} is not in the scene -- register it with " |
| f"YAM_RW_OBJECTS and point ROBOTWIN_USD at the converted assets") |
|
|
|
|
| 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, lq0 = eef_root(L, Lbn, lroot, lrootq) |
| rp0, rq0 = eef_root(R, Rbn, rroot, rrootq) |
| def tilted_grasp(sign): |
| """Grasp frame that leans `--grip_tilt` deg off vertical toward the pot's centre. |
| |
| `sign` is +1 for the arm on the +y side and -1 for the -y side, so both hands come at the |
| ear handles from outside-in. Jaw closes along world x, i.e. ACROSS the ear, which is the |
| only way a parallel jaw can hold a tab that sticks out sideways. |
| """ |
| t = np.radians(args.grip_tilt) |
| c, s = np.cos(t), np.sin(t) |
| X = np.array([1., 0., 0.]) |
| Z = np.array([0., -sign*s, -c]) |
| Y = np.cross(Z, X) |
| return qR(np.stack([X, Y, Z], axis=1)) |
|
|
|
|
| if args.grip_tilt != 0.0: |
| gq = tilted_grasp(+1) |
| else: |
| gq = qR(np.stack([np.array([1., 0., 0.]), np.array([0., -1., 0.]), np.array([0., 0., -1.])], axis=1) |
| if args.jaw == "x" else |
| np.stack([np.array([0., 1., 0.]), np.array([1., 0., 0.]), np.array([0., 0., -1.])], axis=1)) |
| GQ_L = tilted_grasp(+1) if args.grip_tilt != 0.0 else gq |
| GQ_R = tilted_grasp(-1) if args.grip_tilt != 0.0 else gq |
|
|
|
|
| def act2(lp, lg, rp, rg): |
| |
| return torch.tensor(np.concatenate([lp, GQ_L, [lg], rp, GQ_R, [rg]]), |
| dtype=torch.float32, device=dev).view(1, -1) |
|
|
|
|
| |
| PXY = [float(v) for v in args.pot_xy.split(",")] |
| pot = u.scene.rigid_objects[args.obj] |
|
|
|
|
| def _quat(roll, pitch, yaw): |
| cr, sr = np.cos(roll/2), np.sin(roll/2) |
| cp, sp = np.cos(pitch/2), np.sin(pitch/2) |
| cy, sy = np.cos(yaw/2), np.sin(yaw/2) |
| return np.array([cr*cp*cy+sr*sp*sy, sr*cp*cy-cr*sp*sy, cr*sp*cy+sr*cp*sy, cr*cp*sy-sr*sp*cy]) |
|
|
|
|
| _RPY = [np.radians(float(v)) for v in args.pot_rpy.split(",")] |
|
|
|
|
| def _set_pot(xy, yaw_extra, z=0.62): |
| q = _quat(_RPY[0], _RPY[1], _RPY[2]+yaw_extra) |
| pose = torch.tensor(np.concatenate([origin+np.array([xy[0], xy[1], z]), q]), |
| dtype=torch.float32, device=dev).view(1, 7) |
| pot.write_root_pose_to_sim(pose); pot.write_root_velocity_to_sim(torch.zeros((1, 6), device=dev)) |
|
|
|
|
| _set_pot(PXY, 0.0) |
| for _ in range(70): |
| env.step(act2(lp0, OPEN, rp0, OPEN)) |
|
|
| 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]) |
| rng = bbc.ComputeWorldBound(stage.GetPrimAtPath(pot.root_physx_view.prim_paths[0])).ComputeAlignedRange() |
| ext = np.array(rng.GetMax())-np.array(rng.GetMin()) |
| print(f"[pot] {args.obj} bbox extents={np.round(ext,3)}", flush=True) |
| |
| |
| |
| |
| |
| _set_pot(PXY, 0.0, z=TABLE_TOP+float(ext[2])/2.0+0.004) |
| for _ in range(90): |
| env.step(act2(lp0, OPEN, rp0, OPEN)) |
| rng = bbc.ComputeWorldBound(stage.GetPrimAtPath(pot.root_physx_view.prim_paths[0])).ComputeAlignedRange() |
| ext = np.array(rng.GetMax())-np.array(rng.GetMin()) |
| |
| _yaw = np.pi/2 if ext[0] > ext[1]*1.05 else 0.0 |
| if _yaw: |
| _set_pot(PXY, _yaw, z=TABLE_TOP+float(ext[2])/2.0+0.004) |
| for _ in range(90): |
| env.step(act2(lp0, OPEN, rp0, OPEN)) |
| |
| |
| |
| ext = np.array([ext[1], ext[0], ext[2]]) |
| _q0 = pot.data.root_quat_w[0].cpu().numpy() |
| |
| |
| |
| _UP_REF = Rq(_q0)@np.array([0., 0., 1.]) |
| _tilt0 = 0.0 |
|
|
|
|
| def tilt_from_rest(q): |
| v = Rq(q)@np.array([0., 0., 1.]) |
| return float(np.degrees(np.arccos(np.clip(float(np.dot(v, _UP_REF)), -1.0, 1.0)))) |
| print(f"[pot] seated: yaw={np.degrees(_yaw):.0f}deg extents={np.round(ext,3)} rest_tilt={_tilt0:.1f}deg", flush=True) |
| if args.probe_only: |
| _w = pot.data.root_pos_w[0].cpu().numpy()-origin |
| print(f"[pot] PROBE pos=({_w[0]:.3f},{_w[1]:.3f},{_w[2]:.3f}) extents={np.round(ext,4)} " |
| f"tilt={_tilt0:.1f}deg", flush=True) |
| env.close(); app.close(); print("YAM_POT_PROBE_OK", flush=True); sys.exit(0) |
|
|
| pw = pot.data.root_pos_w[0].cpu().numpy()-origin |
| half_y = float(args.half_y) if args.half_y > 0 else float(ext[1])/2.0 |
| pot_h = float(ext[2]) |
| |
| grip_z = float(args.grip_z) if args.grip_z > 0 else TABLE_TOP + pot_h*0.72 |
| _hy_l = float(args.half_y_l) if args.half_y_l > 0 else half_y |
| _hy_r = float(args.half_y_r) if args.half_y_r > 0 else half_y |
| Ly = pw[1] + _hy_l - args.grip_inset |
| Ry = pw[1] - _hy_r + args.grip_inset |
| L_grip_w = np.array([pw[0], Ly, grip_z], np.float32) |
| R_grip_w = np.array([pw[0], Ry, grip_z], np.float32) |
| print(f"[pot] pot at ({pw[0]:.3f},{pw[1]:.3f}) h={pot_h:.3f} half_y={half_y:.3f} grip_z={grip_z:.3f}", flush=True) |
|
|
|
|
| def to_root(w, root, rootq): |
| return (Rq(rootq).T@(np.asarray(w, np.float32)-root)).astype(np.float32) |
|
|
|
|
| L_grip = to_root(L_grip_w, lroot, lrootq); R_grip = to_root(R_grip_w, rroot, rrootq) |
| L_pre = L_grip+np.array([0, 0, 0.13], np.float32); R_pre = R_grip+np.array([0, 0, 0.13], np.float32) |
| print(f"[pot] L_grip(root)={np.round(L_grip,3)} R_grip(root)={np.round(R_grip,3)}", flush=True) |
|
|
| lhome_q = L.data.joint_pos[0].clone() |
|
|
|
|
| def _boost(view, tag, s=1.8, d=1.6): |
| 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"[pot] friction set failed on {tag}:", e, flush=True) |
|
|
|
|
| _boost(R.root_physx_view, "right"); _boost(L.root_physx_view, "left") |
| _boost(pot.root_physx_view, args.obj) |
|
|
|
|
| def eefL(): |
| p, _ = eef_root(L, Lbn, lroot, lrootq); return p |
|
|
|
|
| def eefR(): |
| p, _ = eef_root(R, Rbn, rroot, rrootq); return p |
|
|
|
|
| def potw(): |
| return pot.data.root_pos_w[0].cpu().numpy()-origin |
|
|
|
|
| def potq(): |
| return pot.data.root_quat_w[0].cpu().numpy() |
|
|
|
|
| def fsep(art): |
| jn = list(art.data.joint_names) |
| return (float(art.data.joint_pos[0, jn.index("left_finger")].item()) |
| + float(art.data.joint_pos[0, jn.index("right_finger")].item()))/2 |
|
|
|
|
| |
| frames = []; _phase = {"v": "approach"}; _RESULT = {"v": ""}; _GRIP = {"v": "OPEN"} |
| SEM = {"approach": "1. BOTH ARMS APPROACH handles", "descend": "2. DESCEND to handle height", |
| "close": "3. BOTH JAWS CLOSE on handles", "lift": "4. SYNCHRONISED LIFT", |
| "hold": "5. HOLD (pot level)", "lower": "6. LOWER back", "done": "DONE"} |
|
|
|
|
| def capture(): |
| img = env.render() |
| if img is None: |
| return |
| im = Image.fromarray(np.asarray(img)[..., :3].copy()); d = ImageDraw.Draw(im) |
| w = potw() |
| lines = ["=== DUAL-ARM POT LIFT (two handles) ===" |
| + (f" EP {args.episode}" if args.episode >= 0 else "")] |
| if _RESULT["v"]: |
| lines.append(f"RESULT: {_RESULT['v']}") |
| lines += [f"ACTION: {SEM.get(_phase['v'], _phase['v'])}", |
| f"grippers={_GRIP['v']} pot_z={w[2]:.3f} (start {z0:.3f})" if "z0" in globals() |
| else f"grippers={_GRIP['v']} pot_z={w[2]:.3f}"] |
| d.rectangle([0, 0, 420, 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)) |
|
|
|
|
| _CORRL = {"v": np.zeros(3, np.float32)}; _CORRR = {"v": np.zeros(3, np.float32)} |
| _CMD = {"l": None, "r": None} |
|
|
|
|
| def _ease(a): |
| return float(0.5-0.5*np.cos(np.pi*min(max(a, 0.0), 1.0))) |
|
|
|
|
| def drive(lt, lg, rt, rg, n, hold_left=False): |
| """Both arms LERP to their targets on the SAME profile, so the pot stays level.""" |
| 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 = np.asarray(lt, np.float32); rt = np.asarray(rt, np.float32) |
| _GRIP["v"] = ("CLOSE" if lg < 0 else "OPEN") |
| cl = _CORRL["v"]; cr = _CORRR["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"] = lc; _CMD["r"] = rc |
| env.step(act2((lc+cl).astype(np.float32), lg, (rc+cr).astype(np.float32), rg)) |
| if hold_left: |
| L.write_joint_state_to_sim(lhome_q.view(1, -1), torch.zeros((1, lhome_q.shape[0]), device=dev)) |
| 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) |
| _CORRL["v"] = cl; _CORRR["v"] = cr |
| if k % 3 == 0: |
| capture() |
|
|
|
|
| z0 = float(potw()[2]) |
| _phase["v"] = "approach"; drive(L_pre, OPEN, R_pre, OPEN, 110) |
| print(f"[pot] approached: L_err={np.linalg.norm(eefL()-L_pre):.3f} R_err={np.linalg.norm(eefR()-R_pre):.3f}", flush=True) |
| _phase["v"] = "descend"; drive(L_grip, OPEN, R_grip, OPEN, 120) |
| print(f"[pot] descended: L_err={np.linalg.norm(eefL()-L_grip):.3f} R_err={np.linalg.norm(eefR()-R_grip):.3f}", flush=True) |
|
|
| _phase["v"] = "close"; _GRIP["v"] = "CLOSE" |
| Lhold = eefL().astype(np.float32); Rhold = eefR().astype(np.float32) |
| _CMD["l"] = Lhold.copy(); _CMD["r"] = Rhold.copy() |
| prevL, prevR = fsep(L), fsep(R); stall = 0 |
| for k in range(170): |
| env.step(act2((Lhold+_CORRL["v"]).astype(np.float32), CLOSE, (Rhold+_CORRR["v"]).astype(np.float32), CLOSE)) |
| if k % 5 == 0: |
| capture() |
| curL, curR = fsep(L), fsep(R) |
| stall = stall+1 if (abs(curL-prevL) < 0.0002 and abs(curR-prevR) < 0.0002) else 0 |
| prevL, prevR = curL, curR |
| |
| |
| |
| if stall >= 8 and -args.grip_max < curL < -0.002 and -args.grip_max < curR < -0.002: |
| print(f"[pot] both jaws stalled on handle-sized gaps: L={curL:.4f} R={curR:.4f} " |
| f"(gaps {2*abs(curL)*100:.1f}/{2*abs(curR)*100:.1f} cm) after {k} steps", flush=True) |
| break |
|
|
| _phase["v"] = "lift" |
| Ltop = Lhold+np.array([0, 0, args.lift], np.float32) |
| Rtop = Rhold+np.array([0, 0, args.lift], np.float32) |
| drive(Ltop, CLOSE, Rtop, CLOSE, 150) |
| _phase["v"] = "hold" |
| for _ in range(40): |
| env.step(act2((Ltop+_CORRL["v"]).astype(np.float32), CLOSE, (Rtop+_CORRR["v"]).astype(np.float32), CLOSE)) |
| capture() |
|
|
| w = potw(); q = potq() |
| dz = float(w[2])-z0 |
| tilt = tilt_from_rest(q) |
| _RESULT["v"] = "SUCCESS" if (dz > 0.06 and tilt < 25.0) else "FAIL" |
| _phase["v"] = "done" |
| print(f"[pot] EPISODE_RESULT: {_RESULT['v']} dz={dz:+.3f} tilt={tilt:.1f}deg " |
| f"pot=({w[0]:.3f},{w[1]:.3f},{w[2]:.3f}) start_z={z0:.3f}", flush=True) |
| for _ in range(16): |
| capture() |
|
|
| os.makedirs(os.path.dirname(args.video), exist_ok=True) |
| |
| |
| if len(frames) > 6: |
| frames = frames[2:] |
| if frames: |
| imageio.mimsave(args.video, frames, fps=14) |
| print(f"[pot] video -> {args.video} ({len(frames)} frames)", flush=True) |
| env.close(); app.close(); print("YAM_POT_LIFT_OK", flush=True) |
|
|