Compare commits
2 Commits
6882f1abd8
...
300a6ff5bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
300a6ff5bf | ||
|
|
29de5eb8de |
738
deploy_45dim_rl_gym/deploy_go1_onnx_mujoco_lab.py
Normal file
738
deploy_45dim_rl_gym/deploy_go1_onnx_mujoco_lab.py
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Go1 RobotLab ONNX Policy - MuJoCo Simulation Deployment.
|
||||||
|
|
||||||
|
Loads the RobotLab ONNX policy exported from RoboGauge and runs it in MuJoCo
|
||||||
|
with PD position control. The ONNX model uses a 10-frame history
|
||||||
|
(450-dim stacked-by-terms input, stateless).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
conda activate free_dog_sdk
|
||||||
|
MUJOCO_GL=glfw mjpython deploy_go1_onnx_mujoco_lab.py
|
||||||
|
MUJOCO_GL=glfw mjpython deploy_go1_onnx_mujoco_lab.py --terrain terrains/stairs/stairs_6.xml
|
||||||
|
|
||||||
|
Controls (matching RoboGauge keyboard convention):
|
||||||
|
↑ / ↓ forward / backward
|
||||||
|
← / → yaw left / right
|
||||||
|
, / . strafe left / right
|
||||||
|
K stop
|
||||||
|
R reset robot
|
||||||
|
Esc quit
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
ONNX input: obs [1, 450] - 10 frames x 45 dims, stacked by TERMS
|
||||||
|
ONNX output: actions [1, 12]
|
||||||
|
Control: target_q = default_q + 0.25 * action, PD with Kp=28, Kd=0.7
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import mujoco
|
||||||
|
import numpy as np
|
||||||
|
import onnxruntime as ort
|
||||||
|
from mujoco import viewer
|
||||||
|
|
||||||
|
# ── path setup ──
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
DEFAULT_ONNX = str(SCRIPT_DIR / "policy_robotlab_6500.onnx")
|
||||||
|
ROBOT_XML = str(SCRIPT_DIR / "go1.xml")
|
||||||
|
TERRAINS_DIR = SCRIPT_DIR / "terrains"
|
||||||
|
|
||||||
|
# ── policy constants (RoboGauge Go1Config) ──
|
||||||
|
NUM_OBS = 45
|
||||||
|
NUM_ACTIONS = 12
|
||||||
|
HISTORY_LEN = 10 # RobotLab ONNX history frames
|
||||||
|
ONNX_INPUT_DIM = 450 # 45 x 10, stacked by terms
|
||||||
|
|
||||||
|
ACTION_SCALE = 0.25
|
||||||
|
KP = 28.0
|
||||||
|
KD = 0.7
|
||||||
|
CLIP_ACTIONS = 100.0
|
||||||
|
CLIP_OBS = 100.0
|
||||||
|
|
||||||
|
ANG_VEL_SCALE = 0.25
|
||||||
|
CMD_SCALE = np.array([1.0, 1.0, 1.0], dtype=np.float32)
|
||||||
|
|
||||||
|
MAX_LIN_VEL_X = 1.0
|
||||||
|
MAX_LIN_VEL_Y = 0.5
|
||||||
|
MAX_ANG_VEL = 1.0
|
||||||
|
|
||||||
|
DEFAULT_DOF_POS = np.array([
|
||||||
|
-0.1, 0.8, -1.5, # FR_hip, FR_thigh, FR_calf
|
||||||
|
0.1, 0.8, -1.5, # FL_hip, FL_thigh, FL_calf
|
||||||
|
-0.1, 1.0, -1.5, # RR_hip, RR_thigh, RR_calf
|
||||||
|
0.1, 1.0, -1.5, # RL_hip, RL_thigh, RL_calf
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
# ── exit flag ──
|
||||||
|
EXIT = False
|
||||||
|
|
||||||
|
def _sig_handler(signum, frame):
|
||||||
|
global EXIT
|
||||||
|
EXIT = True
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, _sig_handler)
|
||||||
|
signal.signal(signal.SIGTERM, _sig_handler)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# XML builder — merge terrain + robot into one MuJoCo scene
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _xml_inner(text, tag):
|
||||||
|
"""Extract inner content of the first <tag>...</tag> in text."""
|
||||||
|
m = re.search(rf"<{tag}>(.*?)</{tag}>", text, re.DOTALL)
|
||||||
|
return m.group(1).strip() if m else ""
|
||||||
|
|
||||||
|
|
||||||
|
def build_scene_xml(robot_xml_path, terrain_xml_path=None):
|
||||||
|
"""Merge robot XML with optional terrain XML into a single scene model.
|
||||||
|
|
||||||
|
The robot XML (go1.xml) has base_link as the root body with no free joint.
|
||||||
|
We add a free joint and optionally merge terrain worldbody/asset elements.
|
||||||
|
"""
|
||||||
|
robot = Path(robot_xml_path).read_text()
|
||||||
|
|
||||||
|
# 1) Add free joint to base_link
|
||||||
|
robot = robot.replace(
|
||||||
|
'<body name="base_link" pos="0 0 0.35">',
|
||||||
|
'<body name="base_link" pos="0 0 0.35">\n <joint type="free"/>'
|
||||||
|
)
|
||||||
|
|
||||||
|
if terrain_xml_path is None:
|
||||||
|
# No terrain — add a simple flat floor
|
||||||
|
floor = (
|
||||||
|
'\n <geom name="floor" size="0 0 0.05" type="plane" '
|
||||||
|
'rgba="0.5 0.9 0.9 0.1"/>\n'
|
||||||
|
)
|
||||||
|
robot = robot.replace(
|
||||||
|
'<body name="base_link"',
|
||||||
|
floor + ' <body name="base_link"'
|
||||||
|
)
|
||||||
|
return robot
|
||||||
|
|
||||||
|
terrain_text = Path(terrain_xml_path).read_text()
|
||||||
|
|
||||||
|
# 2) Merge terrain <asset> into robot <asset>
|
||||||
|
terrain_assets = _xml_inner(terrain_text, "asset")
|
||||||
|
if terrain_assets:
|
||||||
|
# Insert before closing </asset> of robot (or before <worldbody> if no asset)
|
||||||
|
robot = robot.replace('</asset>', '\n' + terrain_assets + '\n </asset>', 1)
|
||||||
|
|
||||||
|
# 3) Merge terrain <visual> settings
|
||||||
|
terrain_visual = _xml_inner(terrain_text, "visual")
|
||||||
|
if terrain_visual:
|
||||||
|
robot = robot.replace('</visual>', '\n' + terrain_visual + '\n </visual>', 1)
|
||||||
|
|
||||||
|
# 4) Merge terrain worldbody elements (lights, geoms, etc.) before base_link
|
||||||
|
terrain_wb = _xml_inner(terrain_text, "worldbody")
|
||||||
|
if terrain_wb:
|
||||||
|
# Remove <body> elements from terrain worldbody (we only want geoms/lights/cameras)
|
||||||
|
terrain_wb_no_bodies = re.sub(r'<body\b.*?</body>', '', terrain_wb, flags=re.DOTALL)
|
||||||
|
robot = robot.replace(
|
||||||
|
'<body name="base_link"',
|
||||||
|
terrain_wb_no_bodies.strip() + '\n <body name="base_link"'
|
||||||
|
)
|
||||||
|
|
||||||
|
return robot
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Observation building
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def get_projected_gravity(quat_wxyz):
|
||||||
|
qw, qx, qy, qz = quat_wxyz
|
||||||
|
g = np.zeros(3, dtype=np.float32)
|
||||||
|
g[0] = 2.0 * (-qz * qx + qw * qy)
|
||||||
|
g[1] = -2.0 * (qz * qy + qw * qx)
|
||||||
|
g[2] = 1.0 - 2.0 * (qw * qw + qz * qz)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def quat_rotate_inverse(q_wxyz, v):
|
||||||
|
"""Rotate vector v from world frame into body frame using quaternion q."""
|
||||||
|
q_w = q_wxyz[0]
|
||||||
|
q_vec = np.array(q_wxyz[1:], dtype=np.float32)
|
||||||
|
v = np.array(v, dtype=np.float32)
|
||||||
|
a = v * (2.0 * q_w * q_w - 1.0)
|
||||||
|
b = np.cross(q_vec, v) * q_w * 2.0
|
||||||
|
c = q_vec * np.dot(q_vec, v) * 2.0
|
||||||
|
return a - b + c
|
||||||
|
|
||||||
|
|
||||||
|
def quat_to_rpy_deg(q_wxyz):
|
||||||
|
"""Convert MuJoCo [w, x, y, z] quaternion to roll/pitch/yaw in degrees."""
|
||||||
|
qw, qx, qy, qz = [float(x) for x in q_wxyz]
|
||||||
|
sinr_cosp = 2.0 * (qw * qx + qy * qz)
|
||||||
|
cosr_cosp = 1.0 - 2.0 * (qx * qx + qy * qy)
|
||||||
|
roll = np.arctan2(sinr_cosp, cosr_cosp)
|
||||||
|
|
||||||
|
sinp = 2.0 * (qw * qy - qz * qx)
|
||||||
|
pitch = np.arcsin(np.clip(sinp, -1.0, 1.0))
|
||||||
|
|
||||||
|
siny_cosp = 2.0 * (qw * qz + qx * qy)
|
||||||
|
cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz)
|
||||||
|
yaw = np.arctan2(siny_cosp, cosy_cosp)
|
||||||
|
return np.degrees(np.array([roll, pitch, yaw], dtype=np.float32))
|
||||||
|
|
||||||
|
|
||||||
|
def read_sensor(model, data, name, expected_dim):
|
||||||
|
"""Read a MuJoCo sensor by name, returning None if it is not present."""
|
||||||
|
sid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, name)
|
||||||
|
if sid < 0:
|
||||||
|
return None
|
||||||
|
adr = int(model.sensor_adr[sid])
|
||||||
|
dim = int(model.sensor_dim[sid])
|
||||||
|
if dim != expected_dim:
|
||||||
|
raise ValueError(f"sensor {name!r} has dim {dim}, expected {expected_dim}")
|
||||||
|
return np.asarray(data.sensordata[adr:adr + dim], dtype=np.float32).copy()
|
||||||
|
|
||||||
|
|
||||||
|
class ObsBuilder:
|
||||||
|
"""Build 45-dim single-frame obs and stack 10-frame history into 450-dim ONNX input.
|
||||||
|
|
||||||
|
ONNX expects observation terms stacked by groups (not by frames):
|
||||||
|
[ang_vel(t-9..t), gravity(t-9..t), cmd(t-9..t),
|
||||||
|
dof_pos(t-9..t), dof_vel(t-9..t), last_action(t-9..t)]
|
||||||
|
Within each group, frames go from oldest (t-9) to newest (t).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.history: deque = deque(maxlen=HISTORY_LEN)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.history.clear()
|
||||||
|
|
||||||
|
def build_single_obs(self, base_ang_vel_body, base_quat_wxyz, cmd, q, dq, last_action):
|
||||||
|
"""Build 45-dim single-frame observation (RoboGauge layout)."""
|
||||||
|
obs = np.zeros(NUM_OBS, dtype=np.float32)
|
||||||
|
obs[0:3] = np.asarray(base_ang_vel_body, dtype=np.float32) * ANG_VEL_SCALE
|
||||||
|
obs[3:6] = get_projected_gravity(np.asarray(base_quat_wxyz, dtype=np.float32))
|
||||||
|
obs[6:9] = np.asarray(cmd, dtype=np.float32) * CMD_SCALE
|
||||||
|
obs[9:21] = np.asarray(q, dtype=np.float32) - DEFAULT_DOF_POS
|
||||||
|
obs[21:33] = np.asarray(dq, dtype=np.float32) * 0.05
|
||||||
|
obs[33:45] = np.asarray(last_action, dtype=np.float32)
|
||||||
|
return np.clip(obs, -CLIP_OBS, CLIP_OBS)
|
||||||
|
|
||||||
|
def build_onnx_input(self, obs_single):
|
||||||
|
"""Stack 10-frame history into 450-dim ONNX input (by-terms format)."""
|
||||||
|
self.history.append(obs_single.copy())
|
||||||
|
|
||||||
|
frames = list(self.history)
|
||||||
|
while len(frames) < HISTORY_LEN:
|
||||||
|
frames.insert(0, np.zeros(NUM_OBS, dtype=np.float32))
|
||||||
|
|
||||||
|
# term_dims: [ang_vel(3), gravity(3), cmd(3), dof_pos(12), dof_vel(12), last_action(12)]
|
||||||
|
term_dims = [3, 3, 3, 12, 12, 12]
|
||||||
|
|
||||||
|
stacked = []
|
||||||
|
offset = 0
|
||||||
|
for dim in term_dims:
|
||||||
|
for f_idx in range(HISTORY_LEN):
|
||||||
|
stacked.append(frames[f_idx][offset:offset + dim])
|
||||||
|
offset += dim
|
||||||
|
|
||||||
|
return np.concatenate(stacked, dtype=np.float32).reshape(1, -1)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Keyboard input (pynput)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class KbReader:
|
||||||
|
"""Non-blocking keyboard reader using a background pynput listener.
|
||||||
|
|
||||||
|
Key names match RoboGauge convention:
|
||||||
|
key.up / key.down → forward/back
|
||||||
|
key.left / key.right → yaw
|
||||||
|
, / . → strafe left/right
|
||||||
|
k → stop
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
self._q = queue.Queue()
|
||||||
|
self.held = set()
|
||||||
|
self._running = True
|
||||||
|
self._listener = None
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _name(key):
|
||||||
|
try:
|
||||||
|
if hasattr(key, 'char') and key.char is not None:
|
||||||
|
return key.char.lower()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return str(key).lower()
|
||||||
|
|
||||||
|
def _worker(self):
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
evt, key = self._q.get(timeout=0.05)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
name = self._name(key)
|
||||||
|
if evt == 'press':
|
||||||
|
self.held.add(name)
|
||||||
|
else:
|
||||||
|
self.held.discard(name)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
from pynput import keyboard
|
||||||
|
self._listener = keyboard.Listener(
|
||||||
|
on_press=lambda k: self._q.put(('press', k)),
|
||||||
|
on_release=lambda k: self._q.put(('release', k)),
|
||||||
|
)
|
||||||
|
self._listener.start()
|
||||||
|
import threading
|
||||||
|
self._thread = threading.Thread(target=self._worker, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
print("[INFO] keyboard started (↑↓←→ , . K R Esc)")
|
||||||
|
|
||||||
|
def is_held(self, key):
|
||||||
|
return self._name(key) in self.held
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
return set(self.held)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
if self._listener:
|
||||||
|
self._listener.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def get_command(held):
|
||||||
|
"""Extract velocity command from held key set.
|
||||||
|
|
||||||
|
Returns (vx, vy, yaw_rate).
|
||||||
|
"""
|
||||||
|
if 'k' in held:
|
||||||
|
return 0.0, 0.0, 0.0
|
||||||
|
|
||||||
|
def axis(pos_key, neg_key, limit):
|
||||||
|
p = pos_key in held
|
||||||
|
n = neg_key in held
|
||||||
|
if p == n:
|
||||||
|
return 0.0
|
||||||
|
return limit if p else -limit
|
||||||
|
|
||||||
|
vx = axis('key.up', 'key.down', MAX_LIN_VEL_X)
|
||||||
|
vy = axis(',', '.', MAX_LIN_VEL_Y)
|
||||||
|
yaw = axis('key.left', 'key.right', MAX_ANG_VEL)
|
||||||
|
return vx, vy, yaw
|
||||||
|
|
||||||
|
|
||||||
|
class SampleLogger:
|
||||||
|
def __init__(self, log_dir, terrain_path, cmd, args):
|
||||||
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
self.run_dir = Path(log_dir).expanduser().resolve() / f"mujoco_stairs_sample_{ts}"
|
||||||
|
self.run_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.fp = open(self.run_dir / "steps.jsonl", "a", encoding="utf-8", buffering=1)
|
||||||
|
meta = {
|
||||||
|
"created_at": ts,
|
||||||
|
"terrain": str(terrain_path) if terrain_path else "flat",
|
||||||
|
"sample_seconds": args.sample_seconds,
|
||||||
|
"sample_cmd": list(cmd),
|
||||||
|
"spawn": [args.spawn_x, args.spawn_y, args.spawn_z],
|
||||||
|
"action_scale": ACTION_SCALE,
|
||||||
|
"clip_actions": args.clip_actions,
|
||||||
|
"max_target_step": args.max_target_step,
|
||||||
|
"kp": KP,
|
||||||
|
"kd": KD,
|
||||||
|
"control_dt": 0.02,
|
||||||
|
"sim_dt": 0.002,
|
||||||
|
"num_obs": NUM_OBS,
|
||||||
|
"history_len": HISTORY_LEN,
|
||||||
|
"onnx_input_dim": ONNX_INPUT_DIM,
|
||||||
|
}
|
||||||
|
(self.run_dir / "metadata.json").write_text(
|
||||||
|
json.dumps(meta, indent=2, ensure_ascii=False))
|
||||||
|
print(f"[INFO] sample log dir: {self.run_dir}")
|
||||||
|
|
||||||
|
def write(self, rec):
|
||||||
|
self.fp.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.fp.flush()
|
||||||
|
self.fp.close()
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_samples(samples):
|
||||||
|
if not samples:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def max_abs_field(field):
|
||||||
|
return float(max(max(abs(x) for x in s[field]) for s in samples))
|
||||||
|
|
||||||
|
def p99_abs_field(field):
|
||||||
|
vals = sorted(abs(x) for s in samples for x in s[field])
|
||||||
|
if not vals:
|
||||||
|
return 0.0
|
||||||
|
idx = min(len(vals) - 1, int(0.99 * (len(vals) - 1)))
|
||||||
|
return float(vals[idx])
|
||||||
|
|
||||||
|
rpy = np.array([s["rpy_deg"] for s in samples], dtype=np.float32)
|
||||||
|
xyz = np.array([s["base_pos"] for s in samples], dtype=np.float32)
|
||||||
|
return {
|
||||||
|
"num_samples": len(samples),
|
||||||
|
"duration": float(samples[-1]["time_sim"] - samples[0]["time_sim"]),
|
||||||
|
"base_x_start": float(xyz[0, 0]),
|
||||||
|
"base_x_end": float(xyz[-1, 0]),
|
||||||
|
"base_x_progress": float(xyz[-1, 0] - xyz[0, 0]),
|
||||||
|
"base_z_min": float(np.min(xyz[:, 2])),
|
||||||
|
"base_z_max": float(np.max(xyz[:, 2])),
|
||||||
|
"rpy_min_deg": np.min(rpy, axis=0).astype(float).tolist(),
|
||||||
|
"rpy_max_deg": np.max(rpy, axis=0).astype(float).tolist(),
|
||||||
|
"action_raw_maxabs": max_abs_field("action_raw"),
|
||||||
|
"action_raw_p99abs": p99_abs_field("action_raw"),
|
||||||
|
"action_applied_maxabs": max_abs_field("action"),
|
||||||
|
"action_applied_p99abs": p99_abs_field("action"),
|
||||||
|
"target_offset_maxabs": max_abs_field("target_offset"),
|
||||||
|
"target_offset_p99abs": p99_abs_field("target_offset"),
|
||||||
|
"dof_vel_maxabs": max_abs_field("dof_vel"),
|
||||||
|
"torque_maxabs": max_abs_field("torques"),
|
||||||
|
"fallen": bool(any(s["fallen"] for s in samples)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Main
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Go1 RobotLab ONNX MuJoCo Deployment")
|
||||||
|
parser.add_argument("--onnx", type=str, default=DEFAULT_ONNX, help="ONNX model path")
|
||||||
|
parser.add_argument("--terrain", type=str, default=None,
|
||||||
|
help="Terrain XML path (e.g. terrains/stairs/stairs_6.xml)")
|
||||||
|
parser.add_argument("--sample", action="store_true",
|
||||||
|
help="Run a non-interactive fixed-command sampling rollout")
|
||||||
|
parser.add_argument("--sample-stairs-level", type=int, default=None,
|
||||||
|
help="Shortcut for --sample --terrain terrains/stairs/stairs_LEVEL.xml")
|
||||||
|
parser.add_argument("--sample-seconds", type=float, default=20.0)
|
||||||
|
parser.add_argument("--sample-cmd-x", type=float, default=MAX_LIN_VEL_X)
|
||||||
|
parser.add_argument("--sample-cmd-y", type=float, default=0.0)
|
||||||
|
parser.add_argument("--sample-cmd-yaw", type=float, default=0.0)
|
||||||
|
parser.add_argument("--sample-log-dir", type=str, default=str(SCRIPT_DIR / "mujoco_logs"))
|
||||||
|
parser.add_argument("--spawn-x", type=float, default=None)
|
||||||
|
parser.add_argument("--spawn-y", type=float, default=None)
|
||||||
|
parser.add_argument("--spawn-z", type=float, default=None)
|
||||||
|
parser.add_argument("--clip-actions", type=float, default=CLIP_ACTIONS,
|
||||||
|
help="Clip ONNX actions before applying action_scale")
|
||||||
|
parser.add_argument("--max-target-step", type=float, default=0.0,
|
||||||
|
help="Optional per-control-step target q slew limit in rad")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
sample_mode = args.sample or args.sample_stairs_level is not None
|
||||||
|
if args.sample_stairs_level is not None:
|
||||||
|
args.sample = True
|
||||||
|
args.terrain = f"terrains/stairs/stairs_{args.sample_stairs_level}.xml"
|
||||||
|
|
||||||
|
if args.spawn_x is None:
|
||||||
|
args.spawn_x = -0.6 if args.sample_stairs_level is not None else 0.0
|
||||||
|
if args.spawn_y is None:
|
||||||
|
args.spawn_y = 0.0
|
||||||
|
if args.spawn_z is None:
|
||||||
|
args.spawn_z = 0.34
|
||||||
|
args.sample_log_dir = str(Path(args.sample_log_dir).expanduser().resolve())
|
||||||
|
|
||||||
|
# Resolve terrain path
|
||||||
|
terrain_path = None
|
||||||
|
if args.terrain:
|
||||||
|
terrain_path = Path(args.terrain)
|
||||||
|
if not terrain_path.is_absolute():
|
||||||
|
terrain_path = SCRIPT_DIR / terrain_path
|
||||||
|
if not terrain_path.exists():
|
||||||
|
print(f"[ERROR] terrain not found: {terrain_path}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# ── Build scene XML ──
|
||||||
|
print(f"[INFO] robot: {ROBOT_XML}")
|
||||||
|
print(f"[INFO] terrain: {terrain_path or 'flat floor'}")
|
||||||
|
scene_xml = build_scene_xml(ROBOT_XML, str(terrain_path) if terrain_path else None)
|
||||||
|
|
||||||
|
# ── Load MuJoCo model ──
|
||||||
|
os.chdir(str(SCRIPT_DIR)) # meshdir="assets" is relative to go1.xml
|
||||||
|
model = mujoco.MjModel.from_xml_string(scene_xml)
|
||||||
|
data = mujoco.MjData(model)
|
||||||
|
model.opt.timestep = 0.002
|
||||||
|
print(f"[INFO] model: {model.nbody} bodies, qpos={model.nq}, actuators={model.nu}")
|
||||||
|
print(f"[INFO] timestep: {model.opt.timestep}s")
|
||||||
|
has_imu_gyro = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, "Body_Gyro") >= 0
|
||||||
|
has_imu_quat = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, "Body_Quat") >= 0
|
||||||
|
print(f"[INFO] IMU sensors: Body_Gyro={has_imu_gyro}, Body_Quat={has_imu_quat}")
|
||||||
|
|
||||||
|
# ── Load ONNX ──
|
||||||
|
session = ort.InferenceSession(args.onnx, providers=['CPUExecutionProvider'])
|
||||||
|
inp = session.get_inputs()[0]
|
||||||
|
print(f"[INFO] ONNX input : {inp.name} {inp.shape}")
|
||||||
|
for o in session.get_outputs():
|
||||||
|
print(f"[INFO] ONNX output: {o.name} {o.shape}")
|
||||||
|
|
||||||
|
# ── Init state ──
|
||||||
|
data.qpos[0:3] = np.array([args.spawn_x, args.spawn_y, args.spawn_z], dtype=np.float64)
|
||||||
|
data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64)
|
||||||
|
data.qpos[7:19] = DEFAULT_DOF_POS.astype(np.float64)
|
||||||
|
data.qvel[:] = 0.0
|
||||||
|
data.ctrl[:] = 0.0
|
||||||
|
mujoco.mj_forward(model, data)
|
||||||
|
|
||||||
|
# ── Control state ──
|
||||||
|
ctrl_dt = 0.02 # 50 Hz
|
||||||
|
sim_dt = model.opt.timestep
|
||||||
|
steps_per_inference = max(1, int(ctrl_dt / sim_dt))
|
||||||
|
print(f"[INFO] control: {ctrl_dt}s ({1/ctrl_dt:.0f}Hz), "
|
||||||
|
f"sim steps per inference: {steps_per_inference}")
|
||||||
|
|
||||||
|
last_action = np.zeros(NUM_ACTIONS, dtype=np.float32)
|
||||||
|
action_raw = np.zeros(NUM_ACTIONS, dtype=np.float32)
|
||||||
|
action = np.zeros(NUM_ACTIONS, dtype=np.float32)
|
||||||
|
target_pos = DEFAULT_DOF_POS.copy()
|
||||||
|
prev_target_pos = DEFAULT_DOF_POS.copy()
|
||||||
|
obs_builder = ObsBuilder()
|
||||||
|
step_count = 0
|
||||||
|
inference_step = 0
|
||||||
|
|
||||||
|
if sample_mode:
|
||||||
|
cmd = np.array([
|
||||||
|
args.sample_cmd_x,
|
||||||
|
args.sample_cmd_y,
|
||||||
|
args.sample_cmd_yaw,
|
||||||
|
], dtype=np.float32)
|
||||||
|
cmd = np.clip(cmd, [-MAX_LIN_VEL_X, -MAX_LIN_VEL_Y, -MAX_ANG_VEL],
|
||||||
|
[MAX_LIN_VEL_X, MAX_LIN_VEL_Y, MAX_ANG_VEL])
|
||||||
|
logger = SampleLogger(args.sample_log_dir, terrain_path, cmd, args)
|
||||||
|
samples = []
|
||||||
|
max_sim_steps = int(args.sample_seconds / sim_dt)
|
||||||
|
print(
|
||||||
|
f"[INFO] sampling: seconds={args.sample_seconds:.1f} "
|
||||||
|
f"cmd=({cmd[0]:.2f},{cmd[1]:.2f},{cmd[2]:.2f}) "
|
||||||
|
f"spawn=({args.spawn_x:.2f},{args.spawn_y:.2f},{args.spawn_z:.2f})"
|
||||||
|
)
|
||||||
|
|
||||||
|
while step_count < max_sim_steps and not EXIT:
|
||||||
|
if inference_step == 0:
|
||||||
|
imu_quat = read_sensor(model, data, "Body_Quat", 4)
|
||||||
|
imu_ang_vel = read_sensor(model, data, "Body_Gyro", 3)
|
||||||
|
quat_wxyz = imu_quat if imu_quat is not None else data.qpos[3:7].copy()
|
||||||
|
if imu_ang_vel is not None:
|
||||||
|
base_ang_vel_body = imu_ang_vel
|
||||||
|
else:
|
||||||
|
world_ang_vel = data.qvel[3:6].copy()
|
||||||
|
base_ang_vel_body = quat_rotate_inverse(quat_wxyz, world_ang_vel)
|
||||||
|
|
||||||
|
q = data.qpos[7:19].copy()
|
||||||
|
dq = data.qvel[6:18].copy()
|
||||||
|
obs_single = obs_builder.build_single_obs(
|
||||||
|
base_ang_vel_body, quat_wxyz, cmd, q, dq, last_action)
|
||||||
|
onnx_input = obs_builder.build_onnx_input(obs_single)
|
||||||
|
outputs = session.run(None, {'obs': onnx_input})
|
||||||
|
action_raw = outputs[0][0].astype(np.float32)
|
||||||
|
action = np.clip(action_raw, -args.clip_actions, args.clip_actions)
|
||||||
|
last_action = action.copy()
|
||||||
|
|
||||||
|
target_pos_raw = DEFAULT_DOF_POS + action * ACTION_SCALE
|
||||||
|
if args.max_target_step > 0.0:
|
||||||
|
delta = np.clip(
|
||||||
|
target_pos_raw - prev_target_pos,
|
||||||
|
-args.max_target_step,
|
||||||
|
args.max_target_step,
|
||||||
|
)
|
||||||
|
target_pos = prev_target_pos + delta
|
||||||
|
else:
|
||||||
|
target_pos = target_pos_raw
|
||||||
|
prev_target_pos = target_pos.copy()
|
||||||
|
|
||||||
|
current_pos = data.qpos[7:19]
|
||||||
|
current_vel = data.qvel[6:18]
|
||||||
|
torques = KP * (target_pos - current_pos) - KD * current_vel
|
||||||
|
torques = np.clip(torques, -33.5, 33.5)
|
||||||
|
data.ctrl[:] = torques.astype(np.float64)
|
||||||
|
|
||||||
|
mujoco.mj_step(model, data)
|
||||||
|
|
||||||
|
if inference_step == 0:
|
||||||
|
quat_wxyz = data.qpos[3:7].copy()
|
||||||
|
rpy_deg = quat_to_rpy_deg(quat_wxyz)
|
||||||
|
fallen = bool(
|
||||||
|
data.qpos[2] < 0.16
|
||||||
|
or abs(rpy_deg[0]) > 60.0
|
||||||
|
or abs(rpy_deg[1]) > 60.0
|
||||||
|
or not np.all(np.isfinite(data.qpos))
|
||||||
|
)
|
||||||
|
rec = {
|
||||||
|
"step": int(step_count),
|
||||||
|
"control_step": int(step_count // steps_per_inference),
|
||||||
|
"time_sim": float(data.time),
|
||||||
|
"cmd": cmd.astype(float).tolist(),
|
||||||
|
"base_pos": data.qpos[0:3].astype(float).tolist(),
|
||||||
|
"base_quat": quat_wxyz.astype(float).tolist(),
|
||||||
|
"rpy_deg": rpy_deg.astype(float).tolist(),
|
||||||
|
"base_lin_vel": data.qvel[0:3].astype(float).tolist(),
|
||||||
|
"base_ang_vel": data.qvel[3:6].astype(float).tolist(),
|
||||||
|
"dof_pos": current_pos.astype(float).tolist(),
|
||||||
|
"dof_vel": current_vel.astype(float).tolist(),
|
||||||
|
"action_raw": action_raw.astype(float).tolist(),
|
||||||
|
"action": action.astype(float).tolist(),
|
||||||
|
"target_pos": target_pos.astype(float).tolist(),
|
||||||
|
"target_offset": (target_pos - DEFAULT_DOF_POS).astype(float).tolist(),
|
||||||
|
"torques": torques.astype(float).tolist(),
|
||||||
|
"fallen": fallen,
|
||||||
|
}
|
||||||
|
logger.write(rec)
|
||||||
|
samples.append(rec)
|
||||||
|
if rec["control_step"] % 50 == 0:
|
||||||
|
print(
|
||||||
|
f"[sample {rec['control_step']:04d}] "
|
||||||
|
f"t={rec['time_sim']:.2f} x={rec['base_pos'][0]:.2f} "
|
||||||
|
f"z={rec['base_pos'][2]:.2f} rpy={np.round(rpy_deg, 1)} "
|
||||||
|
f"act_max={np.max(np.abs(action)):.2f} "
|
||||||
|
f"target_off={np.max(np.abs(target_pos - DEFAULT_DOF_POS)):.2f}"
|
||||||
|
)
|
||||||
|
if fallen:
|
||||||
|
print(f"[WARN] sample stopped: fallen at t={data.time:.2f}s")
|
||||||
|
break
|
||||||
|
|
||||||
|
step_count += 1
|
||||||
|
inference_step = (inference_step + 1) % steps_per_inference
|
||||||
|
|
||||||
|
summary = summarize_samples(samples)
|
||||||
|
(logger.run_dir / "summary.json").write_text(
|
||||||
|
json.dumps(summary, indent=2, ensure_ascii=False))
|
||||||
|
logger.close()
|
||||||
|
print("[INFO] sample summary:")
|
||||||
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||||
|
print(f"[INFO] sample saved: {logger.run_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# ── Keyboard ──
|
||||||
|
kb = KbReader()
|
||||||
|
kb.start()
|
||||||
|
|
||||||
|
# ── Viewer ──
|
||||||
|
view = viewer.launch_passive(model, data)
|
||||||
|
# Track the robot body
|
||||||
|
body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "base_link")
|
||||||
|
if body_id >= 0:
|
||||||
|
view.cam.type = mujoco.mjtCamera.mjCAMERA_TRACKING
|
||||||
|
view.cam.trackbodyid = body_id
|
||||||
|
view.cam.distance = 2.5
|
||||||
|
view.cam.elevation = -20
|
||||||
|
view.cam.azimuth = 60
|
||||||
|
print("[INFO] viewer launched")
|
||||||
|
|
||||||
|
loop_start = time.time()
|
||||||
|
|
||||||
|
while view.is_running() and not EXIT:
|
||||||
|
held = kb.snapshot()
|
||||||
|
|
||||||
|
# ── Quit ──
|
||||||
|
if 'key.esc' in held:
|
||||||
|
break
|
||||||
|
|
||||||
|
# ── Command from keyboard ──
|
||||||
|
vx, vy, yaw = get_command(held)
|
||||||
|
cmd = np.array([vx, vy, yaw], dtype=np.float32)
|
||||||
|
|
||||||
|
# ── Reset ──
|
||||||
|
if 'r' in held:
|
||||||
|
data.qpos[0:3] = np.array([0.0, 0.0, 0.34], dtype=np.float64)
|
||||||
|
data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64)
|
||||||
|
data.qpos[7:19] = DEFAULT_DOF_POS.astype(np.float64)
|
||||||
|
data.qvel[:] = 0.0
|
||||||
|
data.ctrl[:] = 0.0
|
||||||
|
last_action = np.zeros(NUM_ACTIONS, dtype=np.float32)
|
||||||
|
action_raw = np.zeros(NUM_ACTIONS, dtype=np.float32)
|
||||||
|
action = np.zeros(NUM_ACTIONS, dtype=np.float32)
|
||||||
|
target_pos = DEFAULT_DOF_POS.copy()
|
||||||
|
prev_target_pos = DEFAULT_DOF_POS.copy()
|
||||||
|
obs_builder.reset()
|
||||||
|
mujoco.mj_forward(model, data)
|
||||||
|
print("[RESET]")
|
||||||
|
|
||||||
|
# ── Inference ──
|
||||||
|
if inference_step == 0:
|
||||||
|
# Match RoboGauge: policy obs uses XML IMU gyro and framequat sensors.
|
||||||
|
# Fall back to qpos/qvel only for XMLs without those sensors.
|
||||||
|
imu_quat = read_sensor(model, data, "Body_Quat", 4)
|
||||||
|
imu_ang_vel = read_sensor(model, data, "Body_Gyro", 3)
|
||||||
|
quat_wxyz = imu_quat if imu_quat is not None else data.qpos[3:7].copy()
|
||||||
|
if imu_ang_vel is not None:
|
||||||
|
base_ang_vel_body = imu_ang_vel
|
||||||
|
else:
|
||||||
|
world_ang_vel = data.qvel[3:6].copy()
|
||||||
|
base_ang_vel_body = quat_rotate_inverse(quat_wxyz, world_ang_vel)
|
||||||
|
|
||||||
|
# Joint state (qpos[7:19] is FR,FL,RR,RL — matches policy order)
|
||||||
|
q = data.qpos[7:19].copy()
|
||||||
|
dq = data.qvel[6:18].copy()
|
||||||
|
|
||||||
|
# Build obs
|
||||||
|
obs_single = obs_builder.build_single_obs(
|
||||||
|
base_ang_vel_body, quat_wxyz, cmd, q, dq, last_action)
|
||||||
|
onnx_input = obs_builder.build_onnx_input(obs_single)
|
||||||
|
|
||||||
|
# Run ONNX
|
||||||
|
outputs = session.run(None, {'obs': onnx_input})
|
||||||
|
action_raw = outputs[0][0].astype(np.float32) # [12]
|
||||||
|
action = np.clip(action_raw, -args.clip_actions, args.clip_actions)
|
||||||
|
last_action = action.copy()
|
||||||
|
|
||||||
|
target_pos_raw = DEFAULT_DOF_POS + action * ACTION_SCALE
|
||||||
|
if args.max_target_step > 0.0:
|
||||||
|
delta = np.clip(
|
||||||
|
target_pos_raw - prev_target_pos,
|
||||||
|
-args.max_target_step,
|
||||||
|
args.max_target_step,
|
||||||
|
)
|
||||||
|
target_pos = prev_target_pos + delta
|
||||||
|
else:
|
||||||
|
target_pos = target_pos_raw
|
||||||
|
prev_target_pos = target_pos.copy()
|
||||||
|
|
||||||
|
# ── PD control ──
|
||||||
|
current_pos = data.qpos[7:19]
|
||||||
|
current_vel = data.qvel[6:18]
|
||||||
|
torques = KP * (target_pos - current_pos) - KD * current_vel
|
||||||
|
torques = np.clip(torques, -33.5, 33.5)
|
||||||
|
data.ctrl[:] = torques.astype(np.float64)
|
||||||
|
|
||||||
|
mujoco.mj_step(model, data)
|
||||||
|
view.sync()
|
||||||
|
|
||||||
|
# Real-time sync — use sim_dt because step_count increments every sim step
|
||||||
|
expected_time = step_count * sim_dt
|
||||||
|
elapsed = time.time() - loop_start
|
||||||
|
if 0 < expected_time - elapsed < ctrl_dt:
|
||||||
|
time.sleep(expected_time - elapsed)
|
||||||
|
|
||||||
|
step_count += 1
|
||||||
|
inference_step = (inference_step + 1) % steps_per_inference
|
||||||
|
|
||||||
|
# Periodic status
|
||||||
|
if step_count % 200 == 0:
|
||||||
|
z = data.qpos[2]
|
||||||
|
lin_vel_abs = np.linalg.norm(data.qvel[0:3])
|
||||||
|
print(f"[{step_count}] cmd=({vx:.1f},{vy:.1f},{yaw:.1f}) "
|
||||||
|
f"z={z:.3f} |v|={lin_vel_abs:.2f} "
|
||||||
|
f"act[0:4]={np.round(action[:4], 3)}")
|
||||||
|
|
||||||
|
kb.stop()
|
||||||
|
view.close()
|
||||||
|
print("[INFO] done.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -390,7 +390,7 @@ def send_position_cmd(client, state, targets, args):
|
|||||||
client.send(cmd)
|
client.send(cmd)
|
||||||
|
|
||||||
|
|
||||||
def ramp_to_default(client, args, state):
|
def ramp_to_default(client, args, state, logger=None, step_base=0):
|
||||||
print("[INFO] Ramping to default pose...")
|
print("[INFO] Ramping to default pose...")
|
||||||
current = motor_pos(state)
|
current = motor_pos(state)
|
||||||
error = current - DEFAULT_DOF_POS
|
error = current - DEFAULT_DOF_POS
|
||||||
@@ -402,6 +402,8 @@ def ramp_to_default(client, args, state):
|
|||||||
|
|
||||||
ramp_steps = max(1, int(args.ramp_time * args.ramp_hz))
|
ramp_steps = max(1, int(args.ramp_time * args.ramp_hz))
|
||||||
dt = 1.0 / args.ramp_hz
|
dt = 1.0 / args.ramp_hz
|
||||||
|
log_every = max(1, int(args.ramp_hz / 5.0))
|
||||||
|
next_t = time.perf_counter()
|
||||||
|
|
||||||
for i in range(ramp_steps):
|
for i in range(ramp_steps):
|
||||||
if EXIT:
|
if EXIT:
|
||||||
@@ -430,6 +432,8 @@ def ramp_to_default(client, args, state):
|
|||||||
position_protect_limit=None,
|
position_protect_limit=None,
|
||||||
)
|
)
|
||||||
client.send(cmd)
|
client.send(cmd)
|
||||||
|
if logger is not None and (i % log_every == 0 or i == ramp_steps - 1):
|
||||||
|
log_state(logger, step_base * 100000 + i, "RAMP", state, target=target)
|
||||||
if i % max(1, ramp_steps // 4) == 0:
|
if i % max(1, ramp_steps // 4) == 0:
|
||||||
actual = motor_pos(state)
|
actual = motor_pos(state)
|
||||||
print(
|
print(
|
||||||
@@ -437,16 +441,29 @@ def ramp_to_default(client, args, state):
|
|||||||
f"target_err={np.max(np.abs(target - DEFAULT_DOF_POS)):.3f} "
|
f"target_err={np.max(np.abs(target - DEFAULT_DOF_POS)):.3f} "
|
||||||
f"actual_err={np.max(np.abs(actual - DEFAULT_DOF_POS)):.3f}"
|
f"actual_err={np.max(np.abs(actual - DEFAULT_DOF_POS)):.3f}"
|
||||||
)
|
)
|
||||||
time.sleep(dt)
|
next_t += dt
|
||||||
|
sleep = next_t - time.perf_counter()
|
||||||
|
if sleep > 0:
|
||||||
|
time.sleep(sleep)
|
||||||
|
else:
|
||||||
|
next_t = time.perf_counter()
|
||||||
|
|
||||||
for _ in range(max(1, int(0.5 * args.ramp_hz))):
|
hold_steps = max(1, int(0.5 * args.ramp_hz))
|
||||||
|
for i in range(hold_steps):
|
||||||
if EXIT:
|
if EXIT:
|
||||||
return state
|
return state
|
||||||
new_state = client.recv_latest()
|
new_state = client.recv_latest()
|
||||||
if new_state is not None:
|
if new_state is not None:
|
||||||
state = new_state
|
state = new_state
|
||||||
send_hold_cmd(client, state, args)
|
send_hold_cmd(client, state, args)
|
||||||
time.sleep(dt)
|
if logger is not None and (i % log_every == 0 or i == hold_steps - 1):
|
||||||
|
log_state(logger, step_base * 100000 + ramp_steps + i, "RAMP_HOLD", state, target=DEFAULT_DOF_POS)
|
||||||
|
next_t += dt
|
||||||
|
sleep = next_t - time.perf_counter()
|
||||||
|
if sleep > 0:
|
||||||
|
time.sleep(sleep)
|
||||||
|
else:
|
||||||
|
next_t = time.perf_counter()
|
||||||
|
|
||||||
print("[INFO] Default pose reached.")
|
print("[INFO] Default pose reached.")
|
||||||
return state
|
return state
|
||||||
@@ -853,7 +870,7 @@ def run_deploy(args):
|
|||||||
if r2_rose:
|
if r2_rose:
|
||||||
print("\n[R2] IDLE -> CALIBRATE")
|
print("\n[R2] IDLE -> CALIBRATE")
|
||||||
sm_state = State.CALIBRATE
|
sm_state = State.CALIBRATE
|
||||||
state = ramp_to_default(client, args, state)
|
state = ramp_to_default(client, args, state, logger=logger, step_base=step)
|
||||||
obs_builder.reset()
|
obs_builder.reset()
|
||||||
cmd_filter.reset()
|
cmd_filter.reset()
|
||||||
last_action[:] = 0.0
|
last_action[:] = 0.0
|
||||||
@@ -925,7 +942,7 @@ def run_deploy(args):
|
|||||||
cmd_filter.reset()
|
cmd_filter.reset()
|
||||||
last_action[:] = 0.0
|
last_action[:] = 0.0
|
||||||
prev_action[:] = 0.0
|
prev_action[:] = 0.0
|
||||||
state = ramp_to_default(client, args, state)
|
state = ramp_to_default(client, args, state, logger=logger, step_base=step)
|
||||||
prev_target = DEFAULT_DOF_POS.copy()
|
prev_target = DEFAULT_DOF_POS.copy()
|
||||||
rl_step = 0
|
rl_step = 0
|
||||||
continue
|
continue
|
||||||
@@ -965,7 +982,7 @@ def run_deploy(args):
|
|||||||
if r2_rose:
|
if r2_rose:
|
||||||
print("\n[R2] FAULT -> CALIBRATE")
|
print("\n[R2] FAULT -> CALIBRATE")
|
||||||
sm_state = State.CALIBRATE
|
sm_state = State.CALIBRATE
|
||||||
state = ramp_to_default(client, args, state)
|
state = ramp_to_default(client, args, state, logger=logger, step_base=step)
|
||||||
prev_target = DEFAULT_DOF_POS.copy()
|
prev_target = DEFAULT_DOF_POS.copy()
|
||||||
sm_state = State.HOLD
|
sm_state = State.HOLD
|
||||||
print("[STATE] HOLD")
|
print("[STATE] HOLD")
|
||||||
@@ -1076,8 +1093,8 @@ def build_arg_parser():
|
|||||||
parser.add_argument("--cmd-yaw", type=float, default=0.0)
|
parser.add_argument("--cmd-yaw", type=float, default=0.0)
|
||||||
|
|
||||||
parser.add_argument("--rate-hz", type=float, default=50.0)
|
parser.add_argument("--rate-hz", type=float, default=50.0)
|
||||||
parser.add_argument("--ramp-time", type=float, default=2.5)
|
parser.add_argument("--ramp-time", type=float, default=5.0)
|
||||||
parser.add_argument("--ramp-hz", type=float, default=100.0)
|
parser.add_argument("--ramp-hz", type=float, default=50.0)
|
||||||
parser.add_argument("--warmup-steps", type=int, default=50)
|
parser.add_argument("--warmup-steps", type=int, default=50)
|
||||||
parser.add_argument("--max-steps", type=int, default=0)
|
parser.add_argument("--max-steps", type=int, default=0)
|
||||||
parser.add_argument("--print-every", type=int, default=50)
|
parser.add_argument("--print-every", type=int, default=50)
|
||||||
|
|||||||
1130
deploy_45dim_rl_gym/deploy_go1_rlgym_pro_sdk_lab.py
Normal file
1130
deploy_45dim_rl_gym/deploy_go1_rlgym_pro_sdk_lab.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
deploy_45dim_rl_gym/policy_robotlab_6500.onnx
Normal file
BIN
deploy_45dim_rl_gym/policy_robotlab_6500.onnx
Normal file
Binary file not shown.
Reference in New Issue
Block a user