File size: 10,034 Bytes
24fe873
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Metric-only walk probe: reuse the env cfg but NO render context (headless GL
is broken on this host). Measures whether the policy WALKS LIKE A HUMAN, not just
moves. A human gait has a recognizable signature:

  1. Upright torso, sustained (no walk-then-fall, no wild sway).
  2. Alternating single-support with brief double-support (not both feet sliding,
     not hopping). Stance transfers left<->right at a human-like cadence.
  3. KNEE BENDS DURING STANCE -- humans flex the support knee to absorb load;
     a straight-leg shuffle is robot-like and rejected.
  4. Sagittal leg swing -- hips flex/extend fore-aft, not just splay sideways.
  5. Smooth forward progression at the COMMANDED speed (signed, along heading),
     with stable velocity (no lurching / trembling).
  6. Left/right symmetry (mirror_loss should enforce this).

Exit 0 = WALKS (human-like), 1 = no.
"""
import argparse, json, os
import torch
from dataclasses import asdict

import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from mjlab.envs import ManagerBasedRlEnv
from mjlab.rl import MjlabOnPolicyRunner, RslRlVecEnvWrapper
from mjlab.tasks.registry import load_env_cfg, load_rl_cfg, load_runner_cls

TASK = "Mjlab-Velocity-Flat-TwoLeg"
DUTY_FLOOR = 0.20          # a foot "loaded" if contact fraction > this
FRAME_ALT_CEIL = -0.20     # proj_gravity_b z < -0.20  => upright (z=-1 upright)
SPEED_TOL = 0.05           # min forward speed (signed, along heading)
MIN_VALID_FRAC = 0.60       # >=60% of envs must pass
MIN_SWITCH_HZ = 0.40       # stance must transfer L<->R at least this often
MIN_KNEE_FLEX_RAD = 0.35    # support/ swing knee must actually bend
MIN_HIP_SWING_RAD = 0.20    # legs must swing fore-aft
MIN_DOUBLE_SUPPORT = 0.10   # some double-support (human ~0.2-0.4); pure hop fails
MAX_DOUBLE_SUPPORT = 0.85   # but not mostly both-feet-sliding
MIN_STANCE_KNEE_BEND = 0.20 # knee bent WHILE that foot is the support foot
MAX_SPEED_CV = 0.60         # velocity stability: std/mean of forward speed

KNEE_JOINTS = ("L_knee_test", "R_knee_test")
HIP_JOINTS = ("L_hip_test", "R_hip_test")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--checkpoint", required=True)
    ap.add_argument("--command", type=float, default=0.1)
    ap.add_argument("--steps", type=int, default=300)
    ap.add_argument("--envs", type=int, default=4)
    ap.add_argument("--device", default="cuda:0")
    args = ap.parse_args()

    env_cfg = load_env_cfg(TASK, play=True)
    env_cfg.scene.num_envs = args.envs
    agent_cfg = load_rl_cfg(TASK)

    env = ManagerBasedRlEnv(cfg=env_cfg, device=args.device)
    venv = RslRlVecEnvWrapper(env)
    runner_cls = load_runner_cls(TASK) or MjlabOnPolicyRunner
    runner = runner_cls(venv, asdict(agent_cfg), device=args.device)
    runner.load(args.checkpoint, load_cfg={"actor": True}, strict=True,
                map_location=args.device)
    policy = runner.get_inference_policy(device=args.device)

    robot = env.scene["robot"]
    robot.data.forward_vec_b[:] = 0.0
    robot.data.forward_vec_b[:, 1] = -1.0  # facing -y on this model
    twist = env.command_manager.get_term("twist")
    step_dt = env.step_dt
    obs, _ = venv.reset()
    for _ in range(50):
        cmd = torch.zeros_like(twist.command)
        cmd[:, 1] = -args.command
        twist.command[:] = cmd
        with torch.no_grad():
            obs, _, _, _ = venv.step(policy(obs))

    joint_cols = {n: robot.joint_names.index(n) for n in KNEE_JOINTS + HIP_JOINTS}
    knee_cols = [joint_cols[n] for n in KNEE_JOINTS]
    hip_cols = [joint_cols[n] for n in HIP_JOINTS]

    P, H, C, U, K, A = [], [], [], [], [], []
    for _ in range(args.steps):
        cmd = torch.zeros_like(twist.command)
        cmd[:, 1] = -args.command
        twist.command[:] = cmd
        with torch.no_grad():
            obs, _, _, _ = venv.step(policy(obs))
        P.append(robot.data.root_link_pos_w[:, :2].clone())
        H.append(robot.data.heading_w.clone())
        C.append(env.scene["feet_ground_contact"].data.found[:].clone())
        U.append(robot.data.projected_gravity_b[:, 2].clone())
        jp = robot.data.joint_pos
        K.append(jp[:, knee_cols].clone())
        A.append(jp[:, hip_cols].clone())
    P, H, C = torch.stack(P), torch.stack(H), torch.stack(C)
    U, K, A = torch.stack(U), torch.stack(K), torch.stack(A)  # [steps, envs, legs]

    # --- forward travel along each env's own heading (signed) ---
    d = P[1:] - P[:-1]
    h = H[:-1]
    fwd = torch.stack([torch.cos(h), torch.sin(h)], -1)
    travel = (d * fwd).sum(-1).sum(0)
    dur = (P.shape[0] - 1) * step_dt
    speed = travel / dur  # [envs]

    # --- contact duty per foot ---
    found = C.squeeze(-1) if C.ndim == 4 else C  # [steps, envs, slots]
    left = found[:, :, 0]
    right = found[:, :, 1]
    left_duty = left.float().mean(0)
    right_duty = right.float().mean(0)
    contact_alt = (left_duty - right_duty).abs()

    # --- upright per step (z=-1 upright) ---
    up = U  # [steps, envs]
    last_frame_alt = up[-args.steps // 5:].mean(0) if args.steps // 5 > 0 else up.mean(0)

    # --- sustained upright rolling window (no walk-then-fall) ---
    WINDOW = max(10, args.steps // 5)
    upright_step = up < FRAME_ALT_CEIL
    n_steps = upright_step.shape[0]
    rolling_ok = torch.zeros(n_steps, args.envs, dtype=torch.bool, device=up.device)
    for s in range(n_steps):
        lo = max(0, s - WINDOW + 1)
        rolling_ok[s] = upright_step[lo:s + 1].all(dim=0)
    first_fail = torch.full((args.envs,), -1, dtype=torch.long)
    for e in range(args.envs):
        fails = (rolling_ok[:, e] == 0).nonzero(as_tuple=True)[0].cpu()
        if fails.numel() > 0:
            first_fail[e] = fails[0].item()

    # --- both feet share load ON AVERAGE (alternating contact ok) ---
    both_load_avg = ((left > DUTY_FLOOR).float().mean(0) > 0.5) & \
                   ((right > DUTY_FLOOR).float().mean(0) > 0.5)

    # --- stance transfer + double-support fraction ---
    stance = torch.where(left > 0, 0, torch.where(right > 0, 1, -1))
    switched = (stance[1:] != stance[:-1]) & (stance[1:] >= 0) & (stance[:-1] >= 0)
    switch_hz = switched.float().sum(0) / dur
    double_support = ((left > 0) & (right > 0)).float().mean(0)  # fraction of steps
    single_support_frac = ((left > 0) ^ (right > 0)).float().mean(0)

    # --- joint articulation: peak-to-peak over time, worse of two legs ---
    knee_flex = (K.amax(0) - K.amin(0)).min(1).values      # [envs]
    hip_swing = (A.amax(0) - A.amin(0)).min(1).values      # [envs]
    articulated = (knee_flex > MIN_KNEE_FLEX_RAD) & (hip_swing > MIN_HIP_SWING_RAD)

    # --- knee bent DURING stance (support knee flexes, human-like) ---
    # For each foot, average its knee angle while THAT foot is the support foot.
    l_knee = K[:, :, 0]; r_knee = K[:, :, 1]
    l_stance_knee = l_knee[left > 0].mean() if (left > 0).any() else torch.tensor(0.0)
    r_stance_knee = r_knee[right > 0].mean() if (right > 0).any() else torch.tensor(0.0)
    stance_knee_bend = min(l_stance_knee.item(), r_stance_knee.item())
    stance_knee_ok = stance_knee_bend > MIN_STANCE_KNEE_BEND

    # --- velocity smoothness (no lurching/trembling) ---
    speed_cv = speed.std().item() / (abs(speed.mean().item()) + 1e-6)

    # --- HUMAN-LIKE GATE: all must hold ---
    passed = (rolling_ok[-1] & both_load_avg & (speed > SPEED_TOL)
              & (switch_hz > MIN_SWITCH_HZ)
              & (double_support > MIN_DOUBLE_SUPPORT) & (double_support < MAX_DOUBLE_SUPPORT)
              & articulated
              & torch.tensor(stance_knee_ok)
              & (speed_cv < MAX_SPEED_CV))
    n_pass = int(passed.sum().item())
    valid_frac = n_pass / max(1, args.envs)

    if args.envs == 1:
        # cv needs a batch; for single-env use per-step speed std instead
        step_speed = (d * fwd).sum(-1)  # [steps-1]
        cv1 = step_speed.std().item() / (abs(step_speed.mean().item()) + 1e-6)
        speed_cv = cv1

    res = {
        "verdict": "WALKS" if valid_frac >= MIN_VALID_FRAC else "NO-WALK",
        "valid_frac": round(valid_frac, 3),
        "rolling_window": WINDOW,
        "rolling_pass_frac": round(rolling_ok[-1].float().mean().item(), 3),
        "first_fail_step": [int(x) for x in first_fail.tolist()],
        "left_duty_mean": round(left_duty.mean().item(), 3),
        "right_duty_mean": round(right_duty.mean().item(), 3),
        "speed_mean": round(speed.mean().item(), 3),
        "last_frame_alt_mean": round(last_frame_alt.mean().item(), 3),
        "contact_alt_mean": round(contact_alt.mean().item(), 3),
        "switch_hz_mean": round(float(switch_hz.mean().item()), 3),
        "double_support_frac": round(float(double_support.mean().item()), 3),
        "single_support_frac": round(float(single_support_frac.mean().item()), 3),
        "knee_flex_min_rad": round(float(knee_flex.mean().item()), 3),
        "hip_swing_min_rad": round(float(hip_swing.mean().item()), 3),
        "stance_knee_bend_rad": round(stance_knee_bend, 3),
        "speed_cv": round(float(speed_cv), 3),
        "articulated_frac": round(float(articulated.float().mean().item()), 3),
        "per_env": {
            "left_duty": [round(x, 3) for x in left_duty.tolist()],
            "right_duty": [round(x, 3) for x in right_duty.tolist()],
            "speed": [round(x, 3) for x in speed.tolist()],
            "switch_hz": [round(x, 3) for x in switch_hz.tolist()],
            "knee_flex_rad": [round(x, 3) for x in knee_flex.tolist()],
            "hip_swing_rad": [round(x, 3) for x in hip_swing.tolist()],
            "passed": [bool(x) for x in passed.tolist()],
        },
    }
    print(json.dumps(res, indent=2))
    out = os.path.join(os.path.dirname(args.checkpoint),
                       f"verify_metrics_cmd{args.command:.2f}.json")
    with open(out, "w") as fh:
        json.dump(res, fh, indent=2)
    print("WROTE", out)
    sys.exit(0 if res["verdict"] == "WALKS" else 1)


if __name__ == "__main__":
    main()