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.

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.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 (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-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.4.0.tar.gz (74.0 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.4.0-py3-none-any.whl (64.1 kB view details)

Uploaded Python 3

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

Hashes for efferent-0.4.0.tar.gz
Algorithm Hash digest
SHA256 8fe17fca0e8a175da50a051311e91a01804c1a92c3e0ee2cf65bfedc3a1fa0c3
MD5 e2682262360006c4a8597d75e1e4f908
BLAKE2b-256 dd2b5c1d6392fe2ea9f3eb167fb61dda09019a80e9590e0ee4c0702502cdc5f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for efferent-0.4.0.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.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

Hashes for efferent-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bc2444016f3c4bbdfa98e7fc5799f248cb046e22218772d14c86bd610184d536
MD5 22c7a446f37b3d06786f98a082727535
BLAKE2b-256 8b8c2b522c1da78d7302dedbbb41b854d7ed83f34af0b5991325dc5fd8820dfc

See more details on using hashes here.

Provenance

The following attestation bundles were made for efferent-0.4.0-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