← All skills

Skill

MuJoCo

A fast physics engine for robotics. Achilles's reference and benchmark, and where OpenDubs's arms are planned, simulated and tested.

7 log entries2 projects

AchillesBest example

Beating MuJoCo on throughput

Benchmark code

Also shows: Simulation (MuJoCo)Performance & SIMDC++

To see where V3 stood, I benchmarked it against MuJoCo 3.15 on the same arm scenes, converted from the same .arow files, at the same 2 ms step, single-threaded on my laptop (Intel Core Ultra 9 285H). Batched across SIMD lanes, Achilles steps a seven-link arm 3.3× faster than MuJoCo's best single-threaded mode, reaching 5.45 million robot-steps per second at 64 robots. In double precision, MuJoCo's own, Achilles is still 1.5–1.6× faster at most batch sizes, and its results match MuJoCo's to about one part in 10¹¹. The one loss is a single seven-link arm, where most SIMD lanes sit empty (0.97×).

Line chart of speed relative to MuJoCo by number of links. V2 with one robot falls from 1.1× at one link to 0.48× at eight. V3 with one robot is 2.8×, 2.1×, 1.6× and 0.97× at one, two, three and seven links. V3 with 64 robots batched is 2.7×, 2.6×, 3.0× and 3.3×.
Each version timed against MuJoCo on the same machine, in its own benchmark: V2 against MuJoCo 3.3 (no gravity, 1 ms step), V3 against MuJoCo 3.15 (2 ms step).
The same double pendulum in Achilles and MuJoCo, matching out to 8 seconds. Same scene and start, both RK4 at a 2 ms step.

OpenDubs

N arms, one planner: ArmGroup

Pull request

Also shows: Motion planningPythonSimulation (MuJoCo)

The lab plans with its own MuJoCo planning library, mj_manipulator, which planned one arm at a time. With two arms on one torso, two plans that are each safe can still hit each other. I added an ArmGroup to it that plans all arms' joints together, so every step is checked arm against arm. On the clip's goals, the two arms planned separately collide in 58 of 210 frames; ArmGroup's plan bends one gripper about 16 cm around the other and touches in none. The same group plans to joint goals, gripper poses and lockstep Cartesian paths, keeps 5 mm between all geometry, retimes the trajectory at 125 Hz, and re-checks it at every sample. I made single-arm planning a one-arm group, and ArmGroup is what plans both arms on the robot today.

The same goals, each arm planned on its own (left, red while they intersect) and planned together by ArmGroup (right). The lines are the grippers' paths. MuJoCo, real time.
A two-arm handover planned with ArmGroup in MuJoCo (the can is carried kinematically).

Achilles

Dynamics: the Articulated Body Algorithm

Pull request

Also shows: Kinematics & DynamicsC++Simulation (MuJoCo)

V2's dynamics were wrong in ways only a reference could show: brought back to life next to MuJoCo, a one-link arm swung exactly 4× too fast (it used the free-body inverse inertia, dropping the parallel-axis term), and a two-link arm moved in the wrong shape. V3 runs the full Articulated Body Algorithm, with Coriolis and centrifugal bias terms, parent-to-body transforms and gravity as a fictitious base acceleration, and steps it forward with semi-implicit Euler, the same default MuJoCo uses. Here all three drive the same arm with the same shoulder torque and no gravity, since V2 never had any. V3 stays within 3×10⁻⁵ rad of MuJoCo for all 4 seconds, while V2 has swung the shoulder 4.5 times as far and folded the elbow forward instead of letting it trail behind.

The demo arm with a constant 0.1 N·m on the shoulder, none on the elbow and no gravity, semi-implicit Euler at 1 ms. All three are drawn by MuJoCo's renderer, since V2 never had one; the yellow trail is the forearm tip's last second.

MuJoCo is only a reference, so I also checked V3 against an exact answer. A double pendulum is chaotic and has no closed-form trajectory: two simulations that differ by a single rounding error eventually go completely different ways. Its equations of motion do have a closed form, though. Lagrangian mechanics reduces them to a 2×2 linear system that gives the exact joint accelerations at any given position and velocity. I derived that system independently of the engine's code, then compared it with V3's accelerations at every state of a 10-second run. Because each state is checked on its own, errors can't build up and chaos can't amplify them. The two agree to within 3×10⁻⁶, which is float32 round-off, and V3 matches MuJoCo to the same 3×10⁻⁶.

Log-scale chart over 10 seconds of a double pendulum: the worst relative error in Achilles' joint accelerations against the closed-form solution stays between about 2e-7 and 3e-6, just above the float32 precision line, with no growth over time
ABA against the closed-form solution at every state of a 10-second double-pendulum run.

OpenDubs

Arm control: three impedance controllers, then a simpler one

Cartesian controlJoint and null-space control

Also shows: Impedance controlC++ROS 2

Each arm is a 7-DOF OpenArm whose Damiao motors run their own impedance loop: the Thor sends position, velocity and torque targets with gains over CAN-FD, and each motor closes the loop itself. I measured the stock ROS 2 stack first. A goal is accepted in 2.55 ms, and with both arms holding one block, 47 ms passes between a command to one arm and the torque spike in the other (σ = 4.7 ms).

What I was after was axial compliance in Cartesian space: a gripper that holds firm along some axes and gives along others. I wrote three controllers with Pinocchio to get it (Cartesian, joint and null space). Pure Cartesian impedance gave that compliance, but it left the redundant elbow free to drift off the collision-checked path. The combined controller ran joint-space impedance and subtracted the Cartesian terms from it, keeping the axial compliance and control of the null space, but it was numerically unstable. The null-space controller worked, but I switched off it for the much lower latency of the motors' own impedance loops: in the end we gave up Cartesian compliance for speed.

What I settled on is simpler: the planned trajectory goes straight to the motors, plus gravity compensation at 500 Hz from Pinocchio's RNEA (the recursive Newton-Euler algorithm), with every goal checked against position and torque limits in both joint space and Cartesian space (through forward kinematics).

My Cartesian impedance controller on the robot: gravity compensation only (June 26), stiff in x and y but compliant in z and rotation, and stiff in x and y but compliant in rotation.
The four controllers in MuJoCo, built unchanged from their repos (faint arm: the plan). On the reaches the motors' own loop stays within about 2 cm of the plan while the others trail by 7 to 18 cm; near the straight arm the Cartesian one stays folded once pushed. Demo gains.

OpenDubs

Teleop with a Meta Quest, in the browser

Code

Also shows: TeleoperationPython

For demonstrations and live driving, I built teleop with a Meta Quest. The apps I tried had no passthrough and left me dizzy, so I wrote my own: instead of an app on the headset, the robot serves a page the Quest's browser opens in AR, so the operator still sees the room, and the page streams both controllers' poses, grip and trigger to the robot over a websocket. Holding a grip engages its arm, and the trigger works the gripper. The headset's page goes dark once it enters AR, so the robot also broadcasts that live telemetry to any other browser pointed at it, and someone at a laptop can watch exactly what the operator is sending. It matches the old SDK's interface, so switching was a one-line change. I built it against the arms in MuJoCo first; at first the loop couldn't keep up, and the bottleneck turned out to be publishing the sim viewer over the network, not the control.

Teleop on the robot after the speed-ups.
My teleop loop in MuJoCo, fed two scripted hands (the spheres): the repo's limits (left) and raised ones like the robot's now (right).

OpenDubs

Running both arms at once

Bimanual refactor

Also shows: PythonROS 2Motion planning

A two-arm plan is only safe if both arms run it together: it was checked with each arm where the other would be at the same instant. On hardware, though, mj_manipulator_ros, the lab's layer that runs those plans on the robot, ran multi-arm plans one arm after the other, which doubled the time and passed through states the planner never checked. Replaying the crossing plan that way in MuJoCo and checking every fourth of its states, the arms intersect in 97 of 1,400.

I rebuilt execution so every arm starts its trajectory on one shared start stamp: the trajectories must share their timestamps, one stamp a moment in the future is computed once, and each arm sends its goal from its own thread against that stamp, so the motors start together however long each goal takes to send. If any arm fails or is aborted, a shared event cancels the others mid-motion. Running two arms from two threads also meant making the shared code thread-safe: both threads stepped one MuJoCo model, and concurrent runs crashed until I serialized every access to it behind one lock. To bring it all up on the real robot safely, I built a hardware-in-the-loop bring-up protocol.

Line chart of the closest distance between the two arms over time for one crossing plan. Run left then right or right then left, it takes 44.8 seconds and dips below zero, the arms overlapping, near the end. Run together from one start stamp, it finishes at 22.4 seconds without overlapping
The same plan run the old way and together: how close the arms come, and when each run finishes.
Both arms moving at once on the robot, September 22.

Achilles

Actuators: closing the loop

Code

Also shows: Kinematics & DynamicsC++Simulation (MuJoCo)

Until now, every torque in Achilles came from the scene file, so a robot could fall or collide but never react. Actuators let a controller outside the simulation push on a joint every step. An actuator is a direction in its joint's frame, an angular one for a torque or a linear one for a force. Attaching it projects that direction onto the joint's free axes once, so applying an effort later costs only a few multiply-adds into the joint-torque field. The simulation never owns or loops over actuators: each one is a small handle into that field, which is now double-buffered. A controller writes one half while the step reads the other, and the halves swap at the start of every step. An effort therefore acts over exactly the next step and is then gone, so a stale command can never leak into a later frame. Here, two actuators tilt a plate to balance a ball. The controller never sees the ball directly, only through a simulated camera that reports its position 30 times a second, 50 ms late, with 2 mm of noise. The plate can tilt no faster than 3 rad/s, about what a hobby servo manages. The plate starts tipped with the ball on its high corner, and the controller has to catch the ball before it rolls off, center it, then roll it around a figure-8.

Real time, rendered by the Achilles viewer: semi-implicit Euler at a 4 ms step. The controller sees only the delayed, noisy camera; the simulation sees only the two pivot torques.

To check that an effort lands on exactly the step it was meant for, I drove a six-joint arm in both Achilles and MuJoCo 3.17. Every joint has an actuator running a PD loop toward its own sine wave, with torques up to 146 N·m. Both engines start from the same state at the same 2 ms step, and each step they read the joints, compute the torques, and apply them over the next step. Over 20 seconds, Achilles stays within 10⁻⁵ rad of MuJoCo under both semi-implicit Euler and RK4, which is float32 round-off, and the torques agree to 3×10⁻⁵. If the torques land one step late, MuJoCo ends up 1 to 2 rad away from its on-time run, so a timing slip would stand out by five orders of magnitude. The free-swinging arms in my throughput benchmark drifted apart after a few seconds because they're chaotic. Here the controller keeps pulling both arms back onto their targets, so the two engines stay together for the whole run.

Two charts over 5 seconds for a six-joint arm. Top: each joint's angle following its own sine wave, between about -2 and 2 rad, with Achilles drawn dashed on top of MuJoCo and the lines indistinguishable. Bottom, log scale: the largest joint-angle difference. Achilles against MuJoCo stays around one millionth of a radian; MuJoCo against itself at a 100 times smaller step stays near a thousandth; MuJoCo with torques applied one step late swings up to about 1 rad
Every joint torque-driven by PD toward a sine, semi-implicit Euler at a 2 ms step. Achilles in float32, MuJoCo in float64.