Metadata-Version: 2.4
Name: efferent
Version: 0.3.1
Summary: Universal ONNX policy deployment for robots: any trained control policy, any hardware, bound by name not index.
Author-email: Arman <arman@skop.dev>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/Eximius-Labs/efferent
Project-URL: Repository, https://github.com/Eximius-Labs/efferent
Project-URL: Issues, https://github.com/Eximius-Labs/efferent/issues
Project-URL: Changelog, https://github.com/Eximius-Labs/efferent/blob/main/CHANGELOG.md
Keywords: robotics,reinforcement-learning,sim-to-real,onnx,deployment,legged-robots,mujoco
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: onnx>=1.15
Requires-Dist: onnxruntime>=1.17
Provides-Extra: mujoco
Requires-Dist: mujoco>=3.0; extra == "mujoco"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: mujoco>=3.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: build; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/Eximius-Labs/efferent/main/assets/efferent-banner-inter.png" alt="Efferent - any policy, any robot" width="100%">
</p>

# efferent

[![CI](https://github.com/Eximius-Labs/efferent/actions/workflows/ci.yml/badge.svg)](https://github.com/Eximius-Labs/efferent/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/efferent?label=pypi)](https://pypi.org/project/efferent/)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/efferent/)
[![License](https://img.shields.io/badge/license-Apache--2.0-green)](LICENSE)

**Universal ONNX policy deployment: any trained control policy, any robot — bound by name, never by index.**

*The efferent pathway carries motor commands from the brain to the muscles.
This package is that pathway for robot policies.*

![The same Go2 policy with a correct config vs. joint order off by one, and the doctor catching it](https://raw.githubusercontent.com/Eximius-Labs/efferent/main/.github/media/doctor_demo.gif)

*Above: the same trained policy, correct config vs. the classic off-by-one
joint-order bug — then `efferent doctor` catching that bug class from the
network alone, printing the exact mapping. Try it in 60 seconds, no robot:
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Eximius-Labs/efferent/blob/main/notebooks/doctor_demo.ipynb)*

A policy trained in MuJoCo, Isaac Lab, Isaac Gym, or mjlab is just an ONNX
network plus an implicit contract: what its observation vector contains, what
its actions mean, and which joints it drives. `efferent` makes that contract
explicit and portable, so deploying a policy on a new robot is configuration —
not another hand-written deploy script.

```
policy package (.app)          robot descriptor (yaml)
  policy.onnx                    joints in HARDWARE order
  manifest.yaml            +     limits, safe gains          ->  efferent run
  (obs recipe, action            backend driver id
   contract, joints in
   POLICY order)
```

The runtime joins the two **by joint name**. The hand-maintained integer
`joint_mapping` arrays that deployment repos warn about become a derived,
validated artifact — a wrong joint name is a startup error, never a fallen
robot.

## Three lines before the robot moves

The bug class that fills every deployment repo's issue tracker — obs joint
order permuted, angular velocity in the wrong frame, command dims wired to
nothing, pipeline drift vs. the training-side rollout — none of it crashes,
all of it makes robots fall, and it's usually debugged afterwards by staring
at numbers. The doctor catches it beforehand by probing the actual network:

```python
import efferent
report = efferent.doctor("policy.app", robot="g1.robot.yaml")
assert report.passed, str(report)
```

**Zero-config**: `efferent doctor policy.onnx` works on a bare ONNX with no
manifest and no YAML at all — joint count comes from the action output, and
the obs layout is inferred by scanning the network's own sensitivity
structure for the joint-indexed blocks (validated on mujoco_playground's
LEAP-hand policy: locates joint_pos/joint_vel at 94% diagonal confidence with
zero configuration). When a policy has a fully-coupled Jacobian (dynamic
quadruped gaits do), the doctor says "not inferable" and reports the
high-influence dim groups instead of hallucinating a layout.

Checks include
finite-difference **joint-order fingerprinting** — a position policy must
respond to joint *i*'s position chiefly with action *i*; if the response
pattern is instead a permutation, the doctor fails and prints the mapping it
observed so you can fix the wiring in one edit. Plus: dead-term detection
("the robot ignores the joystick"), numeric robustness under extreme inputs,
obs-recipe-vs-network size, robot binding, gain sanity, and — given an
`npz` of `obs`/`actions` recorded from your training framework's play script
(`--reference rollout.npz`) — bit-level replay divergence between training
and deployment pipelines.

## Audit a live deployment's observation pipeline

The doctor probes the network; `efferent audit` checks the code *in front of*
it — the obs builder where the frame/order/scale bugs actually live. Log raw
states plus the obs vectors your pipeline built (`npz` with `obs`, `q`, `dq`,
and optionally `quat`/`gyro`/`lin_vel`/`command`/`action`/`t`), then:

```bash
efferent audit policy.app --log deploy_log.npz --robot robot.yaml
```

Every observation term is rebuilt from the raw states (efferent's obs
builder acting as the reference implementation of the manifest semantics)
and diffed per term. Mismatched terms get hypothesis-tested against the
classic bugs, and the report names the fix:

```
[FAIL] base_ang_vel   dims 0..2   max|err| 1.085
       logged values match the WORLD-frame ang_vel - training expects the
       BODY frame (apply quat_rotate_inverse before building the obs)
[ok]   joint_pos_rel  dims 3..6   max|err| 0
```

Detected hypotheses: world-vs-body frame on velocity terms, missing/extra
scale factors (with the fitted factor), and permuted joint columns (with the
observed mapping). No hardware risk — it runs on a log file.

## Quickstart

```bash
pip install -e .[dev]

# build the self-contained demo (2-DOF arm, zero-action hold policy)
python examples/make_demo_policy.py

# what does this policy expect?
efferent inspect examples/demo_arm2.app

# run it in MuJoCo — same runtime, same code path as real hardware
efferent validate examples/demo_arm2.app --robot examples/robots/arm2.robot.yaml --duration 5

# full loop with nothing sent to the robot
efferent run examples/demo_arm2.app --robot examples/robots/arm2.robot.yaml --dry-run
```

For the G1 and Go2 examples, download the official Unitree MuJoCo models
first (~45 MB of meshes, not committed): `python examples/fetch_models.py`.

Import an existing rl_sar-style deployment (G1 example):

```bash
efferent import --from rl_sar --base base.yaml --config config.yaml -o out/
efferent validate out/policy.app --robot out/g1.robot.yaml
```

## How it works

1. **Policy manifest** (`manifest.yaml`, inside the `.app` zip, or embedded in
   the ONNX `metadata_props`) declares the observation recipe as ordered
   semantic terms (`base_ang_vel`, `projected_gravity`, `command`,
   `joint_pos_rel`, `joint_vel`, `last_action`, … with per-term scale and
   history), the action contract (`joint_position_delta` etc., scale, clip,
   PD gains), and the driven joints by name in policy order.
2. **Robot descriptor** declares hardware truth: joints by name in SDK order,
   position/torque limits (authoritative — enforced by the safety layer over
   anything the manifest claims), safe gains, and which backend driver to use.
3. **Binding** validates and joins the two at startup and derives all
   permutations.
4. **Runtime** runs soft-start -> obs build -> ONNX inference -> action mapping ->
   safety clamps -> backend write at the manifest's rate, with an action rate
   limiter, watchdog, and estop-on-failure.
5. **Backends** implement five methods (`connect/read/write/estop/close`) and
   are discovered via the `efferent.backends` entry point, so vendor support
   ships as separate pip packages. Simulators are just backends
   (`advances_time = True` skips wall-clock pacing), which is why sim-to-sim
   validation is the same command with a different descriptor.

## Proven on a real trained policy

`examples/g1_jab/` deploys a real motion-imitation policy (Unitree G1 29-DoF
jab, trained in a unitree_rl_mjlab fork — 154-dim obs, 29 actions) through
this runtime:

- `make_jab_package.py` translates the training repo's `deploy.yaml` into a
  manifest (with self-checks) and packs `g1_jab.app`.
- `jab_hook.py` implements the two motion-specific observation terms
  (`custom.motion_command`, `custom.motion_anchor_ori_b`) as custom-term
  hooks — a faithful port of the C++ deploy stack's `State_Mimic.cpp`.
- `validate_jab_mujoco.py` runs the sim-to-sim gate on the official G1 MJCF:
  spawn at the reference motion's frame 0, hand control to the policy, and
  check the deploy stack's own criteria (never exceeds the 1.0 rad
  bad-orientation estop threshold, stays standing).

Result: **PASS** — 11 s full motion, max tilt 6.3°, mean joint tracking error
0.095 rad, identical metrics on Windows and on a Modal Linux container
(`modal run scripts/modal_verify.py`).

## Proven on a second robot (Go2 quadruped)

`examples/go2_walk/` deploys rl_sar's bundled Go2 velocity policy (robot_lab
/ Isaac Lab-trained, TorchScript converted to ONNX with bit-level equivalence
verified): configs imported automatically with `efferent import`, doctor
PASS, and the sim-to-sim gate walks the official Go2 MuJoCo model 1.9 m
forward in 8 s at 14.7° max tilt — same runtime, same commands, different
robot, different vendor model (whose MJCF even lists joints in a different
order than the SDK; by-name binding absorbs it).

Two spawn lessons are baked into the MuJoCo backend because of this policy:
`auto_base_height` / `settle_s` for floating-base spawns, and the knowledge
that mimic policies must start at the motion's frame 0 (they balance actively
— a pure PD hold of their default pose falls over).

## Status

| Component | State |
|---|---|
| Spec + binding + runtime + safety | Done - implemented, tested (56 tests) |
| Mock backend | Done |
| MuJoCo backend (any MJCF, actuator-free PD via qfrc) | Done |
| rl_sar importer (verified against real G1 configs) | Done |
| CLI: inspect / pack / import / validate / run / dry-run / --hook | Done |
| Real-policy proof (G1 29-DoF jab, mimic) + Modal cloud verification | Done |
| Unitree DDS backend (G1/Go2, unitree_sdk2py) | In progress - structured skeleton — needs on-robot validation |
| Isaac Lab importer | planned |
| ros2_control bridge backend | planned |
| RNN/history policies | Done - carry + per-term history supported |

## Safety model

- Binding refuses to start on any joint-name mismatch or a default pose
  outside hardware limits.
- Descriptor limits clamp every outgoing command (positions, feed-forward
  torque, and kp scaled so PD torque cannot exceed the limit at the current
  error).
- Soft-start interpolates to the default pose with safe gains before the
  policy gets control; estop drops to damping on any failure; a watchdog
  trips on loop/state stalls.
- `--dry-run` runs the entire loop without sending a single command — always
  the first thing to run against a new robot.

Prior art this design learned from: kinfer's self-describing artifacts,
rl_sar's deployment contract vocabulary, and unitree_rl_mjlab's deploy stack.
