Download source/bimanual/yam/solvers/push.py from yqi19/mp_yam_code: direct link, hf CLI and curl.
- Browser
- Download file 7.42 kB
-
https://huggingface.co/yqi19/mp_yam_code/resolve/main/source/bimanual/yam/solvers/push.py
- Command line
-
hf download hf://yqi19/mp_yam_code/source/bimanual/yam/solvers/push.py
-
curl -L -o push.py https://huggingface.co/yqi19/mp_yam_code/resolve/main/source/bimanual/yam/solvers/push.py
7.42 kB
| """Push an object to a target WITHOUT grasping it: closed jaw as a finger, closed-loop servo.""" | |
| import numpy as np | |
| from ..motion.arm import OPEN, CLOSE | |
| from ..motion.planner import plan_path | |
| from ..envs.scene import TABLE_TOP | |
| # half-thickness of a closed gripper finger; the pusher contact point is offset by this much | |
| FINGER_HALF = 0.014 | |
| def relay(env, obj, legs, steps=220, **kw): | |
| """Two arms push the same block in turn, each along ITS OWN leg. | |
| `legs` is [{"arm": "right", "target": "waypoint"}, {"arm": "left", "target": "goal"}]. | |
| The earlier version pushed one straight line and simply handed over halfway, which does not | |
| work on this robot. To push a block in -y the hand must stand on its +y side, and the right | |
| arm's shoulder is at y=-0.2: it had to reach right over the block to get behind it, and the | |
| approach swiped the block off the table. | |
| An L-shaped route fixes it. Each arm only ever pushes AWAY from its own shoulder -- the right | |
| arm drives +x, the left arm drives -y -- so neither reaches past the block, and the corner | |
| between the legs is what makes two arms necessary rather than decorative. | |
| """ | |
| rec = env.recorder | |
| out = {} | |
| for i, leg in enumerate(legs): | |
| side, tgt = leg["arm"], leg["target"] | |
| rec.phase = f"{chr(65+i)}. {side.upper()} ARM pushes to {tgt}" | |
| print(f"[solver] relay leg {i+1}/{len(legs)}: {side} arm -> {tgt}", flush=True) | |
| out[f"leg{i+1}"] = solve(env, obj, tgt, arm=side, steps=steps, **kw) | |
| a = env.arms[side] | |
| rec.phase = f"{chr(65+i)}b. {side.upper()} ARM clears out of the way" | |
| a.flow([a._seg_start()+np.array([0, 0, 0.18], np.float32)], CLOSE) | |
| last = out[f"leg{len(legs)}"] | |
| return {**out, "pushed": last.get("pushed", False), "err": last.get("err")} | |
| def solve(env, obj, target, arm="right", steps=240, approach_gap=0.045, advance=0.009, | |
| stop_at_s=None, advance_max=0.010, push_z_frac=0.45, stop_dist=0.015): | |
| """Re-derive the pusher pose from the block's LIVE pose every step. | |
| Open-loop pushing fails the moment the block skids off the contact normal: the pusher keeps | |
| driving down its planned line while the block squirts away sideways. Servoing on the live | |
| pose keeps the finger on the block->target line the whole way. | |
| """ | |
| a = env.arms[arm] | |
| rec = env.recorder | |
| reg = env.scene.regions[target] | |
| ext = env.scene.object_size(obj) | |
| # Contact height matters for anything that is not a cube: pushing a dome at 45% of its | |
| # height is above its centre of mass, so it tips and rolls instead of sliding. | |
| push_z = TABLE_TOP+float(ext[2])*float(push_z_frac) | |
| w0 = env.scene.object_pos(obj) | |
| rec.phase = "1. CLOSE THE JAW (used as a finger)" | |
| for _ in range(40): | |
| a._drive(a._seg_start(), CLOSE) | |
| d = np.array([reg["xy"][0]-w0[0], reg["xy"][1]-w0[1]], float) | |
| n = d/(np.linalg.norm(d)+1e-9) | |
| behind = np.array([w0[0]-n[0]*(float(ext[0])/2+FINGER_HALF+approach_gap), | |
| w0[1]-n[1]*(float(ext[1])/2+FINGER_HALF+approach_gap), push_z], np.float32) | |
| # OVER THE TOP, then straight down. The contact point is on the far side of the block, so an | |
| # arm whose shoulder is on the target's side has to reach PAST the block to get there -- and | |
| # a planned path that cuts the corner swipes the block on the way in and launches it (it | |
| # ended 0.76 m off the table that way). A high traverse plus a vertical descent cannot. | |
| clear = a.to_root(behind+np.array([0, 0, 0.20], np.float32)) | |
| rec.phase = "2. RISE clear of the block" | |
| a.flow([a._seg_start()+np.array([0, 0, 0.16], np.float32)], CLOSE) | |
| rec.phase = "3. TRAVERSE over the block to the far side" | |
| a.move_to(clear, CLOSE, tol=0.012, max_steps=150) | |
| rec.phase = "4. LOWER to push height" | |
| reach = a.move_to(a.to_root(behind), CLOSE, tol=0.008, max_steps=130) | |
| if reach > 0.03: | |
| # Say so plainly: a block that never moves looks identical to a block that would not | |
| # slide, and only this number tells them apart. | |
| print(f"[solver] WARNING: the {arm} arm could not reach the pushing stance " | |
| f"(off by {reach:.3f} m at {np.round(behind, 3)}) -- it is out of its workspace, " | |
| f"so nothing will move.", flush=True) | |
| rec.phase = "4. PUSH (swept finger, lateral correction from the live block pose)" | |
| # The finger SWEEPS: its commanded point marches from `behind` to just short of the target at | |
| # a fixed rate, so it keeps displacing the block. Re-deriving the command from the block's | |
| # live pose each step instead (the obvious closed-loop form) makes the finger trail the block | |
| # by a constant stand-off and it creeps -- 2.5 cm in 240 steps, because a commanded point | |
| # that is always just-touching never actually advances into anything. | |
| back = float(ext[0])/2+FINGER_HALF | |
| p0 = behind[:2].astype(float) | |
| goal = np.array([reg["xy"][0], reg["xy"][1]], float)-n*back | |
| # The sweep is parameterised by CONTROL STEPS, not by distance. Sizing the loop as | |
| # span/advance conflated the two: it gave 25 iterations = 25 sim steps for a 22 cm push, so | |
| # the loop exited before the arm had traversed anything and the block "crept" 3 cm. | |
| ramp = max(1, int(steps*0.85)) | |
| for k in range(steps): | |
| w = env.scene.object_pos(obj) | |
| to_t = np.array([reg["xy"][0]-w[0], reg["xy"][1]-w[1]], float) | |
| dist = float(np.linalg.norm(to_t)) | |
| # A rolling object needs the push to END EARLY and let it coast in: keep driving | |
| # until the centre is on the target and a dome has already rolled past it. | |
| if dist < float(stop_dist): | |
| break | |
| # relay handoff: stop once the block has crossed into the other arm's half | |
| if stop_at_s is not None and float(np.dot(w[:2]-w0[:2], n)) >= stop_at_s: | |
| print(f"[solver] handoff: block passed s={stop_at_s:.3f} m after {k} steps", flush=True) | |
| break | |
| # RE-AIM every step: the contact point is `back` behind the block along the LIVE | |
| # block->target line, driven `bite` past it. Sweeping a line fixed at t=0 instead lets a | |
| # block that skids sideways stay skidded -- it ended 13 cm off in x that way -- because a | |
| # fixed line has no authority to steer the block back. | |
| nn = to_t/(dist+1e-9) | |
| # penetration is capped: 17 mm into a 25 mm half-block launched it 1.8 m off the table | |
| bite = min(advance, dist, advance_max) | |
| cp = w[:2]-nn*(back-bite) | |
| # blend toward the swept ideal so the finger keeps net forward progress even while the | |
| # block is momentarily stuck against static friction | |
| swept = p0+(goal-p0)*min(1.0, (k+1)/ramp) | |
| cp = cp+n*max(0.0, float(np.dot(swept-cp, n)))*0.25 | |
| a._drive(a.to_root(np.array([cp[0], cp[1], push_z], np.float32)), CLOSE) | |
| if k % 60 == 0: | |
| print(f"[solver] push k={k:3d} block=({w[0]:+.3f},{w[1]:+.3f}) dist={dist:.3f}", | |
| flush=True) | |
| wf = env.scene.object_pos(obj) | |
| err = float(np.hypot(wf[0]-reg["xy"][0], wf[1]-reg["xy"][1])) | |
| rec.phase = "5. RETREAT" | |
| a.flow([a._seg_start()+np.array([0, 0, 0.14], np.float32)], CLOSE) | |
| print(f"[solver] push done: block=({wf[0]:.3f},{wf[1]:.3f}) target=" | |
| f"({reg['xy'][0]:.3f},{reg['xy'][1]:.3f}) err={err:.3f}", flush=True) | |
| return {"pushed": err < 0.07, "err": err} | |