Metadata-Version: 2.4
Name: physsim
Version: 0.1.0
Summary: Hardware-in-the-loop robotics simulation for AI agents, built on MuJoCo
Author-email: Daniel <imperialkoi9@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/ImperialKoi/PyPhySim
Project-URL: Repository, https://github.com/ImperialKoi/PyPhySim
Project-URL: Issues, https://github.com/ImperialKoi/PyPhySim/issues
Project-URL: Changelog, https://github.com/ImperialKoi/PyPhySim/blob/main/CHANGELOG.md
Keywords: robotics,simulation,mujoco,hardware-in-the-loop,firmware,esp32,raspberry-pi,rp2040,embedded,digital-twin,ai-agents
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: C
Classifier: Programming Language :: C++
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Embedded Systems
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: System :: Emulators
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mujoco<4,>=3.11
Requires-Dist: numpy>=2.0
Provides-Extra: plot
Requires-Dist: matplotlib>=3.8; extra == "plot"
Provides-Extra: video
Requires-Dist: imageio>=2.34; extra == "video"
Requires-Dist: imageio-ffmpeg>=0.4; extra == "video"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pyflakes>=3.0; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Provides-Extra: all
Requires-Dist: physsim[plot,video]; extra == "all"
Dynamic: license-file

# 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

```bash
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:

```bash
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

```python
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.

```bash
.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

```python
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:

```python
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+, `mujoco` and `numpy` (installed with the package)
- **C/C++ firmware** additionally needs `clang++`, `c++` or `g++` on PATH
- **Plots** need `matplotlib`; **video export** needs `imageio`. Everything else —
  traces, CSV, rendered frames, the live window — needs neither.

---

## Documentation

- **[The website](docs/)** — 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](vercel.json)) or to GitHub Pages from
  `/docs`. See [docs/README.md](docs/README.md).
- **[AGENTS.md](AGENTS.md)** — the reference, written for AI agents using the library
- **[PLAN.md](PLAN.md)** — architecture, design decisions, and the milestone plan
- **[CLAUDE.md](CLAUDE.md)** — invariants and conventions for working *on* the library

Or ask the library directly:

```python
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.
