RoboSpace runs Python directly in your browser, on real MuJoCo physics compiled to WebAssembly. There is nothing to install and no network round trip.
Motion calls are synchronous, and they do not need await. Stepping is 100–1000× faster than real time, so two seconds of simulation is tens of milliseconds of actual work. A motion therefore runs to completion inside the call, recording frames as it goes, and the viewport replays that recording afterwards at normal speed.
One consequence surprises everyone once: your script prints Execution completed while the robot is still moving. That is the replay catching up, not a bug. Anything you read back — body_pos(), get_qpos() — is already the true final value. Call skip_playback() if you would rather jump straight to the end state.
await is needed in exactly one place: calls that fetch over the network. load_robot(), load_scene() and load_metaworld() download 30–70 MB of robot assets, and the vla_* calls reach an inference server.
Control a UR5e manipulator to move to spatial target coordinates and close its end-effector gripper.
import time
# Initialize high-level robot instance
robot = get_robot()
print("Moving UR5e arm to target [0.4, 0.0, 0.25]...")
robot.arm.move_to([0.40, 0.00, 0.25])
print("Closing gripper...")
robot.gripper.close()
# Pause execution for 1 second in physics time
wait(1.0)Downloads a robot from MuJoCo Menagerie and stands it on a floor. Must be awaited — this fetches 30–70 MB the first time, then caches it in your browser, so a second load is fast.
print(list_robots())
await load_robot("stretch_3")
print(get_actuator_names())
# Drive the wheels
set_control([0.3, 0.3, 0.6, 0.1, 0, 0, 0, 0, 0, 0])Compiles and loads a scene you author yourself. Pass robot to have a registry robot pulled in and placed at its home pose.
Put <include> first. The robot's home pose is written into the leading joint slots, and <include> being first is what puts those joints first. Get the order wrong and the pose lands on the wrong joints, with no error.
SCENE = '''<mujoco model="pick">
<include file="panda.xml"/>
<worldbody>
<light pos="0 0 3" dir="0 0 -1" directional="true"/>
<geom name="floor" size="0 0 0.05" type="plane"/>
<body name="cube" pos="0.5 0 0.025">
<freejoint/>
<geom type="box" size="0.025 0.025 0.025" rgba="0.85 0.3 0.3 1"/>
</body>
</worldbody>
</mujoco>'''
await load_scene(SCENE, robot="franka_panda", name="pick")
print(body_pos("cube"))Instantiates a component-based Robot interface providing unified access to arm kinematics, gripper control, mobile base velocity, and joint references.
robot = get_robot()
print(robot)
# Output: <Robot with 6 joints, arm=tool0>Moves the arm end-effector tool frame to target 3D Cartesian coordinates [X, Y, Z] in metres, solving the joint angles with inverse kinematics. Pass quat to also fix the orientation; omit it and the orientation is left free. Raises if the pose cannot be reached, rather than moving somewhere approximate.
robot = get_robot()
# Move the end-effector to [0.4, 0.1, 0.3] over 1.5 seconds
robot.arm.move_to([0.40, 0.10, 0.30], seconds=1.5)
# Same target, but held pointing straight down
robot.arm.move_to([0.40, 0.10, 0.30], quat=tool_down(), seconds=1.5)Returns the robot arm to its default home joint position configuration.
robot = get_robot()
robot.arm.home()Opens or closes the end-effector parallel gripper mechanism over the specified time duration.
robot = get_robot()
robot.gripper.open()
wait(0.5)
robot.gripper.close()Sets a partial gripper opening. opening is normalised, not a width in metres: 1.0 is fully open and 0.0 fully closed, mapped onto the actuator's own control range.
robot = get_robot()
# Half open
robot.gripper.set(0.5)
# Nearly closed, over 1.2 seconds
robot.gripper.set(0.05, seconds=1.2)Drives mobile base velocity actuators with linear forward velocity (m/s) and angular turning velocity (rad/s) for a specified duration.
robot = get_robot()
# Drive forward at 0.5 m/s while turning at 0.2 rad/s for 2.0s
robot.base.drive(forward=0.5, turn=0.2, seconds=2.0)
robot.base.stop()Retrieves a joint proxy by name and drives it to a target angle.
Units are inferred. A magnitude above 2π (6.283) is read as degrees; anything smaller is read as radians. So set_angle(-90) is −90°, but set_angle(5) is 5 radians, not 5°. Pass radians explicitly if the value may be small.
robot = get_robot()
# Rotate wrist_1 to -90 degrees
robot.get_joint("wrist_1").set_angle(-90.0)
# Small angles must be given in radians
import math
robot.get_joint("wrist_1").set_angle(math.radians(5))Pauses Python script execution while advancing physics stepping smoothly in synchronized simulation time.
# Pause execution for 2.5 seconds of simulation time
wait(2.5)Lets the simulation run for seconds, holding the current control targets. wait and sleep are the same function, and time.sleep is redirected to it too — so a script that calls time.sleep(1) steps physics rather than blocking.
set_control([0.5] * get_num_actuators())
run(2.0)Moves the actuators driving these joints to the given values, ramping over seconds. Takes the same {name: value} mapping that ik_solve() returns under joints.
r = ik_solve("attachment_site", pos=[0.4, 0.0, 0.3])
if r["success"]:
move_joints(r["joints"], seconds=2.0)Module-level equivalents of the robot.gripper methods. opening is normalised: 1.0 fully open, 0.0 fully closed.
open_gripper()
move_to("body:hand", pos=[0.5, 0, 0.13], quat=tool_down())
close_gripper()
run(1.0)
print(body_pos("cube"))Jumps straight to the current state instead of watching the recorded motion replay. Useful when you are running many motions and do not want to wait for each animation.
for x in [0.3, 0.4, 0.5]:
move_to("body:hand", pos=[x, 0, 0.3])
skip_playback()
print(body_pos("hand"))The advanced path, for when a command must depend on what just happened. Calls fn(t) every frame, where t is seconds since the loop started. Unlike the motion helpers this one is async, because it yields to the renderer each frame.
Inside any other long-running loop of your own, await yield_control() serves the same purpose.
import math
def wave(t):
set_actuator(0, 0.5 * math.sin(2 * t))
await control_loop(wave, duration=5.0)Returns the unique string identifier of the 3D body currently selected or double-clicked by the user on the viewport stage.
selected = get_selected_body()
if selected:
print(f"User selected body: {selected}")
else:
print("No 3D object selected.")Retrieves spatial position [X,Y,Z] array and orientation quaternion [W,X,Y,Z] array for any named body in world coordinates.
# Track spatial coordinates of an interactive stage target
target_name = get_selected_body() or "box_target"
pos = body_pos(target_name)
print(f"Target X: {pos[0]:.3f}, Y: {pos[1]:.3f}, Z: {pos[2]:.3f}")
# Move robot arm to target spatial position
robot = get_robot()
robot.arm.move_to([pos[0], pos[1], pos[2] + 0.1])Prints telemetry analysis of the currently selected 3D body, including position vector and ready-to-use code snippet.
print_selection()
# Output:
# Selected 3D Body: 'red_cube'
# Position: [0.35, -0.12, 0.15]
# Snippet: body_pos('red_cube')Moves a named frame to a pose, solving the joint angles for you. This is the one you usually want.
target is a frame name, not a position. Pass a site name, or "body:name" / "geom:name" to aim at one of those. The position goes in pos. Give pos, quat, or both — omitting one leaves that free, so “reach this point, any orientation” is a natural request.
Raises ValueError if the pose cannot be reached, rather than moving somewhere approximate: a silent near-miss makes the next step fail for a reason that looks like physics.
# Move the hand to a point, orientation free
move_to("body:hand", pos=[0.3, 0.2, 0.4], seconds=2.0)
# Same point, tool pointing straight down
move_to("body:hand", pos=[0.3, 0.2, 0.4], quat=tool_down())
# Aim a named site instead
move_to("attachment_site", pos=[0.5, 0.0, 0.2])Finds joint values that put a frame at a pose. Does not move the robot — qpos and qvel are restored before it returns, so nothing you are looking at moves.
Returns a dict, not an array: success, reason, qpos (the full vector), joints (a name → value mapping), pos_err, rot_err, iters, seeds_tried, ms.
It solves only against joints an actuator can actually drive, so a solution is always commandable. Defaults to 16 random restarts: with one seed a 6-DoF solve fails about 29 % of the time, with 16 it does not, and the mean number actually tried is under two.
r = ik_solve("attachment_site", pos=[0.4, 0.0, 0.25], quat=tool_down())
if r["success"]:
print(f"solved in {r['ms']:.1f} ms, error {r['pos_err'] * 1000:.2f} mm")
move_joints(r["joints"], seconds=1.5)
else:
print(f"no solution: {r['reason']}")Returns standard downward-pointing orientation quaternion [W, X, Y, Z] for pick-and-place end-effectors.
down_quat = tool_down()
# Use downward orientation quaternion in IK solver
q = ik_solve([0.3, 0.0, 0.2], down_quat)Lists every helper available, generated from what is actually defined in the running session — so unlike this page, it cannot fall out of date. Coroutines are marked so you know which need await. If anything here disagrees with help_api(), trust help_api().
help_api()Prints the loaded model: actuators with the joints they drive and their limits, then joints and sites. The fastest way to find out what you can actually command on an unfamiliar robot.
await load_robot("stretch_3")
print_model()Names in model order. body_names()[0] is always "world". Sites are the usual way a model marks a tool tip or a grasp point, which is why they are what you normally aim move_to() at.
print(site_names())
print(actuator_names())joint_limits() returns (low, high), or None when the joint is unlimited.
actuator_qpos_index() gives the index in get_qpos() of the joint an actuator drives. You need this to read a joint back correctly. Indexing qpos by actuator number happens to work on a plain 6-joint arm and is wrong on anything with a floating base or a gripper.
for name in actuator_names():
i = actuator_qpos_index(name)
if i is not None:
print(f"{name}: qpos[{i}] = {get_qpos()[i]:.3f} limits={joint_limits(name)}")World pose of a body, site, geom or camera. These return raw MuJoCo coordinates — Z-up, in metres — which match the frame your scene XML is written in.
print(body_pos("cube"))
print(site_pos("attachment_site"))
print(frame("body", "hand"))Retrieves total actuator count and control limit range tuples [[min, max], ...] from the active physics simulation model.
n = get_num_actuators()
ranges = get_actuator_ranges()
for i in range(n):
print(f"Actuator {i}: limits = {ranges[i]}")Direct low-level actuator control input setter and getter.
import math
n = get_num_actuators()
# Apply sine wave velocity controls across actuators
t = get_time()
ctrl = [0.5 * math.sin(t + i * 0.5) for i in range(n)]
set_control(ctrl)Returns complete physics joint position and joint velocity state arrays.
qpos = get_qpos()
qvel = get_qvel()
print(f"Joint Positions (qpos): {qpos[:6].round(3)}")
print(f"Joint Velocities (qvel): {qvel[:6].round(3)}")Manually advances physics simulation by n timestep increments or resets simulation to initial keyframe state.
# Step simulation forward by 10 timesteps
step(10)
# Reset simulation state
reset()RoboSpace supports loading custom robot models from raw URDF, MJCF XML, or compressed ZIP packages (containing meshes, textures, and XML definitions).
When importing a new robot model, the system automatically parses kinematically linked rigid bodies, identifies revolute and prismatic joints, generates position/velocity actuators, and sets up high-level Python API component bindings.
# Programmatically inspecting imported custom robot model
robot = get_robot()
print(f"Loaded joints: {list(robot.joints.keys())}")
# Set joint targets on custom imported robot
if "shoulder_pan_joint" in robot.joints:
robot.get_joint("shoulder_pan_joint").set_angle(45.0)Retrieves sensor telemetry array (force sensors, touch sensors, IMU gyros, joint velocity sensors) defined in the robot scene model.
# Print available sensors and camera feeds
print_sensors()
# Read sensor data array
data = get_sensor_data()
print(f"Readings: {data}")Returns list of named viewpoints and camera perspective configurations attached to the robot or environment stage.
cameras = get_camera_names()
print(f"Available camera perspectives: {cameras}")A vision-language-action policy can drive the robot from a sentence. The policy runs on an inference server, not in your browser: the simulator sends it camera frames plus the robot state, and gets back a chunk of actions.
You must be signed in. Inference is proxied through RoboSpace so the server credential is never shipped to the browser, and requests are rate limited — the board runs one inference at a time.
# Check the inference path before spending episodes on it
print(vla_ready())Drives the arm with the policy. Each returned action is an end-effector delta, applied through inverse kinematics, one action per rendered frame. Returns {steps, ik_failures}.
Worth knowing when you read the numbers: the policy was trained under operational-space control, while this simulator drives joint targets through IK. That is a different plant, so transfer is an empirical question rather than a given — which is what ik_failures and the benchmark below are for.
await load_scene(SCENE, robot="franka_panda", name="pick")
result = await vla_control_loop("pick up the red block", duration=20.0)
print(result)Runs the policy on a Meta-World scene, where the Sawyer arm is driven by a welded mocap body rather than by IK. Load the scene first with await load_metaworld().
task accepts a plain sentence as well as an id. Matching is deliberately conservative: it declines rather than guesses when a sentence does not clearly pick one task.
By default the policy is sent the task's own training sentence rather than your wording, because the checkpoints are sensitive to exact phrasing. Pass verbatim=True to send your words instead — it will tell you it is going off-distribution.
await load_metaworld()
# Resolved by sentence: "shut" folds to "close"
await vla_metaworld("shut the drawer")
# Or by task id
await vla_metaworld(19)The Dataset panel in the editor toolbar records scripted demonstrations and exports them as Hugging Face LeRobot v2.0 datasets — either as a ZIP download or pushed straight to the Hub.
Frames are captured from inside motion calls, at the sim-time rate you choose. That means only time spent inside a motion call is recorded: a bare set_control() with no run() after it contributes no frames.