Universal ONNX policy deployment for robots: any trained control policy, any hardware, bound by name not index.
Project description
efferent
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.
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:
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:
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:
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
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):
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
- Policy manifest (
manifest.yaml, inside the.appzip, or embedded in the ONNXmetadata_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_deltaetc., scale, clip, PD gains), and the driven joints by name in policy order. - 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.
- Binding validates and joins the two at startup and derives all permutations.
- 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.
- Backends implement five methods (
connect/read/write/estop/close) and are discovered via theefferent.backendsentry point, so vendor support ships as separate pip packages. Simulators are just backends (advances_time = Trueskips wall-clock pacing), which is why sim-to-sim validation is the same command with a different descriptor.
The deployment protocol as config
Real deployments are never "run one policy" — they are damp -> stand -> policy -> recover sequences, and that protocol traditionally lives in hand-written C++ per robot. In efferent it is YAML:
robot: g1_29dof.robot.yaml
initial: stand
states:
stand: {type: pose, target_from: jab, duration_s: 0.4, gain_scale: 3.5}
jab: {type: policy, policy: g1_jab.app, hook: jab_hook.py, duration_s: 11.0}
done: {type: damp, duration_s: 0.5}
transitions:
- {from: stand, to: jab}
- {from: jab, to: done}
target_from: jab means "stand exactly where the jab policy expects to
start" — because policies now declare a start envelope in their manifest
(expected pose, joint tolerance, max velocity, max tilt). The runtime checks
the envelope at every handoff and refuses an out-of-envelope transition
before a single policy command is sent:
handoff refused - robot state is outside the policy's start envelope:
joint 'waist_roll_joint' at +0.398 rad, policy expects +0.032 (tol 0.3)
base tilted 1.63 rad from upright (max 0.35)
The gate for all of this (examples/g1_jab/validate_fsm_mujoco.py) spawns
the G1 in a perturbed pose and runs the full protocol in MuJoCo: stand snaps
to the envelope pose, the handoff is approved, and the jab runs its full 11 s
at 3 degrees final tilt. Two protocol lessons are encoded in the example
config's comments: a free-standing robot under pure damping topples, and a
joint-PD stand has no balance feedback, so the stand phase must be brief —
snap and hand over.
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.pytranslates the training repo'sdeploy.yamlinto a manifest (with self-checks) and packsg1_jab.app.jab_hook.pyimplements 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'sState_Mimic.cpp.validate_jab_mujoco.pyruns 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 (76 tests, CI on 3 OS x Python 3.10-3.13) |
| Mock backend | Done |
| MuJoCo backend (any MJCF, actuator-free PD via qfrc) | Done |
| Importers: rl_sar configs, Isaac Lab / mjlab env.yaml | Done - both cross-validated against real training runs |
| CLI: doctor / audit / inspect / pack / import / validate / run (dry-run, hooks) | Done |
| doctor: zero-config fingerprinting, permutation detection, recurrent-export check | Done |
| audit: per-term obs verification vs deploy logs (frame / scale / permutation / sign / deg-rad / lag) | Done |
| Real-policy proofs (G1 29-DoF jab mimic, Go2 locomotion) + Modal cloud verification | Done |
| Recurrent policies | Done - multi-tensor state (LSTM h/c) + per-term history |
| Multi-policy FSM + start envelopes (refused handoffs) | Done - full stand-to-mimic protocol gated in MuJoCo |
| Unitree DDS backend (G1/Go2, unitree_sdk2py) | In progress - structured skeleton, needs on-robot validation |
| ros2_control bridge backend | Planned (#1) |
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-runruns 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.
Project details
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 efferent-0.4.0.tar.gz.
File metadata
- Download URL: efferent-0.4.0.tar.gz
- Upload date:
- Size: 74.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8fe17fca0e8a175da50a051311e91a01804c1a92c3e0ee2cf65bfedc3a1fa0c3
|
|
| MD5 |
e2682262360006c4a8597d75e1e4f908
|
|
| BLAKE2b-256 |
dd2b5c1d6392fe2ea9f3eb167fb61dda09019a80e9590e0ee4c0702502cdc5f9
|
Provenance
The following attestation bundles were made for efferent-0.4.0.tar.gz:
Publisher:
release.yml on Eximius-Labs/efferent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
efferent-0.4.0.tar.gz -
Subject digest:
8fe17fca0e8a175da50a051311e91a01804c1a92c3e0ee2cf65bfedc3a1fa0c3 - Sigstore transparency entry: 2333471683
- Sigstore integration time:
-
Permalink:
Eximius-Labs/efferent@799a118d9e9740620d614d12c93f9bd2d5c426fa -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/Eximius-Labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@799a118d9e9740620d614d12c93f9bd2d5c426fa -
Trigger Event:
release
-
Statement type:
File details
Details for the file efferent-0.4.0-py3-none-any.whl.
File metadata
- Download URL: efferent-0.4.0-py3-none-any.whl
- Upload date:
- Size: 64.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc2444016f3c4bbdfa98e7fc5799f248cb046e22218772d14c86bd610184d536
|
|
| MD5 |
22c7a446f37b3d06786f98a082727535
|
|
| BLAKE2b-256 |
8b8c2b522c1da78d7302dedbbb41b854d7ed83f34af0b5991325dc5fd8820dfc
|
Provenance
The following attestation bundles were made for efferent-0.4.0-py3-none-any.whl:
Publisher:
release.yml on Eximius-Labs/efferent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
efferent-0.4.0-py3-none-any.whl -
Subject digest:
bc2444016f3c4bbdfa98e7fc5799f248cb046e22218772d14c86bd610184d536 - Sigstore transparency entry: 2333471700
- Sigstore integration time:
-
Permalink:
Eximius-Labs/efferent@799a118d9e9740620d614d12c93f9bd2d5c426fa -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/Eximius-Labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@799a118d9e9740620d614d12c93f9bd2d5c426fa -
Trigger Event:
release
-
Statement type: