File size: 18,856 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 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | """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 # noqa: F401 (kept for parity with the other demos)
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 # scripted sequence: never auto-reset mid-run
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.]) # jaw axis: across the handle
Z = np.array([0., -sign*s, -c]) # approach: down, leaning toward the centre
Y = np.cross(Z, X)
return qR(np.stack([X, Y, Z], axis=1))
if args.grip_tilt != 0.0:
gq = tilted_grasp(+1) # replaced per-arm below
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 # left arm sits at +y
GQ_R = tilted_grasp(-1) if args.grip_tilt != 0.0 else gq # right arm sits at -y
def act2(lp, lg, rp, rg):
# each arm carries its own grasp orientation so the two hands can lean in symmetrically
return torch.tensor(np.concatenate([lp, GQ_L, [lg], rp, GQ_R, [rg]]),
dtype=torch.float32, device=dev).view(1, -1)
# ---- place the pot, then measure it ------------------------------------------------------
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)
# Handles stick out along the pot's LONG horizontal axis. The two arms work at -y (right) and
# +y (left), so if the long axis is x, yaw the pot 90 deg to face the handles at the arms.
# Re-seat it just above the table first: dropping a tall pot from spawn height topples it before
# the arms ever reach it. Only once it is settled is the bbox meaningful, so the handle-facing
# yaw is decided from THAT measurement (deciding from the mid-air bbox picked the wrong axis).
_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())
# handles lie along the pot's LONG horizontal axis; the arms work at -y (right) and +y (left)
_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))
# BBoxCache reads the AUTHORED transform, not the physics pose, so re-measuring after the
# yaw returns the same numbers. Swap the horizontal extents by hand instead -- otherwise the
# arms aim 5.8 cm from centre (inside the pot body) rather than at the 12.6 cm handle tips.
ext = np.array([ext[1], ext[0], ext[2]])
_q0 = pot.data.root_quat_w[0].cpu().numpy()
# "Upright" is measured against the pose it SETTLED in, not against world +z: a RoboTwin mesh
# needs a 90 deg roll to stand up, so its local +z is horizontal by construction and comparing
# to world +z reports a perfectly upright pot as "tilted 90 degrees".
_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 the handle tips: just inside the y extremes, at the pot's upper body where the handles are.
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 # left arm -> +y handle
Ry = pw[1] - _hy_r + args.grip_inset # right arm -> -y handle
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
# ---- overlay + smooth two-arm executor ---------------------------------------------------
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
# A jaw that stops at a WIDE opening has hit the pot body, not a handle -- accepting that as
# "grasped" is how an episode ends up dangling from one lid pinch while the other hand hangs
# empty and the numeric check still reports success. Require a plausible handle-sized gap.
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) # how far it rolled AWAY from how it was resting, not from world +z
_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)
# 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"[pot] video -> {args.video} ({len(frames)} frames)", flush=True)
env.close(); app.close(); print("YAM_POT_LIFT_OK", flush=True)
|