Hardware-in-the-loop robotics simulation for AI agents, built on MuJoCo
Project description
physsim
Hardware-in-the-loop robotics simulation. Describe a robot, write firmware for a real board, and run that firmware against MuJoCo physics — with a window to watch it in.
The firmware is the same code you would flash. Python firmware runs unmodified on a real ESP32 under MicroPython; C++ firmware is written against an Arduino-compatible API. Pin assignments, PWM resolution, I²C transaction time, ADC quantisation and motor back-EMF are all in the loop, so code that would miss its deadline on a bench misses it here too.
Install
pip install physsim # the library
pip install "physsim[plot,video]" # plus matplotlib and mp4 export
C/C++ firmware additionally needs a host compiler (clang++, c++ or g++). The QEMU
tier needs Espressif's qemu-system-xtensa and xtensa-esp32-elf-gcc —
physsim.runtime.qemu.install_hint() prints exactly what to download and where to put
it. Everything else works without either.
Working on physsim itself:
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
.venv/bin/python -m physsim.examples.pid_arm --view
What it looks like
from physsim import Board, Robot, run_firmware
FIRMWARE = """
from machine import Pin, PWM
import utime
servo = PWM(Pin(13), freq=50)
def main():
servo.duty_us(2000)
utime.sleep_ms(800)
log("done")
"""
esp = Board("esp32")
arm = Robot("arm", board=esp)
base = arm.base(mass=1.0)
link = base.link(length=0.30, mass=0.25, name="upper")
link.joint("revolute", axis="y", range=(-90, 90), name="shoulder") \
.servo(esp.pwm(13), stall_torque=2.5)
run = run_firmware(arm.build(), FIRMWARE, duration=3.0, view=True)
print(run.summary())
view=True shows the robot in MuJoCo's own interactive viewer — orbit, zoom, contact
forces — and a second window with the camera views, meters, plots, buttons and inputs you
compose. MuJoCo draws the scene; physsim builds only what MuJoCo has no opinion about.
Run it with a plain python; no mjpython needed. On macOS, Tk and mjpython cannot
share a process, so physsim runs MuJoCo's viewer in a child process and streams it the
pose. You get both windows either way.
.venv/bin/python -m physsim.examples.voice_arm --view
Why it catches things
The compiler validates the design before a run happens, and reports problems as structured diagnostics with a code, a message and a fix:
PIN002 [servo1]: 'servo1' needs pwm on GPIO34 (pwm), but that pin only supports
digital_in|adc — input-only: no output driver, no internal pull-up/down
fix: pins on esp32 with pwm: 0, 1, 2, 3, 4, 5, 12, 13, 14, 15, ...
PHY006 [servo1]: actuator 'servo1' can supply 0.05 N·m but holding 'shoulder' against
gravity needs up to 1.6 N·m (32x) somewhere in its range
fix: use a stronger actuator, gear it down, shorten or lighten the links beyond
this joint, or restrict the joint range
Testing a controller
from physsim import Scenario
s = Scenario(model, FIRMWARE, duration=9.0)
s.disturb(body="fore", at=3.0, force=(4, 0, 0), duration=0.15)
s.payload(body="fore", mass=0.12, at=6.0)
s.assert_settles("shoulder.angle_deg", target=40.0, within=2.0, by=2.5)
s.assert_recovers("shoulder.angle_deg", target=40.0, within=2.0, at=3.0, by=5.0)
report = s.run()
print(report.summary()) # PASSED/FAILED, metrics, every check with its numbers
report.save_csv("trace.csv")
Examples
| Command | Shows |
|---|---|
python -m physsim.examples.arm2dof |
Building and validating; gravity vs. powered servos |
python -m physsim.examples.blink |
Firmware and physics in lockstep |
python -m physsim.examples.pid_arm --view |
Closed-loop PID written as firmware |
python -m physsim.examples.pid_arm_cpp |
The same controller in C++, identical trajectory |
python -m physsim.examples.disturbance --view |
Disturbance rejection with assertions |
python -m physsim.examples.camera_arm |
Live window with a hand-mounted camera |
python -m physsim.examples.voice_arm --view |
Custom dashboard: 2 cameras, mic meter, E-STOP, text input |
python -m physsim.examples.predict_tune |
Predict → tune → run → verify, without touching firmware first |
python -m physsim.examples.pi_vision --view |
Pi 5 firmware tracking a red target through the camera |
Add --view to watch. A plain python is all you need — MuJoCo's interactive scene
viewer and the panel window come up together.
Predicting before you run
physsim.predict answers design questions from the physics alone — no firmware, no bus,
no GUI, and about 100x faster than a real run:
from physsim import predict
predict.worst_case_torque(model) # "shoulder: needs 1.09 N.m, has 5.04 N.m (4.62x)"
predict.payload_capacity(model, "fore") # 0.98 kg
predict.solve_ik(model, "tip", [0.3, 0, 0.2])
gains = predict.tune_pid(model, "shoulder", "sh_motor", target=40.0)
report = Scenario(model, firmware_using(gains), duration=6.0).run()
print(predict.verify(gains, report)) # how far was the prediction from reality?
A rollout has no sensor noise or bus timing, so reality is always slower and less exact.
verify() measures that gap instead of letting you assume it away.
Requirements
- Python 3.11+,
mujocoandnumpy(installed with the package) - C/C++ firmware additionally needs
clang++,c++org++on PATH - Plots need
matplotlib; video export needsimageio. Everything else — traces, CSV, rendered frames, the live window — needs neither.
Documentation
- The website — landing page, full guide, and a searchable reference for every
board, pin and diagnostic code. Static, no build step, no dependencies. Deploys to Vercel
as-is (
npx vercel --prod, config in vercel.json) or to GitHub Pages from/docs. See docs/README.md. - AGENTS.md — the reference, written for AI agents using the library
- PLAN.md — architecture, design decisions, and the milestone plan
- CLAUDE.md — invariants and conventions for working on the library
Or ask the library directly:
import physsim
physsim.capabilities() # boards, peripherals, runtimes, signals, diagnostics
physsim.board_info("esp32") # which pins can do what, and which to avoid
physsim.explain("PHY006") # what a diagnostic means
Status
Every planned milestone (M0–M9) is complete: robot DSL and validating compiler, MJCF/URDF import,
lockstep scheduler, Python and C++ firmware runtimes, peripheral models, the scenario
harness, composable GUIs with recorded-and-replayable input, the microphone peripheral,
live tunables, a 13-board catalog with electrical validation, a QEMU tier running real Xtensa
firmware on an emulated ESP32, a Raspberry Pi tier with gpiozero/picamera2,
firmware-side camera capture, and M9 prediction. 411 tests, 5 slow.
The same C/C++ firmware produces a byte-identical trajectory host-compiled and on emulated Xtensa — one conformance suite runs across all three runtimes, and the reference PID controller settles to the same hash on Python, host C++, and a real Xtensa ELF.
The electrical checks are worth calling out because a simulator cannot find what they
find. Wiring an HC-SR04's 5 V echo line into an ESP32 input works perfectly in simulation
and stresses a real pin, so it is a build error (PIN008). Two servos off the board's own
5 V pin draw 1400 mA against a 500 mA regulator, so that is reported too (PIN009) —
along with ADC ranges that clip, I²C devices slower than their bus, PWM pins forced to
share a timer, and software PWM that jitters.
Not yet built: camera formats beyond raw RGB888, audio waveforms or speech recognition
(the microphone is level + keyword), memory-limit modelling, multi-board runs, FreeRTOS
task semantics. The Pi tier is a gpiozero/picamera2 shim rather than CPU emulation,
because upstream QEMU does not model the RP1 southbridge that owns Pi 5 GPIO.
physsim.capabilities()["not_yet_implemented"] is the authoritative list.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file physsim-0.1.0.tar.gz.
File metadata
- Download URL: physsim-0.1.0.tar.gz
- Upload date:
- Size: 289.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
373a1bfaf212be3d8e2a6c5e386400cfd4a44dcae3087556174d3c94230de6e7
|
|
| MD5 |
0a7abd08722199f338b46d6f676d7274
|
|
| BLAKE2b-256 |
0fe8758b3a2ff215bb203e2e85f3a193c22a3910afcde8fc7254fcc0655d4efd
|
File details
Details for the file physsim-0.1.0-py3-none-any.whl.
File metadata
- Download URL: physsim-0.1.0-py3-none-any.whl
- Upload date:
- Size: 223.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12cc9b6f73964eee56ed5dfbc9940ffe1a33ecd455e39770ab6e2ad1d39d7c8b
|
|
| MD5 |
98fef0f8c3b00218ce929f45826a13c7
|
|
| BLAKE2b-256 |
ac13c3c0b905af9f6e64ab4b785504022609909c2c38ef8e80f3b9c7ae1b0166
|