Georg commited on
Commit
436f91f
·
1 Parent(s): 375d9f7

Enhance mujoco_server.py and UR5 environment for new UR5e T-Push scene support

Browse files

- Added support for a new UR5e T-Push scene, including environment initialization and task reward calculation.
- Updated robot switching logic to accommodate the new robot type and scene.
- Enhanced WebSocket API to handle gym-style interactions for the new scene, including reset and step functionalities.
- Modified UI elements to reflect the addition of the UR5e T-Push robot and its specific controls.
- Updated README.md to document the new gym-style WebSocket API and example payloads for the T-Push scene.

README.md CHANGED
@@ -30,6 +30,7 @@ A unified MuJoCo-based robot simulation platform with web interface for multiple
30
  - Real-time MuJoCo physics simulation
31
  - Web-based video streaming interface
32
  - WebSocket-based state/command communication
 
33
  - Interactive camera controls (rotate, zoom, pan)
34
  - Robot switching without restart
35
  - Keyboard and button controls for locomotion
@@ -39,11 +40,20 @@ A unified MuJoCo-based robot simulation platform with web interface for multiple
39
  ### Native (Recommended for Development)
40
 
41
  ```bash
 
 
 
 
42
  # Install dependencies
43
  pip install mujoco gymnasium flask flask-sock opencv-python torch numpy
44
 
45
- # Optional: For PyMPC gait controller
46
- pip install jax jaxlib quadruped-pympc gym-quadruped
 
 
 
 
 
47
 
48
  # Start the server
49
  python mujoco_server.py
@@ -51,6 +61,24 @@ python mujoco_server.py
51
  # Open browser at http://localhost:3004/nova-sim/api/v1
52
  ```
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  ### Docker
55
 
56
  ```bash
 
30
  - Real-time MuJoCo physics simulation
31
  - Web-based video streaming interface
32
  - WebSocket-based state/command communication
33
+ - Gym-style WebSocket API for RL/IL clients
34
  - Interactive camera controls (rotate, zoom, pan)
35
  - Robot switching without restart
36
  - Keyboard and button controls for locomotion
 
40
  ### Native (Recommended for Development)
41
 
42
  ```bash
43
+ # Create and activate a virtualenv
44
+ python3 -m venv .venv
45
+ source .venv/bin/activate
46
+
47
  # Install dependencies
48
  pip install mujoco gymnasium flask flask-sock opencv-python torch numpy
49
 
50
+ # Optional: For PyMPC gait controller (Quadruped-PyMPC isn't on PyPI)
51
+ pip install jax jaxlib gym-quadruped
52
+
53
+ # Quadruped-PyMPC needs submodules; install from a local clone
54
+ git clone --recurse-submodules https://github.com/iit-DLSLab/Quadruped-PyMPC
55
+ cd Quadruped-PyMPC
56
+ pip install -e .
57
 
58
  # Start the server
59
  python mujoco_server.py
 
61
  # Open browser at http://localhost:3004/nova-sim/api/v1
62
  ```
63
 
64
+ If you see `ModuleNotFoundError: No module named 'cv2'`, make sure the venv is activated
65
+ and `opencv-python` is installed in it.
66
+
67
+ ## Gym WebSocket API (RL/IL)
68
+
69
+ The gym-style API is exposed at `ws://localhost:3004/nova-sim/api/v1/gym/ws`.
70
+ It supports `reset`, `step`, `configure`, and `get_spaces`.
71
+
72
+ Example request payloads:
73
+
74
+ ```json
75
+ {"type": "configure", "data": {"robot": "ur5_t_push"}}
76
+ {"type": "reset"}
77
+ {"type": "step", "data": {"action": [0,0,0,0,0,0,0], "render": false}}
78
+ ```
79
+
80
+ The server responds with `gym_reset`, `gym_step`, `gym_spaces`, or `gym_configured` messages.
81
+
82
  ### Docker
83
 
84
  ```bash
mujoco_server.py CHANGED
@@ -3,6 +3,7 @@ import sys
3
  import time
4
  import threading
5
  import json
 
6
  import cv2
7
  import numpy as np
8
  import mujoco
@@ -43,12 +44,13 @@ TARGET_FPS = int(os.environ.get('TARGET_FPS', 30 if IN_DOCKER else 60))
43
  SIM_STEPS_PER_FRAME = int(os.environ.get('SIM_STEPS_PER_FRAME', 10 if IN_DOCKER else 5))
44
 
45
  # Current robot type
46
- current_robot = "g1" # "g1", "spot", or "ur5"
47
 
48
  # Environment instances (lazy loaded)
49
  env_g1 = None
50
  env_spot = None
51
  env_ur5 = None
 
52
  env = None # Active environment
53
 
54
  # Simulation state
@@ -98,15 +100,27 @@ def init_spot():
98
  return env_spot
99
 
100
 
101
- def init_ur5():
102
  """Initialize UR5e environment."""
103
- global env_ur5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  if env_ur5 is None:
105
- # Import UR5Env from robots/ur5 directory
106
- ur5_dir = os.path.join(_nova_sim_dir, 'robots', 'ur5')
107
- sys.path.insert(0, ur5_dir)
108
- from ur5_env import UR5Env
109
- sys.path.pop(0)
110
  env_ur5 = UR5Env(render_mode="rgb_array", width=RENDER_WIDTH, height=RENDER_HEIGHT)
111
  env_ur5.reset()
112
  return env_ur5
@@ -135,11 +149,17 @@ def switch_robot(robot_type):
135
  cam.lookat = np.array([0, 0, 0.4])
136
  cam.distance = 2.5
137
  elif robot_type == "ur5":
138
- env = init_ur5()
139
  cam.lookat = np.array([0.3, 0, 0.6])
140
  cam.distance = 1.8
141
  cam.azimuth = 150
142
  cam.elevation = -25
 
 
 
 
 
 
143
  else:
144
  print(f"Unknown robot type: {robot_type}")
145
  return
@@ -166,7 +186,7 @@ def broadcast_state():
166
  steps = env.steps
167
 
168
  # UR5 has different state structure
169
- if current_robot == "ur5":
170
  ee_pos = env.get_end_effector_pos()
171
  ee_quat = env.get_end_effector_quat()
172
  target = env.get_target()
@@ -190,7 +210,8 @@ def broadcast_state():
190
  'joint_targets': [float(j) for j in joint_targets],
191
  'control_mode': control_mode,
192
  'use_orientation': use_orientation,
193
- 'steps': int(steps)
 
194
  }
195
  })
196
  else:
@@ -255,7 +276,7 @@ def simulation_loop():
255
  env.step_with_controller(dt=sim_dt)
256
 
257
  # Update camera to follow robot (not for UR5 which is stationary)
258
- if camera_follow and current_robot != "ur5":
259
  robot_pos = env.data.qpos[:3]
260
  cam.lookat[0] = robot_pos[0]
261
  cam.lookat[1] = robot_pos[1]
@@ -334,7 +355,7 @@ def handle_ws_message(data):
334
  if current_robot == "g1":
335
  cam.distance = 3.0
336
  cam.lookat = np.array([0.0, 0.0, 0.8])
337
- elif current_robot == "ur5":
338
  cam.distance = 1.8
339
  cam.lookat = np.array([0.3, 0.0, 0.6])
340
  cam.azimuth = 150
@@ -381,7 +402,7 @@ def handle_ws_message(data):
381
  y = payload.get('y', 0.0)
382
  z = payload.get('z', 0.6)
383
  with mujoco_lock:
384
- if env is not None and current_robot == "ur5":
385
  env.set_target(x, y, z)
386
 
387
  elif msg_type == 'gripper':
@@ -395,14 +416,14 @@ def handle_ws_message(data):
395
  else:
396
  value = payload.get('value', 128)
397
  with mujoco_lock:
398
- if env is not None and current_robot == "ur5":
399
  env.set_gripper(value)
400
 
401
  elif msg_type == 'control_mode':
402
  payload = data.get('data', {})
403
  mode = payload.get('mode', 'ik')
404
  with mujoco_lock:
405
- if env is not None and current_robot == "ur5":
406
  env.set_control_mode(mode)
407
 
408
  elif msg_type == 'joint_positions':
@@ -410,7 +431,7 @@ def handle_ws_message(data):
410
  positions = payload.get('positions', [])
411
  if len(positions) == 6:
412
  with mujoco_lock:
413
- if env is not None and current_robot == "ur5":
414
  env.set_joint_positions(positions)
415
 
416
  elif msg_type == 'arm_orientation':
@@ -419,17 +440,101 @@ def handle_ws_message(data):
419
  pitch = payload.get('pitch', np.pi/2)
420
  yaw = payload.get('yaw', 0.0)
421
  with mujoco_lock:
422
- if env is not None and current_robot == "ur5":
423
  env.set_target_orientation(roll, pitch, yaw)
424
 
425
  elif msg_type == 'use_orientation':
426
  payload = data.get('data', {})
427
  use = payload.get('enabled', True)
428
  with mujoco_lock:
429
- if env is not None and current_robot == "ur5":
430
  env.set_use_orientation(use)
431
 
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  @sock.route(f'{API_PREFIX}/ws')
434
  def websocket_handler(ws):
435
  """Handle WebSocket connections."""
@@ -460,6 +565,87 @@ def websocket_handler(ws):
460
  print('WebSocket client disconnected')
461
 
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  # Serve UI at /nova-sim (no redirect)
464
  @app.route('/nova-sim')
465
  @app.route('/nova-sim/')
@@ -679,6 +865,7 @@ def index():
679
  EE Pos: <span id="ee_pos">0.00, 0.00, 0.00</span><br>
680
  EE Ori: <span id="ee_ori">0.00, 0.00, 0.00</span><br>
681
  Gripper: <span id="gripper_val">50%</span><br>
 
682
  Mode: <span id="control_mode_display">IK</span> | Steps: <span id="arm_step_val">0</span>
683
  </div>
684
  </div>
@@ -711,6 +898,7 @@ def index():
711
  <option value="g1">Unitree G1 (Humanoid)</option>
712
  <option value="spot">Boston Dynamics Spot (Quadruped)</option>
713
  <option value="ur5">Universal Robots UR5e (Arm)</option>
 
714
  </select>
715
  <div class="robot-info" id="robot_info">
716
  29 DOF humanoid with RL walking policy
@@ -870,13 +1058,15 @@ def index():
870
  const robotInfoText = {
871
  'g1': '29 DOF humanoid with RL walking policy',
872
  'spot': '12 DOF quadruped with trot gait controller',
873
- 'ur5': '6 DOF robot arm with Robotiq gripper'
 
874
  };
875
 
876
  const robotTitles = {
877
  'g1': 'Unitree G1 Humanoid',
878
  'spot': 'Boston Dynamics Spot',
879
- 'ur5': 'Universal Robots UR5e'
 
880
  };
881
 
882
  const locomotionControls = document.getElementById('locomotion_controls');
@@ -924,7 +1114,7 @@ def index():
924
  if (msg.type === 'state') {
925
  const data = msg.data;
926
 
927
- if (data.robot === 'ur5') {
928
  // UR5 state
929
  const ee = data.end_effector;
930
  document.getElementById('ee_pos').innerText =
@@ -942,6 +1132,12 @@ def index():
942
  document.getElementById('gripper_val').innerText =
943
  ((255 - data.gripper) / 255 * 100).toFixed(0) + '% open';
944
  document.getElementById('arm_step_val').innerText = data.steps;
 
 
 
 
 
 
945
 
946
  // Update joint position display (actual positions)
947
  if (data.joint_positions) {
@@ -1055,7 +1251,7 @@ def index():
1055
  robotInfo.innerText = robotInfoText[robot] || '';
1056
 
1057
  // Toggle controls based on robot type
1058
- if (robot === 'ur5') {
1059
  locomotionControls.classList.add('hidden');
1060
  armControls.classList.add('active');
1061
  document.getElementById('locomotion_state').style.display = 'none';
 
3
  import time
4
  import threading
5
  import json
6
+ import base64
7
  import cv2
8
  import numpy as np
9
  import mujoco
 
44
  SIM_STEPS_PER_FRAME = int(os.environ.get('SIM_STEPS_PER_FRAME', 10 if IN_DOCKER else 5))
45
 
46
  # Current robot type
47
+ current_robot = "g1" # "g1", "spot", "ur5", or "ur5_t_push"
48
 
49
  # Environment instances (lazy loaded)
50
  env_g1 = None
51
  env_spot = None
52
  env_ur5 = None
53
+ env_ur5_t_push = None
54
  env = None # Active environment
55
 
56
  # Simulation state
 
100
  return env_spot
101
 
102
 
103
+ def init_ur5(scene_name="scene"):
104
  """Initialize UR5e environment."""
105
+ global env_ur5, env_ur5_t_push
106
+ # Import UR5Env from robots/ur5 directory
107
+ ur5_dir = os.path.join(_nova_sim_dir, 'robots', 'ur5')
108
+ sys.path.insert(0, ur5_dir)
109
+ from ur5_env import UR5Env
110
+ sys.path.pop(0)
111
+
112
+ if scene_name == "scene_t_push":
113
+ if env_ur5_t_push is None:
114
+ env_ur5_t_push = UR5Env(
115
+ render_mode="rgb_array",
116
+ width=RENDER_WIDTH,
117
+ height=RENDER_HEIGHT,
118
+ scene_name="scene_t_push",
119
+ )
120
+ env_ur5_t_push.reset()
121
+ return env_ur5_t_push
122
+
123
  if env_ur5 is None:
 
 
 
 
 
124
  env_ur5 = UR5Env(render_mode="rgb_array", width=RENDER_WIDTH, height=RENDER_HEIGHT)
125
  env_ur5.reset()
126
  return env_ur5
 
149
  cam.lookat = np.array([0, 0, 0.4])
150
  cam.distance = 2.5
151
  elif robot_type == "ur5":
152
+ env = init_ur5("scene")
153
  cam.lookat = np.array([0.3, 0, 0.6])
154
  cam.distance = 1.8
155
  cam.azimuth = 150
156
  cam.elevation = -25
157
+ elif robot_type == "ur5_t_push":
158
+ env = init_ur5("scene_t_push")
159
+ cam.lookat = np.array([0.5, 0, 0.55])
160
+ cam.distance = 1.9
161
+ cam.azimuth = 150
162
+ cam.elevation = -25
163
  else:
164
  print(f"Unknown robot type: {robot_type}")
165
  return
 
186
  steps = env.steps
187
 
188
  # UR5 has different state structure
189
+ if current_robot in ("ur5", "ur5_t_push"):
190
  ee_pos = env.get_end_effector_pos()
191
  ee_quat = env.get_end_effector_quat()
192
  target = env.get_target()
 
210
  'joint_targets': [float(j) for j in joint_targets],
211
  'control_mode': control_mode,
212
  'use_orientation': use_orientation,
213
+ 'steps': int(steps),
214
+ 'reward': env.get_task_reward()
215
  }
216
  })
217
  else:
 
276
  env.step_with_controller(dt=sim_dt)
277
 
278
  # Update camera to follow robot (not for UR5 which is stationary)
279
+ if camera_follow and current_robot not in ("ur5", "ur5_t_push"):
280
  robot_pos = env.data.qpos[:3]
281
  cam.lookat[0] = robot_pos[0]
282
  cam.lookat[1] = robot_pos[1]
 
355
  if current_robot == "g1":
356
  cam.distance = 3.0
357
  cam.lookat = np.array([0.0, 0.0, 0.8])
358
+ elif current_robot in ("ur5", "ur5_t_push"):
359
  cam.distance = 1.8
360
  cam.lookat = np.array([0.3, 0.0, 0.6])
361
  cam.azimuth = 150
 
402
  y = payload.get('y', 0.0)
403
  z = payload.get('z', 0.6)
404
  with mujoco_lock:
405
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
406
  env.set_target(x, y, z)
407
 
408
  elif msg_type == 'gripper':
 
416
  else:
417
  value = payload.get('value', 128)
418
  with mujoco_lock:
419
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
420
  env.set_gripper(value)
421
 
422
  elif msg_type == 'control_mode':
423
  payload = data.get('data', {})
424
  mode = payload.get('mode', 'ik')
425
  with mujoco_lock:
426
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
427
  env.set_control_mode(mode)
428
 
429
  elif msg_type == 'joint_positions':
 
431
  positions = payload.get('positions', [])
432
  if len(positions) == 6:
433
  with mujoco_lock:
434
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
435
  env.set_joint_positions(positions)
436
 
437
  elif msg_type == 'arm_orientation':
 
440
  pitch = payload.get('pitch', np.pi/2)
441
  yaw = payload.get('yaw', 0.0)
442
  with mujoco_lock:
443
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
444
  env.set_target_orientation(roll, pitch, yaw)
445
 
446
  elif msg_type == 'use_orientation':
447
  payload = data.get('data', {})
448
  use = payload.get('enabled', True)
449
  with mujoco_lock:
450
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
451
  env.set_use_orientation(use)
452
 
453
 
454
+ def _serialize_space(space):
455
+ if hasattr(space, "low") and hasattr(space, "high"):
456
+ return {
457
+ "type": "box",
458
+ "shape": list(space.shape),
459
+ "low": space.low.tolist(),
460
+ "high": space.high.tolist(),
461
+ "dtype": str(space.dtype),
462
+ }
463
+ return {"type": "unknown"}
464
+
465
+
466
+ def _resolve_robot_scene(robot, scene):
467
+ if robot == "ur5_t_push" and scene is None:
468
+ return "ur5", "scene_t_push"
469
+ if robot == "ur5" and scene is None:
470
+ return "ur5", "scene"
471
+ return robot, scene
472
+
473
+
474
+ def _create_env(robot, scene):
475
+ if robot == "g1":
476
+ return G1Env(render_mode="rgb_array", width=RENDER_WIDTH, height=RENDER_HEIGHT)
477
+ if robot == "spot":
478
+ spot_dir = os.path.join(_nova_sim_dir, 'robots', 'spot')
479
+ sys.path.insert(0, spot_dir)
480
+ from spot_env import SpotEnv
481
+ sys.path.pop(0)
482
+ return SpotEnv(render_mode="rgb_array", width=RENDER_WIDTH, height=RENDER_HEIGHT, controller_type='pympc')
483
+ if robot == "ur5":
484
+ ur5_dir = os.path.join(_nova_sim_dir, 'robots', 'ur5')
485
+ sys.path.insert(0, ur5_dir)
486
+ from ur5_env import UR5Env
487
+ sys.path.pop(0)
488
+ if scene:
489
+ return UR5Env(render_mode="rgb_array", width=RENDER_WIDTH, height=RENDER_HEIGHT, scene_name=scene)
490
+ return UR5Env(render_mode="rgb_array", width=RENDER_WIDTH, height=RENDER_HEIGHT)
491
+ raise ValueError(f"Unsupported robot for gym: {robot}")
492
+
493
+
494
+ class GymSession:
495
+ def __init__(self, robot="ur5", scene=None):
496
+ robot, scene = _resolve_robot_scene(robot, scene)
497
+ self.robot = robot
498
+ self.scene = scene
499
+ self.env = _create_env(robot, scene)
500
+ self.last_obs, _ = self.env.reset()
501
+
502
+ def configure(self, robot, scene=None):
503
+ robot, scene = _resolve_robot_scene(robot, scene)
504
+ if self.env is not None:
505
+ self.env.close()
506
+ self.robot = robot
507
+ self.scene = scene
508
+ self.env = _create_env(robot, scene)
509
+ self.last_obs, _ = self.env.reset()
510
+
511
+ def reset(self, seed=None):
512
+ obs, info = self.env.reset(seed=seed)
513
+ self.last_obs = obs
514
+ return obs, info
515
+
516
+ def step(self, action):
517
+ action = np.array(action, dtype=np.float32)
518
+ obs, reward, terminated, truncated, info = self.env.step(action)
519
+ self.last_obs = obs
520
+ return obs, reward, terminated, truncated, info
521
+
522
+ def render_jpeg(self):
523
+ frame = self.env.render()
524
+ if frame is None:
525
+ return None
526
+ frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
527
+ ret, buffer = cv2.imencode('.jpg', frame_bgr)
528
+ if not ret:
529
+ return None
530
+ return base64.b64encode(buffer.tobytes()).decode("ascii")
531
+
532
+ def close(self):
533
+ if self.env is not None:
534
+ self.env.close()
535
+ self.env = None
536
+
537
+
538
  @sock.route(f'{API_PREFIX}/ws')
539
  def websocket_handler(ws):
540
  """Handle WebSocket connections."""
 
565
  print('WebSocket client disconnected')
566
 
567
 
568
+ @sock.route(f'{API_PREFIX}/gym/ws')
569
+ def gym_websocket_handler(ws):
570
+ """Gym-style WebSocket API for RL/IL clients."""
571
+ session = GymSession()
572
+ try:
573
+ while True:
574
+ message = ws.receive()
575
+ if message is None:
576
+ break
577
+ try:
578
+ data = json.loads(message)
579
+ except json.JSONDecodeError:
580
+ ws.send(json.dumps({"type": "gym_error", "message": "Invalid JSON"}))
581
+ continue
582
+
583
+ msg_type = data.get("type")
584
+ payload = data.get("data", {})
585
+ msg_id = data.get("id")
586
+
587
+ try:
588
+ if msg_type == "reset":
589
+ seed = payload.get("seed")
590
+ obs, info = session.reset(seed=seed)
591
+ response = {
592
+ "type": "gym_reset",
593
+ "data": {"obs": obs.tolist(), "info": info},
594
+ }
595
+ elif msg_type == "step":
596
+ action = payload.get("action", [])
597
+ obs, reward, terminated, truncated, info = session.step(action)
598
+ response = {
599
+ "type": "gym_step",
600
+ "data": {
601
+ "obs": obs.tolist(),
602
+ "reward": float(reward),
603
+ "terminated": bool(terminated),
604
+ "truncated": bool(truncated),
605
+ "info": info,
606
+ },
607
+ }
608
+ if payload.get("render", False):
609
+ frame_jpeg = session.render_jpeg()
610
+ response["data"]["frame_jpeg"] = frame_jpeg
611
+ elif msg_type == "configure":
612
+ robot = payload.get("robot", "ur5")
613
+ scene = payload.get("scene")
614
+ session.configure(robot, scene)
615
+ response = {
616
+ "type": "gym_configured",
617
+ "data": {"robot": session.robot, "scene": session.scene},
618
+ }
619
+ elif msg_type == "get_spaces":
620
+ response = {
621
+ "type": "gym_spaces",
622
+ "data": {
623
+ "action_space": _serialize_space(session.env.action_space),
624
+ "observation_space": _serialize_space(session.env.observation_space),
625
+ },
626
+ }
627
+ elif msg_type == "close":
628
+ response = {"type": "gym_closed"}
629
+ ws.send(json.dumps(response))
630
+ break
631
+ else:
632
+ response = {
633
+ "type": "gym_error",
634
+ "message": f"Unknown message type: {msg_type}",
635
+ }
636
+
637
+ if msg_id is not None:
638
+ response["id"] = msg_id
639
+ ws.send(json.dumps(response))
640
+ except Exception as e:
641
+ error_response = {"type": "gym_error", "message": str(e)}
642
+ if msg_id is not None:
643
+ error_response["id"] = msg_id
644
+ ws.send(json.dumps(error_response))
645
+ finally:
646
+ session.close()
647
+
648
+
649
  # Serve UI at /nova-sim (no redirect)
650
  @app.route('/nova-sim')
651
  @app.route('/nova-sim/')
 
865
  EE Pos: <span id="ee_pos">0.00, 0.00, 0.00</span><br>
866
  EE Ori: <span id="ee_ori">0.00, 0.00, 0.00</span><br>
867
  Gripper: <span id="gripper_val">50%</span><br>
868
+ Reward: <span id="arm_reward">-</span><br>
869
  Mode: <span id="control_mode_display">IK</span> | Steps: <span id="arm_step_val">0</span>
870
  </div>
871
  </div>
 
898
  <option value="g1">Unitree G1 (Humanoid)</option>
899
  <option value="spot">Boston Dynamics Spot (Quadruped)</option>
900
  <option value="ur5">Universal Robots UR5e (Arm)</option>
901
+ <option value="ur5_t_push">UR5e T-Push Scene</option>
902
  </select>
903
  <div class="robot-info" id="robot_info">
904
  29 DOF humanoid with RL walking policy
 
1058
  const robotInfoText = {
1059
  'g1': '29 DOF humanoid with RL walking policy',
1060
  'spot': '12 DOF quadruped with trot gait controller',
1061
+ 'ur5': '6 DOF robot arm with Robotiq gripper',
1062
+ 'ur5_t_push': 'UR5e T-push task with stick tool'
1063
  };
1064
 
1065
  const robotTitles = {
1066
  'g1': 'Unitree G1 Humanoid',
1067
  'spot': 'Boston Dynamics Spot',
1068
+ 'ur5': 'Universal Robots UR5e',
1069
+ 'ur5_t_push': 'UR5e T-Push Scene'
1070
  };
1071
 
1072
  const locomotionControls = document.getElementById('locomotion_controls');
 
1114
  if (msg.type === 'state') {
1115
  const data = msg.data;
1116
 
1117
+ if (data.robot === 'ur5' || data.robot === 'ur5_t_push') {
1118
  // UR5 state
1119
  const ee = data.end_effector;
1120
  document.getElementById('ee_pos').innerText =
 
1132
  document.getElementById('gripper_val').innerText =
1133
  ((255 - data.gripper) / 255 * 100).toFixed(0) + '% open';
1134
  document.getElementById('arm_step_val').innerText = data.steps;
1135
+ const rewardEl = document.getElementById('arm_reward');
1136
+ if (data.reward === null || data.reward === undefined) {
1137
+ rewardEl.innerText = '-';
1138
+ } else {
1139
+ rewardEl.innerText = data.reward.toFixed(3);
1140
+ }
1141
 
1142
  // Update joint position display (actual positions)
1143
  if (data.joint_positions) {
 
1251
  robotInfo.innerText = robotInfoText[robot] || '';
1252
 
1253
  // Toggle controls based on robot type
1254
+ if (robot === 'ur5' || robot === 'ur5_t_push') {
1255
  locomotionControls.classList.add('hidden');
1256
  armControls.classList.add('active');
1257
  document.getElementById('locomotion_state').style.display = 'none';
robots/ur5/model/scene_t_push.xml ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mujoco model="ur5e_with_gripper">
2
+ <compiler angle="radian" meshdir="assets" autolimits="true"/>
3
+
4
+ <option integrator="implicitfast" cone="elliptic" impratio="10"/>
5
+
6
+ <!-- Wandelbots Corporate Design Colors:
7
+ Primary Dark: #01040f (0.004, 0.016, 0.059)
8
+ Light/Secondary: #bcbeec (0.737, 0.745, 0.925)
9
+ Accent: #211c44 (0.129, 0.110, 0.267)
10
+ Highlight: #8b7fef (0.545, 0.498, 0.937)
11
+ -->
12
+ <visual>
13
+ <headlight diffuse="0.6 0.6 0.6" ambient="0.35 0.35 0.4" specular="0 0 0"/>
14
+ <rgba haze="0.02 0.04 0.12 1"/>
15
+ <global azimuth="120" elevation="-20"/>
16
+ </visual>
17
+
18
+ <asset>
19
+ <!-- Wandelbots gradient skybox - deep purple to near black -->
20
+ <texture type="skybox" builtin="gradient" rgb1="0.13 0.11 0.27" rgb2="0.004 0.016 0.059" width="512" height="3072"/>
21
+ <!-- Ground with Wandelbots purple accent -->
22
+ <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.08 0.07 0.15" rgb2="0.04 0.04 0.08"
23
+ markrgb="0.55 0.5 0.94" width="300" height="300"/>
24
+ <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.15"/>
25
+ <!-- Table with subtle purple tint -->
26
+ <material name="table" rgba="0.18 0.16 0.25 1" specular="0.4" shininess="0.4"/>
27
+ <!-- Target marker with Wandelbots highlight color -->
28
+ <material name="target_mat" rgba="0.55 0.5 0.94 0.6" specular="0.5" shininess="0.5"/>
29
+
30
+ <!-- T-push scene materials -->
31
+ <material name="t_target_mat" rgba="0.2 0.7 0.35 0.25" specular="0.2" shininess="0.2"/>
32
+ <material name="t_object_mat" rgba="0.55 0.65 0.98 1" specular="0.3" shininess="0.2"/>
33
+ <material name="stick_mat" rgba="0.6 0.6 0.62 1" specular="0.4" shininess="0.3"/>
34
+
35
+ <!-- UR5e materials - with Wandelbots accent colors -->
36
+ <material name="black" rgba="0.02 0.02 0.04 1" specular="0.5" shininess="0.25"/>
37
+ <material name="jointgray" rgba="0.22 0.22 0.26 1" specular="0.5" shininess="0.25"/>
38
+ <material name="linkgray" rgba="0.74 0.75 0.82 1" specular="0.5" shininess="0.25"/>
39
+ <!-- Wandelbots purple accent instead of UR blue -->
40
+ <material name="urblue" rgba="0.55 0.5 0.94 1" specular="0.6" shininess="0.35"/>
41
+
42
+ <!-- Gripper materials -->
43
+ <material name="metal" rgba="0.58 0.58 0.58 1"/>
44
+ <material name="silicone" rgba="0.1882 0.1882 0.1882 1"/>
45
+ <material name="gray" rgba="0.4627 0.4627 0.4627 1"/>
46
+
47
+ <!-- UR5e meshes -->
48
+ <mesh file="base_0.obj"/>
49
+ <mesh file="base_1.obj"/>
50
+ <mesh file="shoulder_0.obj"/>
51
+ <mesh file="shoulder_1.obj"/>
52
+ <mesh file="shoulder_2.obj"/>
53
+ <mesh file="upperarm_0.obj"/>
54
+ <mesh file="upperarm_1.obj"/>
55
+ <mesh file="upperarm_2.obj"/>
56
+ <mesh file="upperarm_3.obj"/>
57
+ <mesh file="forearm_0.obj"/>
58
+ <mesh file="forearm_1.obj"/>
59
+ <mesh file="forearm_2.obj"/>
60
+ <mesh file="forearm_3.obj"/>
61
+ <mesh file="wrist1_0.obj"/>
62
+ <mesh file="wrist1_1.obj"/>
63
+ <mesh file="wrist1_2.obj"/>
64
+ <mesh file="wrist2_0.obj"/>
65
+ <mesh file="wrist2_1.obj"/>
66
+ <mesh file="wrist2_2.obj"/>
67
+ <mesh file="wrist3.obj"/>
68
+
69
+ <!-- Gripper meshes -->
70
+ <mesh name="base_mount" file="base_mount.stl" scale="0.001 0.001 0.001"/>
71
+ <mesh name="base_g" file="base.stl" scale="0.001 0.001 0.001"/>
72
+ <mesh name="driver" file="driver.stl" scale="0.001 0.001 0.001"/>
73
+ <mesh name="coupler" file="coupler.stl" scale="0.001 0.001 0.001"/>
74
+ <mesh name="follower" file="follower.stl" scale="0.001 0.001 0.001"/>
75
+ <mesh name="pad" file="pad.stl" scale="0.001 0.001 0.001"/>
76
+ <mesh name="silicone_pad" file="silicone_pad.stl" scale="0.001 0.001 0.001"/>
77
+ <mesh name="spring_link" file="spring_link.stl" scale="0.001 0.001 0.001"/>
78
+ </asset>
79
+
80
+ <default>
81
+ <default class="ur5e">
82
+ <joint axis="0 1 0" range="-6.28319 6.28319" armature="0.1"/>
83
+ <general gaintype="fixed" biastype="affine" ctrlrange="-6.2831 6.2831" gainprm="2000" biasprm="0 -2000 -400" forcerange="-150 150"/>
84
+ <default class="size3">
85
+ <default class="size3_limited">
86
+ <joint range="-3.1415 3.1415"/>
87
+ <general ctrlrange="-3.1415 3.1415"/>
88
+ </default>
89
+ </default>
90
+ <default class="size1">
91
+ <general gainprm="500" biasprm="0 -500 -100" forcerange="-28 28"/>
92
+ </default>
93
+ <default class="visual">
94
+ <geom type="mesh" contype="0" conaffinity="0" group="2"/>
95
+ </default>
96
+ <default class="collision">
97
+ <geom type="capsule" group="3"/>
98
+ <default class="eef_collision">
99
+ <geom type="cylinder"/>
100
+ </default>
101
+ </default>
102
+ <site size="0.001" rgba="0.5 0.5 0.5 0.3" group="4"/>
103
+ </default>
104
+
105
+ <default class="gripper">
106
+ <general biastype="affine"/>
107
+ <joint axis="1 0 0"/>
108
+ <default class="driver_j">
109
+ <joint range="0 0.8" armature="0.005" damping="0.1" solimplimit="0.95 0.99 0.001" solreflimit="0.005 1"/>
110
+ </default>
111
+ <default class="follower_j">
112
+ <joint range="-0.872664 0.872664" armature="0.001" pos="0 -0.018 0.0065" solimplimit="0.95 0.99 0.001" solreflimit="0.005 1"/>
113
+ </default>
114
+ <default class="spring_link_j">
115
+ <joint range="-0.29670597283 0.8" armature="0.001" stiffness="0.05" springref="2.62" damping="0.00125"/>
116
+ </default>
117
+ <default class="coupler_j">
118
+ <joint range="-1.57 0" armature="0.001" solimplimit="0.95 0.99 0.001" solreflimit="0.005 1"/>
119
+ </default>
120
+ <default class="visual_g">
121
+ <geom type="mesh" contype="0" conaffinity="0" group="2"/>
122
+ </default>
123
+ <default class="collision_g">
124
+ <geom type="mesh" group="3"/>
125
+ <default class="pad_box1">
126
+ <geom mass="0" type="box" pos="0 -0.0026 0.028125" size="0.011 0.004 0.009375" friction="0.7"
127
+ solimp="0.95 0.99 0.001" solref="0.004 1" priority="1" rgba="0.55 0.55 0.55 1"/>
128
+ </default>
129
+ <default class="pad_box2">
130
+ <geom mass="0" type="box" pos="0 -0.0026 0.009375" size="0.011 0.004 0.009375" friction="0.6"
131
+ solimp="0.95 0.99 0.001" solref="0.004 1" priority="1" rgba="0.45 0.45 0.45 1"/>
132
+ </default>
133
+ </default>
134
+ </default>
135
+ </default>
136
+
137
+ <worldbody>
138
+ <light pos="0 0 3.5" dir="0 0 -1" directional="true"/>
139
+ <geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>
140
+
141
+ <!-- Table -->
142
+ <body name="table" pos="0 0 0">
143
+ <geom name="table_top" type="box" pos="0.5 0 0.4" size="0.4 0.6 0.02" material="table"/>
144
+ <geom name="table_leg1" type="box" pos="0.2 0.4 0.2" size="0.03 0.03 0.2" material="table"/>
145
+ <geom name="table_leg2" type="box" pos="0.2 -0.4 0.2" size="0.03 0.03 0.2" material="table"/>
146
+ <geom name="table_leg3" type="box" pos="0.8 0.4 0.2" size="0.03 0.03 0.2" material="table"/>
147
+ <geom name="table_leg4" type="box" pos="0.8 -0.4 0.2" size="0.03 0.03 0.2" material="table"/>
148
+ </body>
149
+
150
+ <!-- Target visualization sphere (for IK target) -->
151
+ <body name="target" pos="0.4 0.0 0.6" mocap="true">
152
+ <geom name="target_vis" type="sphere" size="0.03" material="target_mat" contype="0" conaffinity="0"/>
153
+ </body>
154
+
155
+ <!-- UR5e robot mounted on table edge -->
156
+ <body name="base" pos="0 0 0.42" quat="0 0 0 -1" childclass="ur5e">
157
+ <inertial mass="4.0" pos="0 0 0" diaginertia="0.00443333156 0.00443333156 0.0072"/>
158
+ <geom mesh="base_0" material="black" class="visual"/>
159
+ <geom mesh="base_1" material="jointgray" class="visual"/>
160
+ <body name="shoulder_link" pos="0 0 0.163">
161
+ <inertial mass="3.7" pos="0 0 0" diaginertia="0.0102675 0.0102675 0.00666"/>
162
+ <joint name="shoulder_pan_joint" class="size3" axis="0 0 1"/>
163
+ <geom mesh="shoulder_0" material="urblue" class="visual"/>
164
+ <geom mesh="shoulder_1" material="black" class="visual"/>
165
+ <geom mesh="shoulder_2" material="jointgray" class="visual"/>
166
+ <geom class="collision" size="0.06 0.06" pos="0 0 -0.04"/>
167
+ <body name="upper_arm_link" pos="0 0.138 0" quat="1 0 1 0">
168
+ <inertial mass="8.393" pos="0 0 0.2125" diaginertia="0.133886 0.133886 0.0151074"/>
169
+ <joint name="shoulder_lift_joint" class="size3"/>
170
+ <geom mesh="upperarm_0" material="linkgray" class="visual"/>
171
+ <geom mesh="upperarm_1" material="black" class="visual"/>
172
+ <geom mesh="upperarm_2" material="jointgray" class="visual"/>
173
+ <geom mesh="upperarm_3" material="urblue" class="visual"/>
174
+ <geom class="collision" pos="0 -0.04 0" quat="1 1 0 0" size="0.06 0.06"/>
175
+ <geom class="collision" size="0.05 0.2" pos="0 0 0.2"/>
176
+ <body name="forearm_link" pos="0 -0.131 0.425">
177
+ <inertial mass="2.275" pos="0 0 0.196" diaginertia="0.0311796 0.0311796 0.004095"/>
178
+ <joint name="elbow_joint" class="size3_limited"/>
179
+ <geom mesh="forearm_0" material="urblue" class="visual"/>
180
+ <geom mesh="forearm_1" material="linkgray" class="visual"/>
181
+ <geom mesh="forearm_2" material="black" class="visual"/>
182
+ <geom mesh="forearm_3" material="jointgray" class="visual"/>
183
+ <geom class="collision" pos="0 0.08 0" quat="1 1 0 0" size="0.055 0.06"/>
184
+ <geom class="collision" size="0.038 0.19" pos="0 0 0.2"/>
185
+ <body name="wrist_1_link" pos="0 0 0.392" quat="1 0 1 0">
186
+ <inertial mass="1.219" pos="0 0.127 0" diaginertia="0.0025599 0.0025599 0.0021942"/>
187
+ <joint name="wrist_1_joint" class="size1"/>
188
+ <geom mesh="wrist1_0" material="black" class="visual"/>
189
+ <geom mesh="wrist1_1" material="urblue" class="visual"/>
190
+ <geom mesh="wrist1_2" material="jointgray" class="visual"/>
191
+ <geom class="collision" pos="0 0.05 0" quat="1 1 0 0" size="0.04 0.07"/>
192
+ <body name="wrist_2_link" pos="0 0.127 0">
193
+ <inertial mass="1.219" pos="0 0 0.1" diaginertia="0.0025599 0.0025599 0.0021942"/>
194
+ <joint name="wrist_2_joint" axis="0 0 1" class="size1"/>
195
+ <geom mesh="wrist2_0" material="black" class="visual"/>
196
+ <geom mesh="wrist2_1" material="urblue" class="visual"/>
197
+ <geom mesh="wrist2_2" material="jointgray" class="visual"/>
198
+ <geom class="collision" size="0.04 0.06" pos="0 0 0.04"/>
199
+ <geom class="collision" pos="0 0.02 0.1" quat="1 1 0 0" size="0.04 0.04"/>
200
+ <body name="wrist_3_link" pos="0 0 0.1">
201
+ <inertial mass="0.1889" pos="0 0.0771683 0" quat="1 0 0 1"
202
+ diaginertia="0.000132134 9.90863e-05 9.90863e-05"/>
203
+ <joint name="wrist_3_joint" class="size1"/>
204
+ <geom material="linkgray" mesh="wrist3" class="visual"/>
205
+ <geom class="eef_collision" pos="0 0.08 0" quat="1 1 0 0" size="0.04 0.02"/>
206
+ <site name="attachment_site" pos="0 0.1 0" quat="-1 1 0 0"/>
207
+
208
+ <!-- Gripper attached to wrist -->
209
+ <body name="gripper_base_mount" pos="0 0.1 0" quat="-1 1 0 0" childclass="gripper">
210
+ <body name="gripper_base_mount_inner" pos="0 0 0.007">
211
+ <geom class="visual_g" mesh="base_mount" material="black"/>
212
+ <geom class="collision_g" mesh="base_mount"/>
213
+ <body name="gripper_base" pos="0 0 0.0038" quat="1 0 0 -1">
214
+ <inertial mass="0.777441" pos="0 -2.70394e-05 0.0354675" quat="1 -0.00152849 0 0"
215
+ diaginertia="0.000260285 0.000225381 0.000152708"/>
216
+ <geom class="visual_g" mesh="base_g" material="black"/>
217
+ <geom class="collision_g" mesh="base_g"/>
218
+ <site name="pinch" pos="0 0 0.145" type="sphere" group="5" rgba="0.9 0.9 0.9 1" size="0.005"/>
219
+ <!-- End-effector site for IK -->
220
+ <site name="ee_site" pos="0 0 0.16" type="sphere" size="0.01" rgba="1 0 0 0.5"/>
221
+
222
+ <!-- Stick tool for T-push task -->
223
+ <geom name="push_stick" type="capsule" fromto="0 0 0.16 0 0.18 0.16" size="0.008" material="stick_mat" mass="0.02" friction="1 0.01 0.01"/>
224
+
225
+ <!-- Right-hand side 4-bar linkage -->
226
+ <body name="right_driver" pos="0 0.0306011 0.054904">
227
+ <inertial mass="0.00899563" pos="2.96931e-12 0.0177547 0.00107314" quat="0.681301 0.732003 0 0"
228
+ diaginertia="1.72352e-06 1.60906e-06 3.22006e-07"/>
229
+ <joint name="right_driver_joint" class="driver_j"/>
230
+ <geom class="visual_g" mesh="driver" material="gray"/>
231
+ <geom class="collision_g" mesh="driver"/>
232
+ <body name="right_coupler" pos="0 0.0315 -0.0041">
233
+ <inertial mass="0.0140974" pos="0 0.00301209 0.0232175" quat="0.705636 -0.0455904 0.0455904 0.705636"
234
+ diaginertia="4.16206e-06 3.52216e-06 8.88131e-07"/>
235
+ <joint name="right_coupler_joint" class="coupler_j"/>
236
+ <geom class="visual_g" mesh="coupler" material="black"/>
237
+ <geom class="collision_g" mesh="coupler"/>
238
+ </body>
239
+ </body>
240
+ <body name="right_spring_link" pos="0 0.0132 0.0609">
241
+ <inertial mass="0.0221642" pos="-8.65005e-09 0.0181624 0.0212658" quat="0.663403 -0.244737 0.244737 0.663403"
242
+ diaginertia="8.96853e-06 6.71733e-06 2.63931e-06"/>
243
+ <joint name="right_spring_link_joint" class="spring_link_j"/>
244
+ <geom class="visual_g" mesh="spring_link" material="black"/>
245
+ <geom class="collision_g" mesh="spring_link"/>
246
+ <body name="right_follower" pos="0 0.055 0.0375">
247
+ <inertial mass="0.0125222" pos="0 -0.011046 0.0124786" quat="1 0.1664 0 0"
248
+ diaginertia="2.67415e-06 2.4559e-06 6.02031e-07"/>
249
+ <joint name="right_follower_joint" class="follower_j"/>
250
+ <geom class="visual_g" mesh="follower" material="black"/>
251
+ <geom class="collision_g" mesh="follower"/>
252
+ <body name="right_pad" pos="0 -0.0189 0.01352">
253
+ <geom class="pad_box1" name="right_pad1"/>
254
+ <geom class="pad_box2" name="right_pad2"/>
255
+ <inertial mass="0.0035" pos="0 -0.0025 0.0185" quat="0.707107 0 0 0.707107"
256
+ diaginertia="4.73958e-07 3.64583e-07 1.23958e-07"/>
257
+ <geom class="visual_g" mesh="pad"/>
258
+ <body name="right_silicone_pad">
259
+ <geom class="visual_g" mesh="silicone_pad" material="black"/>
260
+ </body>
261
+ </body>
262
+ </body>
263
+ </body>
264
+ <!-- Left-hand side 4-bar linkage -->
265
+ <body name="left_driver" pos="0 -0.0306011 0.054904" quat="0 0 0 1">
266
+ <inertial mass="0.00899563" pos="0 0.0177547 0.00107314" quat="0.681301 0.732003 0 0"
267
+ diaginertia="1.72352e-06 1.60906e-06 3.22006e-07"/>
268
+ <joint name="left_driver_joint" class="driver_j"/>
269
+ <geom class="visual_g" mesh="driver" material="gray"/>
270
+ <geom class="collision_g" mesh="driver"/>
271
+ <body name="left_coupler" pos="0 0.0315 -0.0041">
272
+ <inertial mass="0.0140974" pos="0 0.00301209 0.0232175" quat="0.705636 -0.0455904 0.0455904 0.705636"
273
+ diaginertia="4.16206e-06 3.52216e-06 8.88131e-07"/>
274
+ <joint name="left_coupler_joint" class="coupler_j"/>
275
+ <geom class="visual_g" mesh="coupler" material="black"/>
276
+ <geom class="collision_g" mesh="coupler"/>
277
+ </body>
278
+ </body>
279
+ <body name="left_spring_link" pos="0 -0.0132 0.0609" quat="0 0 0 1">
280
+ <inertial mass="0.0221642" pos="-8.65005e-09 0.0181624 0.0212658" quat="0.663403 -0.244737 0.244737 0.663403"
281
+ diaginertia="8.96853e-06 6.71733e-06 2.63931e-06"/>
282
+ <joint name="left_spring_link_joint" class="spring_link_j"/>
283
+ <geom class="visual_g" mesh="spring_link" material="black"/>
284
+ <geom class="collision_g" mesh="spring_link"/>
285
+ <body name="left_follower" pos="0 0.055 0.0375">
286
+ <inertial mass="0.0125222" pos="0 -0.011046 0.0124786" quat="1 0.1664 0 0"
287
+ diaginertia="2.67415e-06 2.4559e-06 6.02031e-07"/>
288
+ <joint name="left_follower_joint" class="follower_j"/>
289
+ <geom class="visual_g" mesh="follower" material="black"/>
290
+ <geom class="collision_g" mesh="follower"/>
291
+ <body name="left_pad" pos="0 -0.0189 0.01352">
292
+ <geom class="pad_box1" name="left_pad1"/>
293
+ <geom class="pad_box2" name="left_pad2"/>
294
+ <inertial mass="0.0035" pos="0 -0.0025 0.0185" quat="1 0 0 1"
295
+ diaginertia="4.73958e-07 3.64583e-07 1.23958e-07"/>
296
+ <geom class="visual_g" mesh="pad"/>
297
+ <body name="left_silicone_pad">
298
+ <geom class="visual_g" mesh="silicone_pad" material="black"/>
299
+ </body>
300
+ </body>
301
+ </body>
302
+ </body>
303
+ </body>
304
+ </body>
305
+ </body>
306
+ </body>
307
+ </body>
308
+ </body>
309
+ </body>
310
+ </body>
311
+ </body>
312
+ </body>
313
+
314
+ <!-- T-shaped target (visual marker) -->
315
+ <body name="t_target" pos="0.62 -0.18 0.425">
316
+ <geom name="t_target_stem" type="box" pos="0 -0.05 0" size="0.02 0.07 0.002" material="t_target_mat" contype="0" conaffinity="0"/>
317
+ <geom name="t_target_cap" type="box" pos="0 0.03 0" size="0.08 0.02 0.002" material="t_target_mat" contype="0" conaffinity="0"/>
318
+ </body>
319
+
320
+ <!-- Movable T-shaped object to push into target -->
321
+ <body name="t_object" pos="0.45 0.2 0.43">
322
+ <freejoint name="t_object_joint"/>
323
+ <geom name="t_object_stem" type="box" pos="0 -0.05 0" size="0.02 0.07 0.01" material="t_object_mat" mass="0.3" friction="1 0.005 0.005"/>
324
+ <geom name="t_object_cap" type="box" pos="0 0.03 0" size="0.08 0.02 0.01" material="t_object_mat" mass="0.15" friction="1 0.005 0.005"/>
325
+ </body>
326
+ </worldbody>
327
+
328
+ <contact>
329
+ <exclude body1="gripper_base" body2="left_driver"/>
330
+ <exclude body1="gripper_base" body2="right_driver"/>
331
+ <exclude body1="gripper_base" body2="left_spring_link"/>
332
+ <exclude body1="gripper_base" body2="right_spring_link"/>
333
+ <exclude body1="right_coupler" body2="right_follower"/>
334
+ <exclude body1="left_coupler" body2="left_follower"/>
335
+ </contact>
336
+
337
+ <tendon>
338
+ <fixed name="split">
339
+ <joint joint="right_driver_joint" coef="0.5"/>
340
+ <joint joint="left_driver_joint" coef="0.5"/>
341
+ </fixed>
342
+ </tendon>
343
+
344
+ <equality>
345
+ <connect anchor="0 0 0" body1="right_follower" body2="right_coupler" solimp="0.95 0.99 0.001" solref="0.005 1"/>
346
+ <connect anchor="0 0 0" body1="left_follower" body2="left_coupler" solimp="0.95 0.99 0.001" solref="0.005 1"/>
347
+ <joint joint1="right_driver_joint" joint2="left_driver_joint" polycoef="0 1 0 0 0" solimp="0.95 0.99 0.001" solref="0.005 1"/>
348
+ </equality>
349
+
350
+ <actuator>
351
+ <!-- UR5e joint actuators -->
352
+ <general class="size3" name="shoulder_pan" joint="shoulder_pan_joint"/>
353
+ <general class="size3" name="shoulder_lift" joint="shoulder_lift_joint"/>
354
+ <general class="size3_limited" name="elbow" joint="elbow_joint"/>
355
+ <general class="size1" name="wrist_1" joint="wrist_1_joint"/>
356
+ <general class="size1" name="wrist_2" joint="wrist_2_joint"/>
357
+ <general class="size1" name="wrist_3" joint="wrist_3_joint"/>
358
+ <!-- Gripper actuator -->
359
+ <general name="gripper" tendon="split" forcerange="-5 5" ctrlrange="0 255" gainprm="0.3137255 0 0" biasprm="0 -100 -10"/>
360
+ </actuator>
361
+
362
+ <keyframe>
363
+ <!-- Official MuJoCo Menagerie UR5e home pose with gripper open (0=open) -->
364
+ <key name="home" qpos="-1.5708 -1.5708 1.5708 -1.5708 -1.5708 0 0 0 0 0 0 0 0 0 0.45 0.2 0.43 1 0 0 0"
365
+ ctrl="-1.5708 -1.5708 1.5708 -1.5708 -1.5708 0 0"/>
366
+ </keyframe>
367
+ </mujoco>
robots/ur5/ur5_env.py CHANGED
@@ -43,7 +43,7 @@ class UR5Env(gym.Env):
43
  0.0, # wrist_3 - no rotation
44
  ], dtype=np.float32)
45
 
46
- def __init__(self, render_mode=None, width=1280, height=720):
47
  """Initialize UR5e environment.
48
 
49
  Args:
@@ -55,7 +55,8 @@ class UR5Env(gym.Env):
55
 
56
  # Load model
57
  ur5_dir = os.path.dirname(os.path.abspath(__file__))
58
- model_path = os.path.join(ur5_dir, "model", "scene.xml")
 
59
  self.model = mujoco.MjModel.from_xml_path(model_path)
60
 
61
  # Override framebuffer size
@@ -71,6 +72,8 @@ class UR5Env(gym.Env):
71
  # Find site IDs
72
  self.ee_site_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_SITE, "ee_site")
73
  self.target_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "target")
 
 
74
 
75
  # Action space: 6 joint positions + 1 gripper (0-255)
76
  self.action_space = spaces.Box(
@@ -97,6 +100,7 @@ class UR5Env(gym.Env):
97
  self.render_mode = render_mode
98
  self.width = width
99
  self.height = height
 
100
  self.renderer = None
101
 
102
  self.steps = 0
@@ -132,6 +136,15 @@ class UR5Env(gym.Env):
132
  # Direct joint targets (used when control_mode is 'joint')
133
  self._joint_targets = self.DEFAULT_HOME_POSE.copy()
134
 
 
 
 
 
 
 
 
 
 
135
  def set_target(self, x: float, y: float, z: float, update_joint_targets=True):
136
  """Set target position for IK controller.
137
 
@@ -290,6 +303,15 @@ class UR5Env(gym.Env):
290
  """Compatibility method - returns zeros for arm robots."""
291
  return np.array([0.0, 0.0, 0.0], dtype=np.float32)
292
 
 
 
 
 
 
 
 
 
 
293
  def reset(self, seed=None, options=None):
294
  super().reset(seed=seed)
295
 
@@ -300,10 +322,9 @@ class UR5Env(gym.Env):
300
  self.data.ctrl[:6] = self.DEFAULT_HOME_POSE.copy()
301
  self.data.ctrl[6] = 0 # Gripper open (Robotiq: 0=open, 255=closed)
302
 
303
- # Reset box position
304
- box_qpos_start = 6 + 8 # 6 arm joints + 8 gripper joints
305
- self.data.qpos[box_qpos_start:box_qpos_start+3] = [0.5, 0.2, 0.45]
306
- self.data.qpos[box_qpos_start+3:box_qpos_start+7] = [1, 0, 0, 0]
307
 
308
  # Compute forward kinematics to get EE pose from home joints
309
  mujoco.mj_forward(self.model, self.data)
 
43
  0.0, # wrist_3 - no rotation
44
  ], dtype=np.float32)
45
 
46
+ def __init__(self, render_mode=None, width=1280, height=720, scene_name="scene"):
47
  """Initialize UR5e environment.
48
 
49
  Args:
 
55
 
56
  # Load model
57
  ur5_dir = os.path.dirname(os.path.abspath(__file__))
58
+ scene_file = f"{scene_name}.xml" if not scene_name.endswith(".xml") else scene_name
59
+ model_path = os.path.join(ur5_dir, "model", scene_file)
60
  self.model = mujoco.MjModel.from_xml_path(model_path)
61
 
62
  # Override framebuffer size
 
72
  # Find site IDs
73
  self.ee_site_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_SITE, "ee_site")
74
  self.target_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "target")
75
+ self.t_object_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "t_object")
76
+ self.t_target_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "t_target")
77
 
78
  # Action space: 6 joint positions + 1 gripper (0-255)
79
  self.action_space = spaces.Box(
 
100
  self.render_mode = render_mode
101
  self.width = width
102
  self.height = height
103
+ self.scene_name = scene_name
104
  self.renderer = None
105
 
106
  self.steps = 0
 
136
  # Direct joint targets (used when control_mode is 'joint')
137
  self._joint_targets = self.DEFAULT_HOME_POSE.copy()
138
 
139
+ def _reset_freejoint(self, joint_name: str, pos, quat):
140
+ """Reset a freejoint pose by name if it exists."""
141
+ joint_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, joint_name)
142
+ if joint_id == -1:
143
+ return
144
+ qpos_adr = self.model.jnt_qposadr[joint_id]
145
+ self.data.qpos[qpos_adr:qpos_adr + 3] = pos
146
+ self.data.qpos[qpos_adr + 3:qpos_adr + 7] = quat
147
+
148
  def set_target(self, x: float, y: float, z: float, update_joint_targets=True):
149
  """Set target position for IK controller.
150
 
 
303
  """Compatibility method - returns zeros for arm robots."""
304
  return np.array([0.0, 0.0, 0.0], dtype=np.float32)
305
 
306
+ def get_task_reward(self):
307
+ """Return task reward for scene-specific tasks, or None if not applicable."""
308
+ if self.t_object_body_id == -1 or self.t_target_body_id == -1:
309
+ return None
310
+ t_object_pos = self.data.xpos[self.t_object_body_id]
311
+ t_target_pos = self.data.xpos[self.t_target_body_id]
312
+ planar_dist = np.linalg.norm(t_object_pos[:2] - t_target_pos[:2])
313
+ return -float(planar_dist)
314
+
315
  def reset(self, seed=None, options=None):
316
  super().reset(seed=seed)
317
 
 
322
  self.data.ctrl[:6] = self.DEFAULT_HOME_POSE.copy()
323
  self.data.ctrl[6] = 0 # Gripper open (Robotiq: 0=open, 255=closed)
324
 
325
+ # Reset task objects if present
326
+ self._reset_freejoint("box_joint", [0.5, 0.2, 0.45], [1, 0, 0, 0])
327
+ self._reset_freejoint("t_object_joint", [0.45, 0.2, 0.43], [1, 0, 0, 0])
 
328
 
329
  # Compute forward kinematics to get EE pose from home joints
330
  mujoco.mj_forward(self.model, self.data)