File size: 7,421 Bytes
7399b6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f84c4af
7399b6f
 
 
 
 
 
 
 
 
 
74596d5
 
 
7399b6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f84c4af
 
 
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
"""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}