Physical AI Sprint
Monday, August 17, 2026 · A hackathon alongside Actuate SF

The Physical AI Sprint

Get warmed up for Actuate the best way possible — by building. Join Nebius, NVIDIA, Antioch, and Toloka for a day of workshops and building at the intersection of AI and the physical world. Start with technical workshops on the tools and challenge space, then form a team — or work solo — and build a working prototype.

100–125 participants Teams of 4–5 One day Antioch + SO-101 + Unitree
Brought to you by
NebiusNVIDIAAntiochToloka

What to expect

We'll begin with technical workshops designed to help you get familiar with the tools, infrastructure, and challenge space. Then, you'll form a team — or work independently — and spend the rest of the day building a working prototype.

  • Robotics workshops
  • A focused physical AI challenge
  • Access to tools, infrastructure, and technical support
  • Project demos and judging
  • Prizes for the strongest submissions

01The challenge

The perception–reasoning–action loop

Modern AI excels at generating text and pixels. Physical AI requires closing the loop between the digital mind and a physical embodiment, integrating three pillars:

Perception · sense

Interpret camera and sensor data into a live understanding of a scene.

Ingesting multi-modal sensor data — RGB-D video, LiDAR, IMU readings, tactile feedback, spatial audio — into a real-time, dynamic understanding of the physical surroundings.

Reasoning · think

Turn goals into multi-step behavior and adapt when the world changes.

Leveraging foundation models, spatial computing, and decision-making frameworks to analyze constraints, anticipate outcomes, plan multi-step tasks, and adapt to unexpected real-world changes.

Action · act

Translate plans into robot motion that actually works.

Translating high-level plans into precise physical outputs: robotic manipulation, autonomous navigation, drone flight dynamics, or interactive IoT actuator responses.

Your goal: design and demonstrate a Physical AI task in simulation, in the real world, or across both. You have several robot modalities, the Antioch simulation platform for building and testing, and a single day. The brief is deliberately open-ended: we provide the robots, the world, and the building blocks, and you decide what the system does and how it thinks. In short, build some cool stuff.

Three project directions

All are equally viable.

  • Sim only. Build and demonstrate entirely on the Antioch platform. No hardware queue, unlimited resets, and every robot modality is available.
  • Hardware only. Build directly on the physical robots at the hardware stations.
  • Sim and real. Show a trajectory between the two: prototype or train in sim and run it on the real robot (sim-to-real), or capture real demonstrations and bring them into sim (real-to-sim).
Planning note
Hardware is limited and shared across all teams, so simulation is the dependable path. The strongest projects will likely run most of their loop in sim even when they touch hardware.

02Format

100 to 125 participants, hacking in teams of 4 or 5.

03Schedule

Monday, August 17, 2026 · Subject to change

Doors open and check-in, grab breakfast
Welcome and challenge briefing
Technical workshops by hosts
Hacking begins
Lunch
Hacking continues
Submission deadline
Demos and judging (top 6 live demos)
Winners announced
Happy hour

04What you get

The Antioch platform

Antioch is a cloud simulation platform for robotics. You write ordinary Python against the NVIDIA Isaac stack (Isaac Sim and Isaac Lab are both available) on your own laptop, and Antioch dispatches it to GPU machines on Nebius. Nothing heavy runs locally: no GPU, no Docker, no Isaac install, and macOS and Linux both work.

You can develop interactively by building simulations and streaming them into the browser. You can also define parameterized simulation scenarios and run variants at scale headlessly. Scenario runs are recorded and replayable, with metrics, logs, live viewport streams, and 3D recordings you can open in the browser.

Your team will receive a link to sign up for the Antioch Console. Each team gets its own development environment where teammates share machines and scenario runs and can see each other's work as it lands. Signup links will be distributed over email once teams are formed.

Start here
For how the platform actually works, start with the Antioch product docs: console.poc.antioch.com/docs/welcome

Robot hardware

10 ×

LeRobot SO-101 pairs

Leader/follower arms. Setup documentation for Linux and Windows; for macOS, use Guide 1.

2 ×

Unitree Go2-W

Wheeled quadrupeds. Documentation at github.com/unitreerobotics.

1 ×

Unitree G1

Humanoid. Documentation at github.com/unitreerobotics.

Hardware stations are shared between teams and staffed by the host teams.

Sim-ready assets

  • Robots. A calibrated SO-101 digital twin, plus the Unitree lineup in the Isaac asset library (Go2, G1, and others; Guide 4 lists what is on the shelf).
  • Environments. Ready-made scenes to stage your task in.
  • Objects. Containers, fixtures, and props, so composing a scene takes minutes rather than hours of importing and tuning your own meshes.

Physical assets for SO-101

  • Blocks of various size, shape, and color
  • Vials and vial racks

Frontier models: Kimi K3 on Nebius Token Factory

Nebius Token Factory serves open-weight frontier models over an OpenAI-compatible API. Kimi K3 is Moonshot AI's open-weight flagship — a mixture-of-experts model with a 1M-token context window, built for agentic work. New accounts get $25 in Token Factory credits plus $25 in Tavily credits for live web search.

In your robot's reasoning layer

This is the pillar that is easiest to underbuild. Any OpenAI client works — point it at Token Factory and use it for natural-language tasking, multi-step planning, or deciding what to do when a grasp fails.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.tokenfactory.nebius.com/v1/",
    api_key=os.environ["NEBIUS_API_KEY"],
)

plan = client.chat.completions.create(
    model="moonshotai/Kimi-K3",
    messages=[
        {"role": "system", "content": "Turn goals into SO-101 pick-and-place steps."},
        {"role": "user", "content": "Put the green cube in the tray, then reset."},
    ],
)
print(plan.choices[0].message.content)

In your coding agent

kimirelay routes Claude Code, Codex CLI, OpenCode, or Pi Code through Token Factory for a single run, without editing your real agent config — useful when you are building fast and would rather not spend your own subscription limits on it.

curl -fsSL https://kimirelay.com/install.sh | sh
kimirelay configure    # your Nebius key, plus an optional Tavily key for web search
klaude                 # or kodex, openkode, kpi
CommandLaunches
klaudeClaude Code
kodexCodex CLI
openkodeOpenCode
kpiPi Code
Put --model before the harness
The short aliases expand to kimirelay <harness>, so a flag placed after the harness name is passed through to the agent and silently dropped — you get the default model instead of the one you asked for. kimirelay --model moonshotai/Kimi-K2.6 codex exec "…" works; kodex --model … does not.
If K3 is unavailable
Nebius capacity for Kimi K3 can be intermittent. moonshotai/Kimi-K2.7-Code (coding, 262K context) and moonshotai/Kimi-K2.6 (vision + reasoning) are the fallbacks. Keys: tokenfactory.nebius.com. Relay docs: kimirelay.com.

Support

Engineers from the host companies are on hand all day for platform, hardware, and training questions.

05Getting started guides

The four guides at the back of this handbook each take you from zero to a working proof of concept with the hardware or with Antioch.

Guide 1

LeRobot on macOS

From a Mac with nothing installed to a real arm following your hand.

Guide 2

Sim teleoperation

Drive the Antioch sim with a real SO-101 leader arm and record scored demonstration episodes.

Guide 3

Policy training

Train an ACT or Diffusion policy on sim data, evaluate it closed-loop, and run it on a real arm.

Guide 4

Unitree robots on Antioch

Spawn a quadruped or humanoid, give it a controller, and verify it on physics rather than video.

These are not the assignment
They exist so you can explore the stack and prove the plumbing works. The challenge is what you build past them: extend one into something of your own, combine several, or ignore them entirely and chart a different course.

Some directions to calibrate ambition

  • A vial pick-and-place cell that survives a shuffled scene
  • A new skill taught by teleop and distilled into a policy
  • Person-following with a quadruped
  • Pouring with the G1
  • A multi-robot scene
  • Natural-language tasking
  • Deliberate failure recovery

06Judging

Judging runs science-fair style first: judges from the hosting companies circulate and view projects at team stations, so have a tight walkthrough and a live or recorded demo ready. The top 6 teams then demo to the full group, and winners are announced after.

Projects are judged on four categories, unweighted.

Ambition

How hard the problem is and how much of the loop you take on.

Functionality

Does it work end-to-end when demonstrated.

Creativity

Originality in the task, approach, or demo.

Architectural quality

How well the system is put together: clean boundaries between perception, reasoning, and action, and sound engineering choices.

07Ground rules

  • Unless you are confident you can pull it off quickly, avoid fine-tuning pre-trained policies or VLA models. Training a small imitation policy from scratch on your own demonstrations fits comfortably in the day (Guide 3 shows the path); wrangling a large pre-trained model usually does not.
  • Make it awesome.

Prizes

Prizes for the strongest submissions, awarded after the top 6 teams demo to the full group. Judging criteria are in section 06.

Details pending
The event page lists a Prizes section but does not yet publish the specific awards. Nothing further is confirmed, so nothing more is stated here.

What to bring

Bring your laptop, charger, and anything else you rely on for a full day of building. Required setup instructions and recommended tools are shared with registered participants before the event — the four guides in this handbook cover the setup you can do in advance.

Who should attend

This event is designed for:

  • Robotics engineers
  • Machine learning engineers
  • Computer vision engineers
  • Research engineers and applied scientists
  • Data and infrastructure engineers working with robotics or physical AI
  • Technical founders and builders exploring embodied intelligence

You don't need to arrive with a team. We'll help participants connect and form teams before and at the event — post yours or find one with room in the team directory.

Important notes

  • Space is limited, and all attendees must apply and be approved to attend.
  • This is a hands-on technical event. Participants should be comfortable writing code and building independently or as part of a small team.
  • Please register individually, even if you plan to attend with a team.

Guide 1LeRobot on macOS: first-time setup for the SO-101

Go from a Mac with nothing installed to a real SO-101 arm following your hand. The published LeRobot instructions assume Windows or Ubuntu, so the port names, permissions, and GPU advice in them do not match a Mac. This guide is the macOS version, with every difference called out where you hit it.

Total time
About 45 minutes, most of it calibration.
Tested against
lerobot 0.4.4, Python 3.10, macOS on Apple Silicon.
Vendor manual
Assembly and servo wiring: SO-ARM101 user manual (hiwonder.com)

Step 0 · What you need

Hardware on the desk

  • An SO-101 follower arm, which is the one that moves under command.
  • An SO-101 leader arm if you want to teleoperate. The leader has a handle and no gripper servo horn.
  • One data-capable USB-C cable per arm, plus the arm power supplies. A charge-only cable shows no serial port at all and wastes twenty minutes.

Software: a terminal, and either miniconda or uv (docs.astral.sh/uv). You do not need a GPU, Docker, or ROS.

Step 1 · Create the environment

Either tool works. The conda route pulls in ffmpeg, which LeRobot uses for video encoding during dataset recording:

conda create -n lerobot python=3.10 ffmpeg -c conda-forge -y
conda activate lerobot
pip install lerobot opencv-python

With uv instead:

uv venv ~/lerobot-env --python 3.10
source ~/lerobot-env/bin/activate
uv pip install lerobot opencv-python

Add opencv-python in either case, because anything that touches a camera needs it and the error you get without it is unhelpful.

python -c "import lerobot, torch; print(lerobot.__version__, torch.__version__)"
python -c "import torch; print('MPS available:', torch.backends.mps.is_available())"

You should see a lerobot version and MPS available: True. Plain pip install torch already gives you Apple Silicon acceleration. There is no CUDA wheel for a Mac, so ignore every tutorial that tells you to install one.

Step 2 · Find the serial ports

Plug in one arm, then list the ports:

ls /dev/tty.usbmodem*

macOS names serial ports /dev/tty.usbmodemXXXXXXXX. Linux tutorials say /dev/ttyACM0 and the vendor manual says COM22 or COM24; neither exists here, so substitute your own port name into every command you copy. LeRobot also ships an interactive finder that walks you through unplugging a cable:

lerobot-find-port

Two more macOS specifics. You do not need udev rules or sudo chmod on the port, because it is usable as soon as it appears. USB hubs also renumber ports across reboots and replugs, so check the name at the start of each session rather than trusting one you wrote down yesterday.

ls /dev/tty.usbmodem* prints one port per plugged-in arm. If it prints nothing, swap the cable before you debug anything else.

Step 3 · Tell the two arms apart

Both arms produce identical port names, so you have to identify them. Two reliable methods:

  • The unplug test. List the ports with both arms connected, unplug one, list again, and note which name disappeared.
  • The passive wiggle test. This project ships a script that reads joints for 15 seconds and commands no motion.
cd ~/my-sim
python replay/check_so101.py --port /dev/tty.usbmodemXXXXXXXX

Move each joint gently by hand while it prints. The sanity bar comes from the script's own docstring: at rest the numbers should be repeatable, none should sit near plus or minus 180, and moving one joint must move only its own number, in a consistent direction.

You know which port is the leader and which is the follower. Save them so later commands are copy-paste:

export LEADER=/dev/tty.usbmodemAAAAAAAA
export FOLLOWER=/dev/tty.usbmodemBBBBBBBB
Expected surprise
On most lerobot versions connect() enables torque, so the arm stiffens when the script starts. That is normal, and the script still sends no motion.

Step 4 · Assign servo IDs (DIY kits only)

Pre-assembled arms arrive with servo IDs set, so skip this step. If you built the arm from a kit, each servo needs its ID written one at a time, with only that servo connected:

lerobot-setup-motors --robot.type=so101_follower --robot.port=$FOLLOWER

Follow the prompts and the vendor manual's wiring order. Connecting several unconfigured servos at once writes the same ID to more than one of them, which is confusing to undo.

Step 5 · Calibrate each arm

Calibration teaches LeRobot each joint's real range. Do it once per arm, in the calibration start pose from the manual:

lerobot-calibrate --robot.type=so101_follower \
    --robot.port=$FOLLOWER --robot.id=my_follower_arm

lerobot-calibrate --teleop.type=so101_leader \
    --teleop.port=$LEADER --teleop.id=my_leader_arm

The --id you choose here is the --id every later command must use, because profiles are keyed by it. They are written to ~/.cache/huggingface/lerobot/calibration/.

Both profiles exist:

ls ~/.cache/huggingface/lerobot/calibration/

Back that directory up now. Recalibrating in the middle of a demo costs you the demo.

Step 6 · First motion: teleoperate

With both arms calibrated, the leader drives the follower:

lerobot-teleoperate \
    --robot.type=so101_follower --robot.port=$FOLLOWER --robot.id=my_follower_arm \
    --teleop.type=so101_leader --teleop.port=$LEADER --teleop.id=my_leader_arm

Keep one hand near the power switch for the first run. Move the leader slowly and watch for a joint that travels the wrong way, which points at a calibration or ID mismatch rather than a hardware fault.

The follower mirrors the leader across all six joints, including the gripper, with no joint racing to its limit. Flags vary slightly between lerobot releases, so run lerobot-teleoperate --help if one is rejected.

Step 7 · Record a dataset

Recording captures synchronized camera frames, joint states, and commands in the LeRobot dataset format, which is what the training scripts consume. Check your cameras first:

lerobot-find-cameras opencv
macOS camera permission
macOS gates camera access per application, so grant your terminal permission under System Settings → Privacy and Security → Camera. Without it the capture returns black frames and reports no error at all.

Then record, adding your camera and dataset flags from lerobot-record --help:

lerobot-record \
    --robot.type=so101_follower --robot.port=$FOLLOWER --robot.id=my_follower_arm \
    --teleop.type=so101_leader --teleop.port=$LEADER --teleop.id=my_leader_arm \
    --dataset.repo_id=<your-name>/so101-pick-place \
    --dataset.single_task="Put the green cube in the tray."

Episodes appear under ~/.cache/huggingface/lerobot/<your-repo-id>/ with video files and parquet data. From there, lerobot-train fits a policy; Guide 3 covers the training and evaluation path, including a way to generate episodes in simulation instead of by hand.

macOS differences at a glance

Every other tutorial saysOn a Mac
/dev/ttyACM0 or COM22/dev/tty.usbmodemXXXXXXXX
sudo chmod 666 /dev/ttyACM0 and udev rulesNothing to do; the port works when it appears
Install a CUDA build of torchpip install torch gives MPS; no CUDA wheel exists
Cameras work once plugged inGrant terminal camera access in Privacy and Security
Port names are stableHubs renumber ports on reboot and replug

Troubleshooting

SymptomCause and fix
ls /dev/tty.usbmodem* prints nothingCharge-only cable, or the plug is not seated. Swap to a data cable.
Both arms show the same kind of port nameExpected. Identify them by the unplug test or check_so101.py (Step 3).
A calibration or teleop command cannot find the profileThe --id does not match the one used at calibration. Check ~/.cache/huggingface/lerobot/calibration/.
One joint moves the wrong directionCalibration or servo ID mismatch on that joint. Recalibrate that arm; check IDs on a DIY kit.
Camera capture returns black framesmacOS camera permission for your terminal (Step 7).
The arm stiffens as soon as a script connectsExpected: connect() enables torque. Passive scripts still send no motion.
Port worked yesterday, not todayThe hub renumbered it. Re-run ls /dev/tty.usbmodem* and update $LEADER and $FOLLOWER.
A script refuses to run over a version or units stampDo not bypass it. Re-stamp in the environment you are running from.

House rules for real hardware

These are enforced by the scripts in replay/, and they exist because each one was learned the expensive way.

  • Pin one driver per data file. Every trajectory and policy records the driver, lerobot version, and calibration id it was made with, and the loader validates all three before anything moves. A mismatch stops the run loudly instead of falling back.
  • Treat units as data. The units dictionary travels with the file and is asserted against the robot config at both ends.
  • Never cut torque while the arm holds a commanded pose, because the arm drops. Ramp back to the rest pose read at connect time, then disconnect.
  • Run the real --dry-run first, every time the code or the trajectory changed. It exercises the whole path with sends suppressed. Note that --dry-lift-mm is a rehearsal that does move the arm.
  • Keep a hand near the power switch on first runs.

Scripts referenced here: replay/check_so101.py, replay/replay_on_so101.py, replay/run_act_on_so101.py. Sim teleop and cloud setup: Guide 2.


Guide 2Sim teleoperation: drive the Antioch sim with a real SO-101 leader arm

Hold a real leader arm, watch a physics-simulated SO-101 mirror your hand on a cloud GPU, and record demonstration episodes that are scored the moment you stop recording. Nothing runs locally except the arm driver, so your Mac needs no GPU, no Docker, and no Isaac install.

First motion
About 5 minutes once the setup in Step 0 is done.
Tested against
antioch-sim 0.3.27 on isaac-601-ga, lerobot 0.4.4, Python 3.10 in the robot env, macOS on Apple Silicon.
your hand leader arm (USB) bridge script cloud sim container scored demo episodes (mp4 + npz) +WebRTC livestream (your browser)

Step 0 · One-time setup

You need three things before the fun part.

  1. The project at ~/my-sim, with its own environment.
  2. A separate environment that talks to the arm over USB. Keep it separate from the project environment, because mixing the two causes the most confusing errors in this whole pipeline.
  3. A calibrated leader arm. If yours has never been calibrated, follow Guide 1 through Step 5 and come back. The follower arm stays in its box for this guide, because the sim replaces it.
cd ~/my-sim && uv sync && .venv/bin/antioch auth login
conda create -n lerobot python=3.10 ffmpeg -c conda-forge -y
conda activate lerobot
pip install lerobot opencv-python
One rule for every command below
Call the project's own CLI at ~/my-sim/.venv/bin/antioch from inside ~/my-sim. A bare antioch on your PATH is a different binary that cannot see the project, and the CLI refuses to run outside a directory holding antioch.yaml.

Step 1 · Two terminals

This pipeline needs two terminals at once, and it helps to know which is which before you start.

Terminal A

The sim session

Run from ~/my-sim with the project environment. It claims the cloud machine and keeps the simulation alive.

Terminal B

The bridge

Run in your (lerobot) environment. It reads the arm and streams joints into the machine that Terminal A claimed.

Step 2 · Find the leader port

Plug in the leader arm by itself, then capture the port in one command:

export LEADER=$(ls /dev/tty.usbmodem* | head -1) && echo $LEADER

It echoes something like /dev/tty.usbmodem58FA0836661. If it prints nothing, the cable is charge-only or unseated. If you have both arms plugged in, this picks one arbitrarily, so unplug the follower or identify the ports as described in Guide 1, Step 3.

Step 3 · Warm a machine and start the session

In Terminal A. The first command claims a cloud machine and warms it, which matters because a freshly claimed machine deadlocks on its first render and a teleop session started cold dies a few minutes in with no useful error:

cd ~/my-sim && .venv/bin/antioch scenario run --scenario so101_render_probe --timeout 300

Then start a 30 minute session on that same machine. Leave off --machine entirely and the session uses the machine you just warmed:

.venv/bin/antioch scenario run --scenario so101_live_teleop \
    --set max_seconds=1800 --verbose --timeout 1900

The output prints [teleop] live for 1800s; waiting for /tmp/leader_stream.jsonl. The simulation is up and idling. Leave this terminal alone for the rest of the session.

Step 4 · Open the livestream

In a third shell, or Terminal B before you connect the arm, open the machine's viewport straight into your browser:

cd ~/my-sim && .venv/bin/antioch machine status | awk '/url/{print $2}' | xargs open

A browser tab shows the arm, a green cube, and a blue tray. The stream starts when a viewer connects, so leave the tab open. Every new session on a new machine has a new URL, so re-run this command rather than reusing an old tab.

Step 5 · Connect the arm

In Terminal B, in your (lerobot) environment. One command, no machine id to look up, because the wrapper resolves the assigned machine itself:

~/my-sim/replay/teleop_pipe.sh $LEADER

It prints connecting to antioch-… and then # streaming at 30.0 Hz. Within about a second the sim arm snaps to your leader's pose. Move the leader and the sim follows.

Step 6 · Record episodes

Focus matters
Click into Terminal B first, because the bridge reads single keypresses and keys pressed in the browser do nothing.

The collection rhythm:

r  →  new cube position
s  →  recording starts
      pick the cube up with the leader, place it in the tray
s  →  episode saved and auto-scored
r  →  again
r

Reset: the cube teleports to a random spot on the table

s

Start recording, and press again to stop and save

Ctrl-C

Stop the bridge; the sim session keeps running

After each stop, Terminal A prints episode N closed: success/fail, NNN frames. Success means the cube was lifted and ended inside the tray, judged by the same gates the automated test suites use, so it is a real score rather than a self-report.

Aim for quality over quantity: smooth approach, clean grasp, deliberate place. Ten to fifteen good episodes per session is a solid haul. Episodes upload as they close, so a session that dies loses at most the episode in progress.

Step 7 · Download your data

From ~/my-sim, once you are done. This resolves the newest run id for you:

.venv/bin/antioch scenario download $(.venv/bin/antioch scenario list --limit 1 \
    | grep -oE '[0-9a-f]{32}' | head -1)

You get epNNN_success.mp4, the robot's wrist camera view at 640×480 and 10 Hz, paired with epNNN_success.npz holding joint states and your leader commands aligned at 10 Hz. That pair is exactly what the training pipeline ingests, so Guide 3 picks up from here.

LeRobot convention
The actions in the .npz are your commanded leader targets rather than the achieved joint positions. Train on it as it is, because a policy trained on achieved positions cannot hold a load.

Why the pipe uses machine exec

This is the one detail worth understanding, because getting it wrong fails silently.

antioch machine exec forwards stdin into the container where the scenario runs. antioch machine ssh lands you in a separate ephemeral filesystem, so a /tmp/leader_stream.jsonl written there is invisible to the scenario.

The sim arm then never moves, with no error printed anywhere. The wrapper script uses machine exec for exactly this reason, which is why the recommended path is to run the wrapper rather than assemble the pipe by hand.

Troubleshooting

Two things to avoid
Do not edit a scenario mid-session and expect it to take effect, because the container keeps the code the session launched with; cycle the session instead. Do not pkill inside the container, because that can wedge the exec channel and take the bridge down with it. Let sessions expire on max_seconds, or use antioch machine reset and then warm the machine again.
SymptomCause and fix
BrokenPipeError from the bridgeThe pipe went through machine ssh instead of machine exec. Use teleop_pipe.sh.
no machine assigned from the wrapperTerminal A is not running. Start the session first, because the wrapper only connects and never claims.
Assigning machine… waiting spinnerA stale machine id made the CLI claim a new machine. Stop it and let the wrapper resolve the id itself.
No antioch.yaml foundYou ran an antioch command outside ~/my-sim. cd ~/my-sim first.
Session hangs at start and dies a few minutes inThe machine was cold. Run so101_render_probe on it, then relaunch the session.
Stream is fine but the sim arm ignores youCheck Terminal A for bridge header ok and a rising frame count. If neither appears, restart the bridge.
Pressed s and nothing recordedThe keypress went to the browser. Click Terminal B, then watch for the # record ON echo.
Stream URL is deadThe platform rotated machines, so the id and URL both changed. Restart from Step 3.
ls /dev/tty.usbmodem* shows nothingCharge-only cable or a loose plug. Use a data-capable USB-C cable.

Scripts referenced here: replay/teleop_pipe.sh, replay/leader_bridge.py, src/so101_live_teleop.py. Arm setup: Guide 1. Training on this data: Guide 3.


Guide 3Train your first SO-101 policy: ACT/Diffusion on sim data

Generate demonstration episodes in simulation, train an imitation policy on them, check it closed-loop in sim, and run it on a real arm. You need the my-sim project with Antioch access, plus a Python 3.10 env with lerobot, torch, and opencv-python. Your laptop needs no GPU and no Isaac install: simulation runs on cloud GPUs, and training runs locally on Apple Silicon (MPS).

First policy
About 1.5 hours, most of it unattended training.
Tested against
lerobot 0.4.4, torch 2.10 for training and 2.11+cu130 for remote eval, Python 3.10 in the lerobot env, antioch-sim 0.3.27 on isaac-601-ga, asset so101_antioch 1.3.0.

Step 1 · Generate episodes

From the project root, run the scripted-expert suite. It replays a verified pick-and-place over randomized cube positions and sizes:

.venv/bin/antioch suite run wrist_cam_enabled --queue
.venv/bin/antioch scenario list --limit 5          # find the run ids
.venv/bin/antioch scenario download <run-id>

About 30 episodes appear in roughly 4 minutes of suite time. Each episode is a pair of files: epNNN_success.npz with joint states and actions at 10 Hz, and epNNN_success.mp4 with the 640×480 wrist camera view.

If you would rather record human demonstrations, drive the sim arm with a real leader arm instead (Guide 2). The output format is the same; plan for 10 to 15 episodes per session.

Important
Whichever route you take, the actions array must hold the commanded joint targets rather than the measured positions. A policy trained on measured positions learns to command whatever pose the arm already has, so it stops producing force the moment it meets resistance and freezes under load. Both routes already record commands; keep it that way if you modify them.

Step 2 · Build the dataset

Convert the episode pairs into a LeRobot dataset with exactly these fields:

FieldValue
observation.images.wrist640×480 RGB, 10 Hz
observation.state6 joint positions, radians
action6 next-step joint targets, radians
taskstring "Put the green cube in the tray."

About 30 episodes of 240 steps each load without errors. That is enough for a first policy. Add more data only after eval shows you need it.

Step 3 · Train

Train ACT with the LeRobot default config (chunk 100), batch 8, inside your lerobot env:

lerobot-train --policy.type=act --dataset.repo_id=<your-dataset> \
    --steps 15000 --batch_size 8 --save_freq 5000

Set the budget to 15k steps rather than the 100k in the LeRobot tutorial. The tutorial assumes large, messy, human-collected datasets; 30 consistent scripted episodes converge much earlier. For Diffusion Policy, swap in --policy.type=diffusion and expect a longer wall clock (about 90 M parameters against ACT's 52 M).

Training runs at about 4 steps/s on MPS and finishes 15k steps in roughly an hour. The smoothed loss should end below about 20% of its value at step 500, with no NaN. Keep all three checkpoints; Step 4 decides which one is best.

Step 4 · Evaluate closed-loop in sim

A low training loss tells you the policy can imitate the dataset. It does not tell you the policy can do the task. To find out, export the checkpoint as TorchScript (trace it on torch 2.10; the remote's 2.11 loads it) and run the eval scenario. The policy takes the scripted expert's place at 10 Hz: it receives the wrist image and the measured joints, and it outputs joint targets. It receives nothing else.

The run scores against the same physics gates as the expert suite: the cube must end inside the tray within the tick budget.

Eval setEpisodesPass bar
Seen positions (training bearings)10TBD
Held-out positions (midpoints)9TBD
Held-out sizes (47.5 / 52.5 mm)6reported

A passing checkpoint clears both bars. If the seen set passes and held-out fails, add data diversity. If both fail, check the commanded-actions rule from Step 1 before you touch anything else.

Step 5 · Run on the real arm

The runner is replay/run_act_on_so101.py:

# stamp the policy with driver + lerobot version + calibration id
python replay/run_act_on_so101.py --stamp --id my_follower_arm
# the full path with motion suppressed
python replay/run_act_on_so101.py --port /dev/tty.usbmodemXXX \
    --id my_follower_arm --camera 0 --dry-run
python replay/run_act_on_so101.py --port /dev/tty.usbmodemXXX \
    --id my_follower_arm --camera 0

--dry-run prints every command it would send while moving nothing. The live run performs the task. The script also enforces the safety rules: it validates the stamped driver, lerobot version, and calibration id at load, asserts units at both ends, and ramps torque back to rest before it disconnects. Leave those checks alone.

Troubleshooting

SymptomFix
Policy grasps, carries, then freezes mid-taskIt was trained on measured positions. Rebuild the dataset from commanded targets (Step 1).
Identical failures across all checkpointsThe first camera frame was black. Warm-render before the first decision, and reuse the previous frame when a capture returns nothing.
First render run on a fresh cloud machine hangsThe machine is cold. Warm it with antioch scenario run --scenario so101_render_probe (about 25 s), then rerun.
Loss looks great, eval failsThis is expected sometimes. The loss curve does not predict closed-loop success; only Step 4 does.
Training crawls or crashes on MacUse plain pip install torch for MPS. There is no CUDA wheel for Mac, and you do not need one.
Real arm ignores the policy fileThe version stamp does not match. Re-stamp (--stamp) in the env you will run with, using the calibration id you calibrated with.

Code: replay/run_act_on_so101.py, replay/leader_bridge.py, src/so101_bench_tasks.py. Benchmark and results: sim2real_benchmark.md.


Guide 4Running Unitree robots on Antioch: Isaac Sim vs Isaac Lab

Spawn a Unitree quadruped or humanoid, give it a controller, and prove it works. The first decision is which engine you run on, because Isaac Sim and Isaac Lab expose completely different Python entry points for the same robot. This guide covers both, then walks the path from a stock asset to a verified walking robot.

Standing robot
About 15 minutes on an engine you have already chosen.
Tested against
antioch-sim 0.3.27, engines isaac-601-ga (Isaac Sim 6.0.1) and isaac-lab-30b2 (Isaac Lab 3.0), URDF importer 3.11.2, Unitree URDFs from unitreerobotics/unitree_ros, policies from fan-ziqi/rl_sar.

Step 1 · Choose your engine

One project runs one engine, selected in three places that must agree: the antioch-sim extra in pyproject.toml, the image coordinate or Dockerfile in antioch.yaml, and the FROM line of that Dockerfile. Changing engines means editing all three and rebuilding the image.

The two engines are not two flavors of the same API. They are different libraries:

Isaac Sim 6.0.1 (isaac-601-ga)Isaac Lab 3.0 (isaac-lab-30b2)
Simulation contextisaacsim.core.api.Worldisaaclab.sim.SimulationContext(SimulationCfg(...))
antioch.world()worksraises UnsupportedEngineError
Robot objectisaacsim.core.api.robots.Robot plus add_reference_to_stageisaaclab.assets.Articulation(ArticulationCfg(...))
Prebuilt robot configsnone; you point at a USDisaaclab_assets.robots.unitree
Gainsauthored in the USD DriveAPI, or set_gains at runtimedeclared in the cfg's actuator groups
Commandscontroller.apply_action(ArticulationAction(...))set_joint_position_target then write_data_to_sim
Cameraisaacsim.sensors.camera.Cameraisaaclab.sensors.Camera
Physics managerthe PhysX default worksneeds isaaclab_ovphysx, see the gotchas below

Choose Isaac Sim when you want a robot standing, driving, or running a pretrained policy, which is what this guide's verified runs use. Choose Isaac Lab when you want its ArticulationCfg library or its managers for training.

The same 15 lines on each engine

from isaacsim.core.api import World
from isaacsim.core.api.robots import Robot
from isaacsim.core.utils.stage import add_reference_to_stage
from isaacsim.core.utils.types import ArticulationAction

world = World(physics_dt=1/200, rendering_dt=1/200, stage_units_in_meters=1.0)
world.scene.add_ground_plane(z_position=0.0)
add_reference_to_stage(usd_path=usd, prim_path="/World/robot")
robot = world.scene.add(Robot(prim_path="/World/robot", name="robot",
                             position=np.array([0.0, 0.0, 0.42])))
world.reset()
controller = robot.get_articulation_controller()
controller.set_gains(kps=kp, kds=kd)
controller.apply_action(ArticulationAction(joint_positions=targets))
world.step(render=True)
Import placement
Keep both Isaac imports inside the scenario function. The CLI imports your scenario files on your laptop, where neither library is installed.

Step 2 · Check what already exists

Before building anything, find out whether your robot is already on the shelf. The Isaac 6.0 asset library carries these Unitree models:

A1  B2  Dex3  Dex5  G1  G1_23dof  Go1  Go2  H1  H2  Z1  aliengo  laikago

Isaac Lab additionally ships ArticulationCfg objects with tuned gains: UNITREE_A1_CFG, UNITREE_GO1_CFG, UNITREE_GO2_CFG, H1_CFG, G1_CFG, G1_MINIMAL_CFG, and G1_29DOF_CFG. Those configs live in isaaclab_assets, which is importable on the Lab engine only.

Nothing wheeled exists
The Go2W, B2W, and similar variants are absent from the asset library and from isaaclab_assets, so they need Step 4.

To check for yourself rather than trusting a list, src/unitree_go2w_probe.py lists the library and the config module in one boot:

.venv/bin/antioch scenario run --scenario unitree_go2w_probe --verbose

The run prints the Unitree folder listing and whether the config module imported. On isaac-601-ga expect No module named 'isaaclab_assets', which is normal and tells you the gains must come from the USD or from your own table.

Step 3 · Spawn a stock robot

For a robot the library already has, point at its USD and let the engine spawn it. On Isaac Sim, resolve the asset root and reference the file:

from isaacsim.storage.native import get_assets_root_path
usd = f"{get_assets_root_path()}/Isaac/Robots/Unitree/Go2/go2.usd"

On Isaac Lab, use the shipped config instead, which carries both the USD path and the tuned actuator groups:

from isaaclab_assets.robots.unitree import UNITREE_GO2_CFG
robot = Articulation(UNITREE_GO2_CFG.replace(prim_path="/World/robot"))

The robot appears at its spawn height and holds a pose without folding. If it collapses, the drives carry placeholder gains, so continue to the bake in Step 4.

Step 4 · Build a robot the shelf does not have

This is the Go2W path, and it applies to any Unitree model missing from the library. Unitree publishes official URDFs in unitreerobotics/unitree_ros under robots/<model>_description, with real masses, limits, and geometry. The pipeline is fetch, patch, import, bake, and it lives in src/unitree_go2w_bake.py:

.venv/bin/antioch scenario run --scenario unitree_go2w_bake --verbose

Four things about the importer on this engine, all measured rather than documented:

  • Importer 3.11.2 has no Kit commands. The documented URDFParseAndImportFile belongs to an older generation. Use URDFImporter(URDFImporterConfig(...)).import_urdf() from isaacsim.asset.importer.urdf.
  • Its usd_path is a directory, not a filename. Output lands at <usd_path>/<robot>/<robot>.usda with payload files beside it, and the call returns the file it wrote.
  • It converts primitive collisions and silently drops mesh collisions. A Go2W imported as-is has no wheel colliders at all and rests on its belly, so author convex hulls on the visual meshes yourself, per mesh rather than on the parent Xform.
  • Patch package:// mesh references to relative paths before importing.

The bake then writes gains into the USD DriveAPI so the file stands on its own. Two conversions matter: USD angular drives store stiffness and damping per degree while robot gain tables are per radian, so divide by 57.2958, and wheel joints take velocity drives with stiffness zero rather than position drives.

The run reports the joint count it baked, the wheel colliders it authored, and a stand plus drive result. Verified reference: run 93cd6ff55fdd4742b0411d0f2514e803 stands at 0.335 m and drives 0.90 m in 2.5 s with a wheel radius of 0.086 m.

Step 5 · Give it a controller

No pretrained locomotion policy ships with either engine for a wheeled quadruped, and Unitree's own RL repositories cover Go2, H1, and G1 rather than the W variants. The one public source of deployable checkpoints is fan-ziqi/rl_sar, which also deploys them to real robots over unitree_sdk2. It carries policies for go2, go2w, b2, b2w, g1, a1, lite3, tita, and others.

Read the contract from each policy's config.yaml rather than guessing it. The two we run differ in a way that matters:

Go2WG1
Joints16 — 12 legs and 4 wheels29
Observation size5796
Legsposition targets, default + scale × actionposition targets, all 29
Wheelsvelocity targets, stiffness 0none
Gainslegs kp 20 / kd 0.5, wheels kp 0 / kd 0.5per joint, kp 20 to 200
Joint orderleg triplets FR, FL, RR, RL then wheelsbase_joint_names[joint_mapping[i]]
Wheel positions in obszeroed, because the joints are continuousnot applicable

Both are velocity-commanded at the operator level, meaning you send a body twist of forward speed, lateral speed, and yaw rate at 50 Hz on a 200 Hz sim. Set the policy's own gains on the articulation controller at runtime, which is what the real deployment does.

Verified reference runs, both on isaac-601-ga: b52b679e94db4b96aca4599b71b923a2 tracks a 0.5 m/s Go2W command at 96%, and 9dbf0a1627cc4a639f2127e1e8c090ba walks the G1 at 85% of a 0.4 m/s command with torso tilt under 5 degrees.

Step 6 · Gate it on physics, not on video

A robot that looks fine in a clip can be sliding on frictionless wheels or holding a pose the drives cannot actually produce. Every scenario here asserts measured quantities, and the useful gates for legged robots are base height above a floor, tilt off vertical, joint tracking error, and real displacement over ground for a commanded velocity.

Allow a grace window at the start of each segment. A dropped-in robot rocks once while the controller catches it, and on the Go2W that transient reaches about 27 degrees of tilt before settling under 4 degrees. Judging the caught state rather than the transient is the difference between a meaningful gate and a flaky one.

Engine gotchas

SymptomCause and fix
Every video films a frozen robot while physics clearly movesThe Fabric scene delegate severs USD from PhysX. Boot with --/app/useFabricSceneDelegate=0, --/rtx/hydra/readTransformsFromFabricInRenderDelegate=0, --/physics/fabricEnabled=0, and call SimulationManager.enable_fabric(False) after world.reset(), because a GPU boot turns it back on inside World.
On Isaac Lab, commands appear to be ignoredThe Lab beta's default physics manager drops command writes. Pass physics=OvPhysxCfg() from isaaclab_ovphysx.
No module named 'isaaclab_assets'You are on the Isaac Sim engine. Read the gain values from the upstream config source and author them yourself, or switch engines.
import isaacsim.sensors fails on Isaac LabThat Kit extension is not enabled by default there. Use isaaclab.sensors.Camera, which needs omni.replicator.core and the carb flag /isaaclab/cameras_enabled.
First render scenario on a fresh machine hangsThe machine is cold. Warm it with a light render scenario first, roughly 25 seconds.
A camera returns black framesThe near clip plane is at its default. Set it explicitly, for example 0.05 m, and check that the subject fits the vertical field of view at your working distance.
The robot falls through the floorMesh collisions were dropped at import. Author convex hulls on the visual meshes of every collider-less link, including feet.

Verified runs

ScenarioRunResult
unitree_go2w_probe2a277fa189fe44cc959a1f2dbab3671fNo Go2W in the library or in isaaclab_assets
unitree_go2w_bake93cd6ff55fdd4742b0411d0f2514e803Stands 0.335 m, drives 0.90 m
unitree_go2w_policy_evalb52b679e94db4b96aca4599b71b923a296% velocity tracking
unitree_go2w_follow8ff230388c444b77b7fe0c75e4260a48Camera-driven person following
unitree_g1_walk9dbf0a1627cc4a639f2127e1e8c090ba85% velocity tracking

Scenarios referenced here: src/unitree_go2w_probe.py, src/unitree_go2w_bake.py, src/unitree_go2w_policy.py, src/unitree_go2w_follow.py, src/unitree_g1_policy.py, and src/unitree_stand.py for the Isaac Lab bake pattern.