Skip to main content

Universal ONNX policy deployment for robots: any trained control policy, any hardware, bound by name not index.

Project description

Efferent - any policy, any robot

efferent

CI PyPI Python 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

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

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

  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.

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

efferent-0.3.1.tar.gz (66.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

efferent-0.3.1-py3-none-any.whl (57.8 kB view details)

Uploaded Python 3

File details

Details for the file efferent-0.3.1.tar.gz.

File metadata

  • Download URL: efferent-0.3.1.tar.gz
  • Upload date:
  • Size: 66.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for efferent-0.3.1.tar.gz
Algorithm Hash digest
SHA256 93f08120a1eb213b01d3ef8c968c12226a48ef35dc584b4ebad533caae3377b2
MD5 8c0deeac793efebec9fbde267384670d
BLAKE2b-256 6f85dad85a87192e46fc4e93cce8a7efe3184eb705959148aafc34a4d1744b92

See more details on using hashes here.

Provenance

The following attestation bundles were made for efferent-0.3.1.tar.gz:

Publisher: release.yml on Eximius-Labs/efferent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file efferent-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: efferent-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 57.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for efferent-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 aa71a227038990aa83b572c079171e929259833a7d42a1d8f9a4d0964e113d6e
MD5 8b68109103b9408a33162005c844d432
BLAKE2b-256 949de49cfb18a35c1abaea672f3f4cb334ff4758481956081fda7b135fce48ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for efferent-0.3.1-py3-none-any.whl:

Publisher: release.yml on Eximius-Labs/efferent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page