multimodalart HF Staff commited on
Commit
cb0fb67
·
verified ·
1 Parent(s): e9d0d11

FLUX 3 Action SO-101 MuJoCo demo

Browse files
.gitattributes CHANGED
@@ -33,3 +33,17 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/base_motor_holder_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
37
+ assets/base_so101_v2.stl filter=lfs diff=lfs merge=lfs -text
38
+ assets/motor_holder_so101_base_v1.stl filter=lfs diff=lfs merge=lfs -text
39
+ assets/motor_holder_so101_wrist_v1.stl filter=lfs diff=lfs merge=lfs -text
40
+ assets/moving_jaw_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
41
+ assets/rotation_pitch_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
42
+ assets/sts3215_03a_no_horn_v1.stl filter=lfs diff=lfs merge=lfs -text
43
+ assets/sts3215_03a_v1.stl filter=lfs diff=lfs merge=lfs -text
44
+ assets/under_arm_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
45
+ assets/upper_arm_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
46
+ assets/wrist_camera_mount_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
47
+ assets/wrist_camera_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
48
+ assets/wrist_roll_follower_so101_v1.stl filter=lfs diff=lfs merge=lfs -text
49
+ assets/wrist_roll_pitch_so101_v2.stl filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,19 @@
1
  ---
2
- title: Flux 3 Action So101 Sim
3
- emoji: 🏆
4
- colorFrom: pink
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.28.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: FLUX 3 Action SO-101 Sim
3
+ emoji: 🦾
4
+ colorFrom: yellow
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.28.0
 
8
  app_file: app.py
9
+ python_version: "3.12"
10
+ short_description: FLUX 3 Action drives a simulated SO-101 arm in MuJoCo
11
+ startup_duration_timeout: 1h
12
+ models:
13
+ - black-forest-labs/flux-3-action-so101
14
+ - black-forest-labs/flux-3-action-base
15
  ---
16
 
17
+ Closed-loop demo of [FLUX 3 Action SO-101](https://huggingface.co/black-forest-labs/flux-3-action-so101) controlling a simulated SO-101 arm in MuJoCo.
18
+
19
+ Robot model and meshes: [TheRobotStudio/SO-ARM100](https://github.com/TheRobotStudio/SO-ARM100) (`so101_new_calib_camera.xml`, Apache-2.0).
app.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.util
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+ os.environ.setdefault("MUJOCO_GL", "osmesa")
7
+ os.environ.setdefault("PYOPENGL_PLATFORM", "osmesa")
8
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
9
+
10
+ import spaces
11
+ import torch
12
+
13
+
14
+ def _ensure_natten():
15
+ if importlib.util.find_spec("natten") is not None:
16
+ return
17
+ base = torch.__version__.split("+")[0]
18
+ tag = "torch" + "".join(base.split(".")[:3]) + "cu" + (torch.version.cuda or "12.8").replace(".", "")
19
+ subprocess.check_call(
20
+ [sys.executable, "-m", "pip", "install", f"natten==0.21.6+{tag}", "-f", "https://whl.natten.org/"]
21
+ )
22
+
23
+
24
+ _ensure_natten()
25
+
26
+ import tempfile
27
+ import time
28
+ from collections import deque
29
+
30
+ import gradio as gr
31
+ import imageio.v2 as imageio
32
+ import numpy as np
33
+ from PIL import Image, ImageDraw
34
+
35
+ import sim
36
+ from flux_action.inference.so101 import load_policy
37
+
38
+ REPO = "black-forest-labs/flux-3-action-so101"
39
+ N_OBS = 8
40
+ N_EXEC = 32
41
+ CAM_W, CAM_H = 320, 240
42
+
43
+ policy = load_policy(REPO).to("cuda")
44
+
45
+
46
+ @spaces.GPU(duration=40)
47
+ def infer_chunk(scene_frames, wrist_frames, states, commands, prompt):
48
+ device = "cuda"
49
+ batch = {
50
+ "images.scene": torch.from_numpy(scene_frames).permute(0, 3, 1, 2)[None].to(device),
51
+ "images.wrist": torch.from_numpy(wrist_frames).permute(0, 3, 1, 2)[None].to(device),
52
+ "state": torch.from_numpy(states).float()[None].to(device),
53
+ "command_history": torch.from_numpy(commands).float()[None].to(device),
54
+ "task": [prompt],
55
+ }
56
+ with torch.inference_mode():
57
+ targets = policy.predict_normalized_targets(batch)
58
+ actions = policy.actions_from_targets(targets, batch["command_history"][:, -1])
59
+ return actions[0].float().cpu().numpy()
60
+
61
+
62
+ def compose(big, scene_img, wrist_img, caption):
63
+ h = big.shape[0]
64
+ side = h // 2
65
+ s = np.array(Image.fromarray(scene_img).resize((int(side * 4 / 3), side)))
66
+ w = np.array(Image.fromarray(wrist_img).resize((int(side * 4 / 3), side)))
67
+ frame = np.concatenate([big, np.concatenate([s, w], 0)], 1)
68
+ im = Image.fromarray(frame)
69
+ d = ImageDraw.Draw(im)
70
+ d.rectangle([0, h - 26, big.shape[1], h], fill=(0, 0, 0))
71
+ d.text((8, h - 20), caption, fill=(255, 255, 255))
72
+ d.text((big.shape[1] + 6, 4), "scene cam (model input)", fill=(255, 255, 255))
73
+ d.text((big.shape[1] + 6, side + 4), "wrist cam (model input)", fill=(255, 255, 255))
74
+ return np.array(im)
75
+
76
+
77
+ def rollout(prompt, scene, n_chunks, seed):
78
+ prompt = (prompt or "").strip()
79
+ if not prompt:
80
+ raise gr.Error("Enter a task instruction.")
81
+ env = sim.Sim(scene=scene, width=CAM_W, height=CAM_H, seed=int(seed))
82
+ history = deque(maxlen=N_OBS)
83
+ last_command = None
84
+ queue = deque()
85
+ frames = []
86
+ total_ticks = int(n_chunks) * N_EXEC
87
+ t0 = time.perf_counter()
88
+ gpu_time = 0.0
89
+ log = []
90
+ tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
91
+ for tick in range(total_ticks):
92
+ scene_img = env.render("scene")
93
+ wrist_img = env.render("wrist")
94
+ state = sim.lerobot_to_model(env.state_deg())
95
+ command = state if last_command is None else last_command
96
+ entry = (scene_img, wrist_img, state, command)
97
+ for _ in range(N_OBS if not history else 1):
98
+ history.append(entry)
99
+ if not queue:
100
+ g0 = time.perf_counter()
101
+ chunk = infer_chunk(
102
+ np.stack([h[0] for h in history]),
103
+ np.stack([h[1] for h in history]),
104
+ np.stack([h[2] for h in history]).astype(np.float32),
105
+ np.stack([h[3] for h in history]).astype(np.float32),
106
+ prompt,
107
+ )
108
+ gpu_time += time.perf_counter() - g0
109
+ queue.extend(chunk[:N_EXEC])
110
+ log.append(f"chunk {len(log) + 1}: {time.perf_counter() - g0:.1f}s")
111
+ action = np.asarray(queue.popleft(), dtype=np.float64)
112
+ last_command = action
113
+ env.step(sim.model_to_lerobot(action))
114
+ big = env.render_big()
115
+ frame = compose(big, scene_img, wrist_img, f"{prompt} t={tick / sim.FPS:4.1f}s")
116
+ frames.append(frame)
117
+ if tick % 8 == 0:
118
+ yield frame, None, "\n".join(log)
119
+ env.close()
120
+ imageio.mimsave(tmp, frames, fps=sim.FPS, codec="libx264", quality=7, macro_block_size=8)
121
+ log.append(f"total {time.perf_counter() - t0:.1f}s, model {gpu_time:.1f}s")
122
+ yield frames[-1], tmp, "\n".join(log)
123
+
124
+
125
+ def preview(scene, seed):
126
+ env = sim.Sim(scene=scene, width=CAM_W, height=CAM_H, seed=int(seed))
127
+ out = compose(env.render_big(), env.render("scene"), env.render("wrist"), "initial state")
128
+ env.close()
129
+ return out
130
+
131
+
132
+ with gr.Blocks(title="FLUX 3 Action SO-101 in MuJoCo") as demo:
133
+ gr.Markdown(
134
+ "# FLUX 3 Action · SO-101 in simulation\n"
135
+ "[black-forest-labs/flux-3-action-so101](https://huggingface.co/black-forest-labs/flux-3-action-so101) "
136
+ "drives a simulated SO-101 arm in MuJoCo, in closed loop. Each step renders the scene and wrist "
137
+ "cameras, the policy predicts 42 joint commands from the last 8 observations and your instruction, "
138
+ "the first 32 run at 30 Hz, then it replans. The policy was trained only on real SO-100/101 "
139
+ "teleoperation data, so the simulator is out of its training distribution."
140
+ )
141
+ with gr.Row():
142
+ with gr.Column(scale=1):
143
+ prompt = gr.Textbox(label="Task instruction", value="Pick up the red cube and put it in the gray container")
144
+ scene = gr.Dropdown(list(sim.SCENES), value="Cubes and tray", label="Scene")
145
+ n_chunks = gr.Slider(2, 12, value=6, step=1, label="Chunks (32 steps each, about 1.07 s)")
146
+ seed = gr.Number(value=0, precision=0, label="Object placement seed")
147
+ run = gr.Button("Run rollout", variant="primary")
148
+ log = gr.Textbox(label="Timing", lines=6)
149
+ with gr.Column(scale=2):
150
+ live = gr.Image(label="Live", value=None, interactive=False)
151
+ video = gr.Video(label="Rollout", autoplay=True)
152
+ gr.Examples(
153
+ [
154
+ ["Pick up the red cube and put it in the gray container", "Cubes and tray"],
155
+ ["Put the blue cube in the box", "Cubes and tray"],
156
+ ["Pick up the yellow block", "Cubes and tray"],
157
+ ["Grab the red cube and place it in the container", "Single cube"],
158
+ ["Stack the orange cube on the green cube", "Blocks without tray"],
159
+ ],
160
+ [prompt, scene],
161
+ cache_examples=False,
162
+ )
163
+ run.click(rollout, [prompt, scene, n_chunks, seed], [live, video, log])
164
+ scene.change(preview, [scene, seed], live)
165
+ seed.change(preview, [scene, seed], live)
166
+ demo.load(preview, [scene, seed], live)
167
+
168
+ demo.queue(default_concurrency_limit=2).launch()
assets/base_motor_holder_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424
3
+ size 1877084
assets/base_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d
3
+ size 471584
assets/motor_holder_so101_base_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b
3
+ size 1129384
assets/motor_holder_so101_wrist_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011
3
+ size 1052184
assets/moving_jaw_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d
3
+ size 1413584
assets/rotation_pitch_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34
3
+ size 883684
assets/sts3215_03a_no_horn_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887
3
+ size 865884
assets/sts3215_03a_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0
3
+ size 954084
assets/under_arm_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71
3
+ size 1975884
assets/upper_arm_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f
3
+ size 1303484
assets/waveshare_mounting_plate_so101_v2.stl ADDED
Binary file (62.8 kB). View file
 
assets/wrist_camera_mount_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e17a626158951ac8cdf8d960962c1a3c8bf23b64635a02f88b62016fe895cef8
3
+ size 173884
assets/wrist_camera_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1147857bb136876c81f98d18b533342bae2383f0e23bec216064c9f093bd3577
3
+ size 128934
assets/wrist_roll_follower_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4
3
+ size 1439884
assets/wrist_roll_pitch_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae
3
+ size 2699784
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ libosmesa6
2
+ libgl1
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flux-action[encoders] @ git+https://github.com/black-forest-labs/flux-action@e2dd1d8dbc5977b54315d61f7548c63c043d6d4f
2
+ mujoco
3
+ PyOpenGL
4
+ imageio[ffmpeg]
5
+ numpy<2.3
sim.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dataclasses import dataclass, field
3
+
4
+ import mujoco
5
+ import numpy as np
6
+
7
+ HERE = os.path.dirname(os.path.abspath(__file__))
8
+ FPS = 30
9
+ PHYSICS_DT = 1.0 / 600.0
10
+ SUBSTEPS = int(round(1.0 / FPS / PHYSICS_DT))
11
+ JOINTS = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"]
12
+ GRIPPER_CLOSED = -0.17453
13
+ GRIPPER_OPEN = 1.74533
14
+ REST_DEG = np.array([0.0, -92.0, 88.0, 72.0, 0.0, 2.0])
15
+
16
+ COLORS = {
17
+ "red": "0.80 0.12 0.10 1",
18
+ "blue": "0.10 0.25 0.75 1",
19
+ "green": "0.12 0.55 0.20 1",
20
+ "yellow": "0.95 0.78 0.10 1",
21
+ "white": "0.92 0.92 0.90 1",
22
+ "black": "0.08 0.08 0.08 1",
23
+ "orange": "0.95 0.45 0.08 1",
24
+ }
25
+
26
+ SCENES = {
27
+ "Cubes and tray": {
28
+ "objects": [
29
+ ("red", "cube", (0.22, 0.02), 0.0),
30
+ ("blue", "cube", (0.27, -0.08), 0.4),
31
+ ("yellow", "cube", (0.17, -0.12), 0.9),
32
+ ],
33
+ "tray": (0.16, 0.17),
34
+ },
35
+ "Single cube": {
36
+ "objects": [("red", "cube", (0.22, -0.04), 0.3)],
37
+ "tray": (0.16, 0.17),
38
+ },
39
+ "Blocks without tray": {
40
+ "objects": [
41
+ ("green", "cube", (0.22, 0.08), 0.2),
42
+ ("orange", "cube", (0.24, -0.06), 0.7),
43
+ ("white", "box", (0.17, 0.0), 0.0),
44
+ ],
45
+ "tray": None,
46
+ },
47
+ }
48
+
49
+
50
+ def _object_xml(i, color, kind, xy, yaw):
51
+ rgba = COLORS[color]
52
+ half = "0.015 0.015 0.015" if kind == "cube" else "0.02 0.03 0.015"
53
+ z = 0.0155
54
+ q = f"{np.cos(yaw / 2):.5f} 0 0 {np.sin(yaw / 2):.5f}"
55
+ return (
56
+ f'<body name="obj{i}" pos="{xy[0]} {xy[1]} {z}" quat="{q}">'
57
+ f'<freejoint name="obj{i}_free"/>'
58
+ f'<geom type="box" size="{half}" rgba="{rgba}" mass="0.02" friction="1.5 0.02 0.001" '
59
+ f'condim="4" solref="0.004 1" priority="1"/>'
60
+ f"</body>"
61
+ )
62
+
63
+
64
+ def _tray_xml(xy):
65
+ x, y = xy
66
+ hx, hy, hz, t = 0.075, 0.06, 0.022, 0.004
67
+ rgba = "0.55 0.56 0.58 1"
68
+ parts = [
69
+ f'<geom type="box" pos="0 0 {t / 2}" size="{hx} {hy} {t / 2}" rgba="{rgba}"/>',
70
+ f'<geom type="box" pos="{hx - t / 2} 0 {hz}" size="{t / 2} {hy} {hz}" rgba="{rgba}"/>',
71
+ f'<geom type="box" pos="{-hx + t / 2} 0 {hz}" size="{t / 2} {hy} {hz}" rgba="{rgba}"/>',
72
+ f'<geom type="box" pos="0 {hy - t / 2} {hz}" size="{hx} {t / 2} {hz}" rgba="{rgba}"/>',
73
+ f'<geom type="box" pos="0 {-hy + t / 2} {hz}" size="{hx} {t / 2} {hz}" rgba="{rgba}"/>',
74
+ ]
75
+ return f'<body name="tray" pos="{x} {y} 0">' + "".join(parts) + "</body>"
76
+
77
+
78
+ def scene_xml(scene):
79
+ spec = SCENES[scene]
80
+ objects = "".join(_object_xml(i, *o) for i, o in enumerate(spec["objects"]))
81
+ tray = _tray_xml(spec["tray"]) if spec["tray"] else ""
82
+ return f"""
83
+ <mujoco model="so101_tabletop">
84
+ <include file="so101.xml"/>
85
+ <option timestep="{PHYSICS_DT}" cone="elliptic" impratio="10"/>
86
+ <visual>
87
+ <headlight diffuse="0.35 0.35 0.35" ambient="0.35 0.35 0.35" specular="0 0 0"/>
88
+ <quality shadowsize="4096"/>
89
+ <global offwidth="640" offheight="480"/>
90
+ <map znear="0.005"/>
91
+ </visual>
92
+ <asset>
93
+ <texture type="skybox" builtin="gradient" rgb1="0.85 0.86 0.88" rgb2="0.55 0.56 0.58" width="256" height="1536"/>
94
+ <texture name="cork" type="2d" builtin="flat" rgb1="0.80 0.66 0.48" mark="random" markrgb="0.70 0.55 0.38" random="0.12" width="512" height="512"/>
95
+ <material name="cork" texture="cork" texrepeat="4 4" reflectance="0.0"/>
96
+ <material name="desk" rgba="0.80 0.81 0.82 1"/>
97
+ <material name="wall" rgba="0.90 0.90 0.88 1"/>
98
+ <material name="white_part" rgba="0.93 0.93 0.91 1"/>
99
+ </asset>
100
+ <worldbody>
101
+ <light pos="0.3 -0.3 1.2" dir="-0.2 0.2 -1" directional="false" diffuse="0.6 0.6 0.6" castshadow="true"/>
102
+ <light pos="-0.4 0.4 1.0" dir="0.3 -0.3 -1" diffuse="0.3 0.3 0.3" castshadow="false"/>
103
+ <geom name="desk" type="box" pos="0.25 0 -0.012" size="0.7 0.7 0.004" material="desk"/>
104
+ <geom name="board" type="box" pos="0.22 0 -0.004" size="0.26 0.32 0.004" material="cork" friction="1 0.01 0.001"/>
105
+ <geom name="wall" type="box" pos="-0.35 0 0.4" size="0.01 1.2 0.5" material="wall" contype="0" conaffinity="0"/>
106
+ <geom name="wall2" type="box" pos="0.3 0.75 0.4" size="1.0 0.01 0.5" material="wall" contype="0" conaffinity="0"/>
107
+ <camera name="scene" pos="0.46 -0.24 0.34" mode="targetbody" target="scene_target" fovy="58"/>
108
+ <camera name="overview" pos="0.62 -0.42 0.45" mode="targetbody" target="scene_target" fovy="50"/>
109
+ <body name="scene_target" pos="0.14 0.03 0.05"/>
110
+ {objects}
111
+ {tray}
112
+ </worldbody>
113
+ </mujoco>
114
+ """
115
+
116
+
117
+ def build_model(scene):
118
+ xml = scene_xml(scene)
119
+ cwd = os.getcwd()
120
+ os.chdir(HERE)
121
+ try:
122
+ model = mujoco.MjModel.from_xml_string(xml)
123
+ finally:
124
+ os.chdir(cwd)
125
+ return model
126
+
127
+
128
+ def deg_to_q(deg):
129
+ q = np.deg2rad(np.asarray(deg[:5], dtype=np.float64))
130
+ g = GRIPPER_CLOSED + np.clip(deg[5], 0.0, 100.0) / 100.0 * (GRIPPER_OPEN - GRIPPER_CLOSED)
131
+ return np.concatenate([q, [g]])
132
+
133
+
134
+ def q_to_deg(q):
135
+ deg = np.rad2deg(np.asarray(q[:5], dtype=np.float64))
136
+ g = (q[5] - GRIPPER_CLOSED) / (GRIPPER_OPEN - GRIPPER_CLOSED) * 100.0
137
+ return np.concatenate([deg, [g]])
138
+
139
+
140
+ def lerobot_to_model(deg):
141
+ out = np.array(deg, dtype=np.float64).copy()
142
+ out[1] = 90.0 - deg[1]
143
+ out[2] = deg[2] + 90.0
144
+ return out
145
+
146
+
147
+ def model_to_lerobot(val):
148
+ out = np.array(val, dtype=np.float64).copy()
149
+ out[1] = 90.0 - val[1]
150
+ out[2] = val[2] - 90.0
151
+ return out
152
+
153
+
154
+ @dataclass
155
+ class Sim:
156
+ scene: str = "Cubes and tray"
157
+ width: int = 256
158
+ height: int = 256
159
+ seed: int = 0
160
+ model: mujoco.MjModel = field(init=False)
161
+ data: mujoco.MjData = field(init=False)
162
+
163
+ def __post_init__(self):
164
+ self.model = build_model(self.scene)
165
+ self.data = mujoco.MjData(self.model)
166
+ self.qadr = np.array([self.model.joint(j).qposadr[0] for j in JOINTS])
167
+ self.renderer = mujoco.Renderer(self.model, self.height, self.width)
168
+ self.big = None
169
+ self.reset()
170
+
171
+ def reset(self):
172
+ mujoco.mj_resetData(self.model, self.data)
173
+ rng = np.random.default_rng(self.seed)
174
+ for i in range(self.model.nbody):
175
+ name = self.model.body(i).name
176
+ if name.startswith("obj"):
177
+ j = self.model.joint(f"{name}_free")
178
+ a = j.qposadr[0]
179
+ self.data.qpos[a : a + 2] += rng.uniform(-0.015, 0.015, 2)
180
+ q = deg_to_q(REST_DEG)
181
+ self.data.qpos[self.qadr] = q
182
+ self.data.ctrl[:] = q
183
+ mujoco.mj_forward(self.model, self.data)
184
+ for _ in range(120):
185
+ mujoco.mj_step(self.model, self.data)
186
+
187
+ def state_deg(self):
188
+ return q_to_deg(self.data.qpos[self.qadr])
189
+
190
+ def step(self, command_deg):
191
+ q = deg_to_q(command_deg)
192
+ lo = self.model.actuator_ctrlrange[:, 0]
193
+ hi = self.model.actuator_ctrlrange[:, 1]
194
+ self.data.ctrl[:] = np.clip(q, lo, hi)
195
+ for _ in range(SUBSTEPS):
196
+ mujoco.mj_step(self.model, self.data)
197
+
198
+ def render(self, camera):
199
+ self.renderer.update_scene(self.data, camera=camera)
200
+ return self.renderer.render().copy()
201
+
202
+ def render_big(self, camera="overview", width=640, height=480):
203
+ if self.big is None:
204
+ self.big = mujoco.Renderer(self.model, height, width)
205
+ self.big.update_scene(self.data, camera=camera)
206
+ return self.big.render().copy()
207
+
208
+ def close(self):
209
+ self.renderer.close()
210
+ if self.big is not None:
211
+ self.big.close()
212
+
213
+ def object_positions(self):
214
+ out = {}
215
+ for i in range(self.model.nbody):
216
+ name = self.model.body(i).name
217
+ if name.startswith("obj"):
218
+ out[name] = self.data.body(i).xpos.copy()
219
+ return out
so101.xml ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" ?>
2
+ <mujoco model="so101_new_calib_camera">
3
+ <compiler angle="radian" meshdir="assets" autolimits="true"/>
4
+ <default>
5
+ <default class="so101_new_calib">
6
+ <joint damping="1" frictionloss="0.1" armature="0.005"/>
7
+ <position kp="50"/>
8
+ <default class="visual">
9
+ <geom type="mesh" contype="0" conaffinity="0" group="2"/>
10
+ </default>
11
+ <default class="collision">
12
+ <geom group="3"/>
13
+ </default>
14
+ </default>
15
+ </default>
16
+ <default>
17
+ <default class="sts3215">
18
+ <geom contype="0" conaffinity="0"/>
19
+ <joint damping="0.60" frictionloss="0.052" armature="0.028"/>
20
+ <position kp="998.22" kv="2.731" forcerange="-2.94 2.94"/>
21
+ </default>
22
+ <default class="backlash">
23
+ <joint damping="0.01" frictionloss="0" armature="0.01" limited="true" range="-0.008726646259971648 0.008726646259971648"/>
24
+ </default>
25
+ </default>
26
+ <worldbody>
27
+ <body name="base" pos="0 0 0" quat="1 0 0 0" childclass="so101_new_calib">
28
+ <inertial pos="0.0137179 -5.19711e-05 0.0334843" mass="0.147" fullinertia="0.000114686 0.000136117 0.000130364 -4.59787e-07 4.97151e-06 9.75275e-08"/>
29
+ <geom type="mesh" class="visual" pos="-0.00636471 -9.94414e-05 -0.0024" quat="0.5 0.5 0.5 0.5" mesh="base_motor_holder_so101_v1" material="base_motor_holder_so101_v1_material"/>
30
+ <geom type="mesh" class="visual" pos="-0.00636471 -8.97657e-09 -0.0024" quat="0.5 0.5 0.5 0.5" mesh="base_so101_v2" material="base_so101_v2_material"/>
31
+ <geom type="mesh" class="visual" pos="0.0263353 -8.97657e-09 0.0437" quat="1 -2.85511e-16 -9.64433e-17 6.12908e-16" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
32
+ <geom type="mesh" class="visual" pos="-0.0309827 -0.000199441 0.0474" quat="0.5 0.5 0.5 0.5" mesh="waveshare_mounting_plate_so101_v2" material="waveshare_mounting_plate_so101_v2_material"/>
33
+ <site group="3" name="baseframe" pos="8.67362e-19 9.55596e-18 3.46945e-18" quat="1 -8.17396e-19 3.78392e-17 2.22045e-16"/>
34
+ <body name="shoulder" pos="0.0388353 -8.97657e-09 0.0624" quat="3.56167e-16 1.22818e-15 -1 -4.14635e-16">
35
+ <joint axis="0 0 1" name="shoulder_pan" type="hinge" range="-1.9198621771937616 1.9198621771937634" class="sts3215"/>
36
+ <inertial pos="-0.0307604 -1.66727e-05 -0.0252713" mass="0.100006" fullinertia="8.3759e-05 8.10403e-05 2.39783e-05 7.55525e-08 -1.16342e-06 1.54663e-07"/>
37
+ <geom type="mesh" class="visual" pos="-0.0303992 0.000422241 -0.0417" quat="0.5 0.5 0.5 -0.5" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
38
+ <geom type="mesh" class="collision" pos="-0.0303992 0.000422241 -0.0417" quat="0.5 0.5 0.5 -0.5" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
39
+ <geom type="mesh" class="visual" pos="-0.0675992 -0.000177759 0.0158499" quat="0.5 0.5 -0.5 0.5" mesh="motor_holder_so101_base_v1" material="motor_holder_so101_base_v1_material"/>
40
+ <geom type="mesh" class="collision" pos="-0.0675992 -0.000177759 0.0158499" quat="0.5 0.5 -0.5 0.5" mesh="motor_holder_so101_base_v1" material="motor_holder_so101_base_v1_material"/>
41
+ <geom type="mesh" class="visual" pos="0.0122008 2.22413e-05 0.0464" quat="0.707107 -0.707107 -0 8.3163e-34" mesh="rotation_pitch_so101_v1" material="rotation_pitch_so101_v1_material"/>
42
+ <geom type="mesh" class="collision" pos="0.0122008 2.22413e-05 0.0464" quat="0.707107 -0.707107 -0 8.3163e-34" mesh="rotation_pitch_so101_v1" material="rotation_pitch_so101_v1_material"/>
43
+ <body name="upper_arm" pos="-0.0303992 -0.0182778 -0.0542" quat="0.5 -0.5 -0.5 -0.5">
44
+ <joint axis="0 0 1" name="shoulder_lift" type="hinge" range="-1.7453292519943224 1.7453292519943366" class="sts3215"/>
45
+ <inertial pos="-0.0898471 -0.00838224 0.0184089" mass="0.103" fullinertia="4.08002e-05 0.000147318 0.000142487 -1.97819e-05 -4.03016e-08 8.97326e-09"/>
46
+ <geom type="mesh" class="visual" pos="-0.11257 -0.0155 0.0187" quat="4.56308e-16 -0.707107 0.707107 -1.37383e-16" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
47
+ <geom type="mesh" class="collision" pos="-0.11257 -0.0155 0.0187" quat="4.56308e-16 -0.707107 0.707107 -1.37383e-16" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
48
+ <geom type="mesh" class="visual" pos="-0.065085 0.012 0.0182" quat="0 1 0 0" mesh="upper_arm_so101_v1" material="upper_arm_so101_v1_material"/>
49
+ <geom type="mesh" class="collision" pos="-0.065085 0.012 0.0182" quat="0 1 0 0" mesh="upper_arm_so101_v1" material="upper_arm_so101_v1_material"/>
50
+ <body name="lower_arm" pos="-0.11257 -0.028 1.73763e-16" quat="0.707107 -5.98613e-17 -2.58051e-17 0.707107">
51
+ <joint axis="0 0 1" name="elbow_flex" type="hinge" range="-1.69 1.69" class="sts3215"/>
52
+ <inertial pos="-0.0980701 0.00324376 0.0182831" mass="0.104" fullinertia="2.87438e-05 0.000159844 0.00014529 7.41152e-06 1.26409e-06 -4.90188e-08"/>
53
+ <geom type="mesh" class="visual" pos="-0.0648499 -0.032 0.0182" quat="0 1 0 0" mesh="under_arm_so101_v1" material="under_arm_so101_v1_material"/>
54
+ <geom type="mesh" class="collision" pos="-0.0648499 -0.032 0.0182" quat="0 1 0 0" mesh="under_arm_so101_v1" material="under_arm_so101_v1_material"/>
55
+ <geom type="mesh" class="visual" pos="-0.0648499 -0.032 0.018" quat="3.92687e-16 -1 -1.9186e-15 -6.38378e-16" mesh="motor_holder_so101_wrist_v1" material="motor_holder_so101_wrist_v1_material"/>
56
+ <geom type="mesh" class="collision" pos="-0.0648499 -0.032 0.018" quat="3.92687e-16 -1 -1.9186e-15 -6.38378e-16" mesh="motor_holder_so101_wrist_v1" material="motor_holder_so101_wrist_v1_material"/>
57
+ <geom type="mesh" class="visual" pos="-0.1224 0.0052 0.0187" quat="7.21645e-16 1.56949e-15 1 -3.33067e-16" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
58
+ <geom type="mesh" class="collision" pos="-0.1224 0.0052 0.0187" quat="7.21645e-16 1.56949e-15 1 -3.33067e-16" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
59
+ <body name="wrist" pos="-0.1349 0.0052 3.62355e-17" quat="0.707107 9.58722e-16 -7.51313e-16 -0.707107">
60
+ <joint axis="0 0 1" name="wrist_flex" type="hinge" range="-1.6580628494556928 1.6580627293335335" class="sts3215"/>
61
+ <inertial pos="-0.000103312 -0.0386143 0.0281156" mass="0.079" fullinertia="3.68263e-05 2.5391e-05 2.1e-05 1.7893e-08 -5.28128e-08 3.6412e-06"/>
62
+ <geom type="mesh" class="visual" pos="8.32667e-17 -0.0424 0.0306" quat="0.5 0.5 0.5 -0.5" mesh="sts3215_03a_no_horn_v1" material="sts3215_03a_no_horn_v1_material"/>
63
+ <geom type="mesh" class="collision" pos="8.32667e-17 -0.0424 0.0306" quat="0.5 0.5 0.5 -0.5" mesh="sts3215_03a_no_horn_v1" material="sts3215_03a_no_horn_v1_material"/>
64
+ <geom type="mesh" class="visual" pos="0 -0.028 0.0181" quat="0.5 -0.5 -0.5 -0.5" mesh="wrist_roll_pitch_so101_v2" material="wrist_roll_pitch_so101_v2_material"/>
65
+ <geom type="mesh" class="collision" pos="0 -0.028 0.0181" quat="0.5 -0.5 -0.5 -0.5" mesh="wrist_roll_pitch_so101_v2" material="wrist_roll_pitch_so101_v2_material"/>
66
+ <body name="gripper" pos="5.55112e-17 -0.0611 0.0181" quat="0.0172091 -0.0172091 0.706897 0.706897">
67
+ <camera name="wrist" pos="0.003 0.066 -0.024" xyaxes="-1 0 0 0 -0.906 0.424" fovy="80"/>
68
+ <joint axis="0 0 1" name="wrist_roll" type="hinge" range="-2.7438472969992493 2.841206309382605" class="sts3215"/>
69
+ <inertial pos="0.000213627 0.000245138 -0.025187" mass="0.087" fullinertia="2.75087e-05 4.33657e-05 3.45059e-05 -3.35241e-07 -5.7352e-06 -5.17847e-08"/>
70
+ <geom type="mesh" class="visual" pos="0.0077 0.0001 -0.0234" quat="0.707107 -0.707107 1.66015e-15 6.45094e-15" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
71
+ <geom type="mesh" class="collision" pos="0.0077 0.0001 -0.0234" quat="0.707107 -0.707107 1.66015e-15 6.45094e-15" mesh="sts3215_03a_v1" material="sts3215_03a_v1_material"/>
72
+ <geom type="mesh" class="visual" pos="8.32667e-17 -0.000218214 0.000949706" quat="0 1 0 0" mesh="wrist_roll_follower_so101_v1" material="wrist_roll_follower_so101_v1_material"/>
73
+ <geom type="mesh" class="collision" pos="8.32667e-17 -0.000218214 0.000949706" quat="0 1 0 0" mesh="wrist_roll_follower_so101_v1" material="wrist_roll_follower_so101_v1_material"/>
74
+ <site group="3" name="gripperframe" pos="-0.0079 -0.000218121 -0.0981274" quat="0.707107 -0 0.707107 -2.37788e-17"/>
75
+ <body name="wrist_camera_mount" pos="-0.0150 0.0240 -0.0315" quat="0.4997981226682881 -0.5001999599912927 0.4998017973423795 -0.5001999599912927">
76
+ <inertial pos="0 0 0" mass="0.02" diaginertia="1e-5 1e-5 1e-5"/>
77
+ <geom type="mesh" class="visual" mesh="wrist_camera_mount_so101_v1" material="wrist_camera_mount_so101_v1_material"/>
78
+ <geom type="mesh" class="collision" mesh="wrist_camera_mount_so101_v1" material="wrist_camera_mount_so101_v1_material"/>
79
+ <body name="wrist_camera" pos="-0.04941 -0.03813 0.0175" quat="0.37978693649389456 -0.5964584309361936 -0.37978833153177 -0.596456240026065">
80
+ <inertial pos="0 0 0" mass="0.015" diaginertia="1e-5 1e-5 1e-5"/>
81
+ <geom type="mesh" class="visual" mesh="wrist_camera_so101_v1" material="wrist_camera_so101_v1_material"/>
82
+ <geom type="mesh" class="collision" mesh="wrist_camera_so101_v1" material="wrist_camera_so101_v1_material"/>
83
+ </body>
84
+ </body>
85
+ <body name="moving_jaw_so101_v1" pos="0.0202 0.0188 -0.0234" quat="0.707107 0.707107 -1.85362e-08 1.85362e-08">
86
+ <joint axis="0 0 1" name="gripper" type="hinge" range="-0.17453297762778586 1.7453291995659765" class="sts3215"/>
87
+ <inertial pos="-0.00157495 -0.0300244 0.0192755" mass="0.012" fullinertia="6.61427e-06 1.89032e-06 5.28738e-06 -3.19807e-07 -5.90717e-09 -1.09945e-07"/>
88
+ <geom type="mesh" class="visual" pos="-5.55112e-17 -5.55112e-17 0.0189" quat="1 -0 3.00524e-16 -2.00834e-17" mesh="moving_jaw_so101_v1" material="moving_jaw_so101_v1_material"/>
89
+ <geom type="mesh" class="collision" pos="-5.55112e-17 -5.55112e-17 0.0189" quat="1 -0 3.00524e-16 -2.00834e-17" mesh="moving_jaw_so101_v1" material="moving_jaw_so101_v1_material"/>
90
+ </body>
91
+ </body>
92
+ </body>
93
+ </body>
94
+ </body>
95
+ </body>
96
+ </body>
97
+ </worldbody>
98
+ <asset>
99
+ <mesh file="waveshare_mounting_plate_so101_v2.stl"/>
100
+ <mesh file="sts3215_03a_v1.stl"/>
101
+ <mesh file="motor_holder_so101_base_v1.stl"/>
102
+ <mesh file="wrist_roll_follower_so101_v1.stl"/>
103
+ <mesh file="moving_jaw_so101_v1.stl"/>
104
+ <mesh file="base_motor_holder_so101_v1.stl"/>
105
+ <mesh file="upper_arm_so101_v1.stl"/>
106
+ <mesh file="wrist_roll_pitch_so101_v2.stl"/>
107
+ <mesh file="under_arm_so101_v1.stl"/>
108
+ <mesh file="rotation_pitch_so101_v1.stl"/>
109
+ <mesh file="motor_holder_so101_wrist_v1.stl"/>
110
+ <mesh file="sts3215_03a_no_horn_v1.stl"/>
111
+ <mesh file="base_so101_v2.stl"/>
112
+ <mesh file="wrist_camera_mount_so101_v1.stl" scale="0.001 0.001 0.001"/>
113
+ <mesh file="wrist_camera_so101_v1.stl"/>
114
+ <material name="base_motor_holder_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
115
+ <material name="base_so101_v2_material" rgba="0.94 0.94 0.92 1"/>
116
+ <material name="sts3215_03a_v1_material" rgba="0.1 0.1 0.1 1"/>
117
+ <material name="waveshare_mounting_plate_so101_v2_material" rgba="0.94 0.94 0.92 1"/>
118
+ <material name="motor_holder_so101_base_v1_material" rgba="0.94 0.94 0.92 1"/>
119
+ <material name="rotation_pitch_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
120
+ <material name="upper_arm_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
121
+ <material name="under_arm_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
122
+ <material name="motor_holder_so101_wrist_v1_material" rgba="0.94 0.94 0.92 1"/>
123
+ <material name="sts3215_03a_no_horn_v1_material" rgba="0.1 0.1 0.1 1.0"/>
124
+ <material name="wrist_roll_pitch_so101_v2_material" rgba="0.94 0.94 0.92 1"/>
125
+ <material name="wrist_roll_follower_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
126
+ <material name="moving_jaw_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
127
+ <material name="wrist_camera_mount_so101_v1_material" rgba="0.94 0.94 0.92 1"/>
128
+ <material name="wrist_camera_so101_v1_material" rgba="0.1 0.1 0.1 1"/>
129
+ </asset>
130
+ <actuator>
131
+ <position class="sts3215" name="shoulder_pan" joint="shoulder_pan" forcerange="-3.35 3.35" ctrlrange="-1.91986 1.91986"/>
132
+ <position class="sts3215" name="shoulder_lift" joint="shoulder_lift" forcerange="-3.35 3.35" ctrlrange="-1.74533 1.74533"/>
133
+ <position class="sts3215" name="elbow_flex" joint="elbow_flex" forcerange="-3.35 3.35" ctrlrange="-1.69 1.69"/>
134
+ <position class="sts3215" name="wrist_flex" joint="wrist_flex" forcerange="-3.35 3.35" ctrlrange="-1.65806 1.65806"/>
135
+ <position class="sts3215" name="wrist_roll" joint="wrist_roll" forcerange="-3.35 3.35" ctrlrange="-2.74385 2.84121"/>
136
+ <position class="sts3215" name="gripper" joint="gripper" forcerange="-3.35 3.35" ctrlrange="-0.17453 1.74533"/>
137
+ </actuator>
138
+ <equality/>
139
+ </mujoco>