Deployment¶
A deployment backend runs a trained task policy on a real robot, or against a MuJoCo model of it for sim-to-sim
validation. It builds the task's own env for its observation and action terms, reads the robot's state, and sends
the action terms' commands over the robot's SDK at the control rate. The deployment guide
covers eden deploy, every supported robot, safety and timing.
from eden.extensions.deployment.base import DEPLOYMENT_REGISTRY, DeploymentBase, resolve_deployment_for
from eden.options.extensions.deployment import DeploymentOptions
A backend registers in DEPLOYMENT_REGISTRY and is bound to a robot options class with bind_deployment;
resolve_deployment_for(robot_options) picks the backend for a task's robot. Importing eden.extensions.deployment
imports every backend module so those bindings exist; a backend whose SDK is missing is skipped. The MuJoCo backend
is robot-agnostic and is looked up by name (eden deploy ... --mujoco), and a composite rig resolves to
CompositeDeployment, which drives each part through that part's own binding.
Base class and registry¶
Base class and registry for real-robot deployment backends.
Attributes¶
Classes¶
DeploymentBase
¶
DeploymentBase(env: RLEnvBase, options: DeploymentOptions)
Bases: ConfigurableMixin[DeploymentOptions], ABC
Base class for real-robot deployment backends.
| ATTRIBUTE | DESCRIPTION |
|---|---|
entity_name |
The name of the entity to deploy
TYPE:
|
control_dt |
The control time step
TYPE:
|
control_freq |
The control frequency
TYPE:
|
decimation |
The decimation factor
TYPE:
|
sync |
Whether to synchronize the control loop
TYPE:
|
connect_timeout_s |
The timeout for connecting to the robot
TYPE:
|
deploy_kp_kd_scale |
The scale of the Kp and Kd for the deployment. Recommend to try from 0.2 (20%), 0.5 (50%), 0.8 (80%), 1.0 (100%)
TYPE:
|
auto_yaw_align |
If True (default), capture the robot's IMU yaw at START (the MuJoCo
backend also captures it on :meth:
TYPE:
|
estimate_lin_vel |
If True, run an onboard contact-aided Kalman filter
(:class:
TYPE:
|
feet_links |
Foot link names for the velocity estimator's leg odometry and contact
detection. Defaults to the robot's own |
lin_vel_estimator_options |
Extra keyword overrides for the estimator (noise parameters,
TYPE:
|
ignore_action_terms |
Names of action terms whose command :class: |
disable_joints |
Joint names or |
gain_ramp_steps |
Control steps over which :meth:
TYPE:
|
debug_record |
If True, record per control step the command actually sent (target
positions + kp/kd) and the measured robot proprioception (joint pos /
vel / torque, base quat / ang-vel / lin-acc). For offline diagnosis
(e.g. commanded-vs-measured tracking, torque saturation, limit-cycle
frequency). Every numeric Streamed to disk as it is produced, not held until :meth:
TYPE:
|
debug_record_path |
Destination
TYPE:
|
debug_record_flush_steps |
Steps per streamed chunk (default 100 — two seconds at 50 Hz), i.e. the most that a kill can cost. Lower it to lose less and write more often; the writing itself is off the control thread either way.
TYPE:
|
command_overrides |
Command terms to substitute at deploy time,
TYPE:
|
operator_keyboard |
Open the terminal keyboard reader (
TYPE:
|
target_lpf |
First-order low-pass on the commanded joint targets, applied to every
payload via :meth:
TYPE:
|
init_target |
Where the INIT ramp drives the joints before control is handed to the policy.
TYPE:
|
init_steps |
Control cycles the INIT ramp takes from the pose measured at the press to the
TYPE:
|
init_dofs_kp_scale |
Scale on the kp sent during that ramp (default 1.0).
TYPE:
|
init_dofs_kd_scale |
Scale on the kd sent during that ramp (default 1.0).
TYPE:
|
on_motion_end |
What a motion-imitation deploy does once the clip reaches its final frame.
TYPE:
|
stand_steps |
Control cycles the
TYPE:
|
motion_end_slot |
1-based policy slot
TYPE:
|
policy_seconds |
End the run once the policy has driven the robot this long since its latest START, as the
operator's KILL does (
TYPE:
|
Source code in eden/extensions/deployment/base.py
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 | |
Attributes¶
robot_options
instance-attribute
¶
control_dt
class-attribute
instance-attribute
¶
control_dt: float | None = self._resolve_control_dt()
sending_hold
property
¶
sending_hold: bool
Whether the payload :meth:send_payload is sending is the state machine's own, not the policy's.
True for the STOP/boot hold, the INIT ramp and the START tick, the commands that hold or
place the robot's joints. A backend whose robot carries a tool the joints do not describe
leaves it alone on these: the Franka keeps its gripper at the policy's last command, so a
STOP or INIT does not open a grasp. False for a command sent from outside :meth:step.
init_done
property
¶
init_done: bool
Whether an INIT ramp has reached its target (and holds it until START or STOP).
policy_active
property
¶
policy_active: bool
Whether the action passed to :meth:step reaches the robot this tick.
False until START hands control over, and again after STOP or INIT.
blocks_for_the_period
property
¶
blocks_for_the_period: bool
Whether :meth:send_payload itself waits out the control period (and so paces the loop). False here.
A composite rig with such a part lets it pace the rig, as it paces itself deployed alone
(:class:~eden.extensions.deployment.composite.CompositeDeployment).
provides_base_lin_vel
property
¶
provides_base_lin_vel: bool
Whether base_lin_vel observations read a real value on this deploy, or zeros.
:class:RobotStateEntity holds the base linear velocity at zero unless something
supplies it: the onboard estimator (estimate_lin_vel) or a backend that publishes it
in RobotState.extra (:meth:_publishes_base_lin_vel -- the MuJoCo backend's ground
truth; no hardware backend does, an IMU measures no velocity). A policy or task whose
observations read it on a deploy where neither holds is silently-wrong control, which
is why the CLI refuses that arrangement at startup rather than letting it run.
Methods:¶
wire_gains
¶
The kp/kd this deployment sends for a policy trained on robot, in deployment DOF order.
robot is a policy's pinned :class:RobotOptions (PolicyBase.robot_options()): its
default_dofs_kp / default_dofs_kd on every DOF it names, the deployed robot's on
the rest, matched by DOF name. None -- a policy pinning no robot, a checkpoint trained
in this env -- or a robot declaring no gains (a composite, or an empty map, as
G1Dofs23's) is the deployed robot's throughout. Both are then scaled by
deploy_kp_kd_scale and zeroed on :attr:disabled_dofs, the operator's choices for the
whole run.
Raises ValueError as :func:resolve_pinned_gains does, which resolves the pinned maps.
Source code in eden/extensions/deployment/base.py
set_gains
¶
set_gains(dofs_kp: ndarray, dofs_kd: ndarray, *, ramp_steps: int | None = None) -> None
Move the kp/kd every command sends to dofs_kp / dofs_kd over ramp_steps control steps.
The targets are wire gains in deployment DOF order -- :meth:wire_gains' output, already
scaled and with disabled joints zeroed. ramp_steps defaults to :attr:gain_ramp_steps;
0 jumps, which is only right before :meth:connect, while nothing is on the wire.
The ramp is linear from whatever is being sent now, a ramp still in progress included, and
advances once per :meth:step. Every payload reads the current gains -- the policy's, the
STOP hold, the INIT ramp, the end-of-clip stand -- so all of them follow it. A policy keeps
driving through it: holding the pose instead topples a humanoid, which only its policy
balances (see "Each slot runs at its own PD gains" in docs/guides/deployment.md).
Raises on a backend that does not send each command's gains (:attr:sends_command_gains):
there the call would change nothing on the robot, which is not what its caller asked for.
Source code in eden/extensions/deployment/base.py
programmed_gains
¶
programmed_gains() -> tuple[ndarray, ndarray] | None
The kp/kd the robot runs whichever slot is active, on a backend without :attr:sends_command_gains.
In deployment DOF order and in :meth:wire_gains' units, so a slot's gains can be compared
with them; None when the robot's gains are not in those units (XHand firmware units, a
controller's own stiffness) or it has none (a position loop), which is the default. A
backend that sends each command's gains runs no fixed set, and is never asked.
Source code in eden/extensions/deployment/base.py
command_gain_dofs
¶
Per DOF, whether each command's kp/kd reaches its motor: :attr:sends_command_gains, on every DOF.
A composite rig answers per part, since its body may take each command's gains while its
hands run their own (:class:~eden.extensions.deployment.composite.CompositeDeployment).
Source code in eden/extensions/deployment/base.py
write_dofs_targets
¶
Write absolute joint-position targets directly into the processed-action buffer.
Bypass for an extension-policy slot, which converts its policy's raw action
with that policy's own controller and writes the resulting targets here,
instead of handing raw actions to the env's action manager (which belongs to
another policy). targets[i] is name-mapped onto deployment DOF
dofs_name[i]; follow with step(None), which builds the payload from
this buffer without running the action manager.
Joint positions only — the payload's dofs_vel and dofs_torque fields
have no writer on this path, which is what keeps a velocity, torque or
velocity-feedforward controller out of a deploy slot.
Raises KeyError if a name is not a deployment DOF.
Source code in eden/extensions/deployment/base.py
action_to_payload
¶
Source code in eden/extensions/deployment/base.py
request_init
¶
INIT, as the operator's key or remote does: ramp from the measured pose to the init_target pose.
Source code in eden/extensions/deployment/base.py
request_start
¶
START, as the operator's key or remote does: hand control to the policy.
A script that drives a deployment without an operator calls this explicitly -- nothing a
backend does on its own starts the policy on a robot (:attr:allow_unattended_start).
Source code in eden/extensions/deployment/base.py
request_stop
¶
STOP, as the operator's key or remote does: disengage the policy and hold the measured pose.
consume_start_events
¶
consume_start_events() -> int
Return and clear the number of START presses since the last drain.
The deploy loop re-arms the active policy slot on START (reset internal state, rewind
command clocks) because :meth:_handle_start re-captures the yaw offset — any policy
state accumulated while stepping during STOP/hold references the pre-START world frame.
Source code in eden/extensions/deployment/base.py
motion_finished
¶
motion_finished() -> bool
True once every tracked motion reference has played through to its final frame.
A task drives its reference through the motion manager, through a MotionCommand,
or through both, and this reads whichever are present — the same two paths
:meth:_advance_motion_reference ticks and :meth:reset_motion_reference rewinds.
Reading only the manager would leave on_motion_end silently inert on every
command-path deploy (SONIC, beyondmimic_moves), which is most of them.
The signals are the clocks the observation terms see, so hardware and --mujoco
agree on when the clip is over. False when no reference is present (a plain RL
env, or an entity no motion term tracks — a config question, not a reason to park the
robot mid-run) and when any present reference is still running.
Costs one device sync per present reference, per control step, until the end fires —
a host-side branch needs the value on the host. Callers latch: on_motion_end
stops asking once it has acted (_standing_from / _motion_end_switched), and
on_motion_end="continue" never asks at all.
Source code in eden/extensions/deployment/base.py
consume_policy_switches
¶
Drain and return pending policy-switch requests (0-based target index or "next").
The deploy loop calls this every step and validates the targets against the loaded slot
count (which the deployment does not know). Requests come from an operator remote /
keyboard on the hardware backends and from on_motion_end="switch" here, so both
reach the loop by the one route — and a MuJoCo sim-to-sim run switches slots exactly as
the robot does.
Source code in eden/extensions/deployment/base.py
request_policy_switch
¶
Queue a slot switch for the deploy loop to apply on its next iteration.
step
¶
Send one control step's command, then read and return the resulting observation.
action is the policy's raw output, run through the env's action manager by
:meth:action_to_payload. It is None for an extension-policy slot, which has
already written its targets with :meth:write_dofs_targets: the payload is then
built from that buffer alone, with the action manager left out of it.
None before any such write is the third case — a caller that has nothing to
send this step — and sends nothing rather than commanding a zero-initialised buffer,
which on hardware is every joint driven to 0.0 rather than left alone. Hence the
_direct_targets_written flag: an all-zero _processed_action is a legitimate
command (every joint to its zero position) and cannot be told from an untouched
buffer by inspecting it.
Every backend sends what the operator state machine wants: the hold while stopped, the
INIT ramp, and the policy's command only under START, with the end-of-clip handover
(on_motion_end) taking precedence over the policy. Sending nothing (the third case
above) is :meth:_idle_tick: MuJoCo does not advance and hardware keeps its last command.
Source code in eden/extensions/deployment/base.py
2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 | |
reset
¶
Boot in STOP/hold and return the first observation: nothing moves until the operator says so.
Every backend runs the same INIT/START/STOP/KILL state machine (see "The operator state
machine" in docs/guides/deployment.md). After this, :meth:step holds the measured pose
until INIT ramps the joints to the init_target pose and START hands control to the
policy -- from the robot's remote, the keyboard, or a script's :meth:request_init /
:meth:request_start. With none of them open, a hardware backend holds in STOP for good.
Source code in eden/extensions/deployment/base.py
reset_motion_reference
¶
Restart a motion-imitation reference cleanly at frame 0 (no-op for non-mimic envs).
Both reference paths are rewound, for the same reason :meth:_advance_motion_reference
advances both: a task drives its reference through the motion manager, through a
MotionCommand, or through both, and the deploy loop is the only thing that reaches
either one.
- the manager path — :meth:
MotionManager.rewind_to_startfor each env's currently-assigned clip, then a refill of the reference buffer; - the command path — :meth:
CommandTerm.rewind_reference, the clock-only rewind.
Motion selection is NOT resampled on either (unlike :meth:MotionManager.reset): a deploy
replays the same clip deterministically instead of the sim's random start phase
(motion_start_from_middle_range), which is correct for a robot that begins from a
stand. Called at deploy reset (before its INIT loop), at INIT, at START, and on every
policy switch.
Rewinding the command path matters most at the handover the operator gate creates. The
clip holds while the robot is in INIT or hold (:meth:step does not advance it), so
without this a STOP at frame 500 followed by a START hands the policy a reference 500
frames into the motion while the robot is standing in its INIT stance — a mid-clip pose it
is nowhere near, on a biped.
Source code in eden/extensions/deployment/base.py
state_to_observation
¶
Sync state onto the env's robot entity and compute the policy's observation dict.
post_physics_refresh is the deploy loop's phase 7. :meth:step passes True so an
observation term with per-step state (:meth:ObservationTerm.post_physics_refresh
<eden.managers.observation_manager.ObservationTerm.post_physics_refresh>) advances exactly once
per control tick, on the freshly synced robot state and before the read — the sim step runs it
at phase 7 and a deploy that skipped it would hand the policy a value frozen at its last
simulated step, silently (the class of bug _advance_motion_reference documents). The reset
paths leave it False: EnvBase.reset runs no phase 7 either, so a term reads as zero
there in both places. A command term's post_physics_refresh still does not run on this path
(see :meth:CommandTerm.post_physics_refresh
<eden.managers.command_manager.CommandTerm.post_physics_refresh>); the observation hook does
because the policy's observations are the hardware path.
The first read of an episode -- after construction, :meth:reset or START -- also clears the
observation manager's per-step state and the action manager's last actions, in the order an
env reset does: this tick's phase 7, then reset(), then post_reset() (a term that
rebuilds its state from the current frame, as ParkourProprio backfills its history, needs
it), then the read, which backfills every history window from the robot. Without it the
policy's first frames come from the training wrapper's env.reset() on the simulated
robot at its spawn pose, or, at START, from the hold and INIT ramp in the yaw frame START just
replaced. Clearing
on the read rather than in each reset covers a backend override that does not chain to
this class's, and a loop that drives :meth:step with no reset at all.
Overrides must accept the keyword (and forward it to super()); the constructor refuses
one that does not (:meth:_check_state_to_observation_override).
One tick of skew relative to the sim, inherent to this loop's order: :meth:step publishes the
new command (_update_commands) before read_state, so a term that integrates
command − measured here pairs this tick's measurement with the command just published,
where phase 7 in the sim pairs it with the command that was in effect during the physics.
Source code in eden/extensions/deployment/base.py
restart_observations
¶
Start every observation window afresh from the last reading, as START does; returns the observation.
The deploy loop calls it when it switches policy slots, so the policy switched in reads what
an env reset would have handed it, with every history window backfilled from the robot as it
is now, a fed-back last action of zero, and the motion reference the switch just rewound.
Without it the switched-in policy reads frames observed while another slot drove, with the
reference still mid-clip, and its own last action from before it was switched out. The
reading is not re-synced and the velocity estimator not re-run: the state is the one the last
:meth:step already observed.
Source code in eden/extensions/deployment/base.py
connect
abstractmethod
¶
close
abstractmethod
¶
read_state
abstractmethod
¶
SoftShutdownMixin
¶
SoftShutdownMixin(options: T)
Bases: ConfigurableMixin, ABC
Settle the robot into a damping state before a backend's close drops its link.
Dropping the link outright leaves the robot holding its last stiff PD target until an
onboard comms watchdog trips, and a standing robot can lurch or collapse. :meth:_soft_shutdown
instead sends shutdown_damping_steps ticks of kp=0, kd>0: the position servo is off, only
velocity damping remains, and the joints settle gently.
With a :class:~eden.extensions.deployment.utils.republisher.CommandRepublisherMixin running,
no watchdog can trip while the process lives: a control thread that stalls leaves the loop
re-sending its last command at full gains, and the loop reports that rather than acting on it
(see "A stalled control thread is reported, not acted on" in docs/guides/deployment.md).
This ramp is then the only orderly way down, which is why such a backend's close runs it
before _stop_publisher -- the damping command goes out at the same packet rate as any other.
Mixed in ahead of :class:DeploymentBase by the backends that run the ramp, which call
:meth:_soft_shutdown from close and implement :meth:_shutdown_hold_pos. The two options
live here rather than on :class:DeploymentBase so a backend without the ramp keeps rejecting
them instead of accepting and ignoring them.
shutdown_damping_steps: int
Control ticks to send the damping command before close releases the link.
shutdown_damping_kd_scale: float
Scale on kd during that soft shutdown.
Source code in eden/utils/common.py
Functions:¶
bind_deployment
¶
Register robot_cls as driven by the named deployment in :data:DEPLOYMENT_REGISTRY.
Resolution walks the robot's MRO, so subclasses inherit the binding unless they register their own override. Call this once per robot class in the deployment module that supports it; collisions are not detected (the last binding wins).
Source code in eden/extensions/deployment/base.py
resolve_deployment_for
¶
resolve_deployment_for(robot_options: RobotOptions | CompositeEntityOptions) -> type[DeploymentBase]
Return the deployment class registered for type(robot_options).
Walks the MRO to honor inheritance (a subclass with no binding falls
back to its parent's). Raises KeyError if no ancestor is bound.
A composite rig (:class:~eden.options.entities.CompositeEntityOptions — an arm with a hand
welded on, a humanoid with hands) resolves to composite_deployment, which drives each of
its parts through that part's own binding.
Source code in eden/extensions/deployment/base.py
resolve_pinned_gains
¶
resolve_pinned_gains(robot: Any, dofs_name: Sequence[str]) -> tuple[dict[str, float], dict[str, float]]
A policy's pinned gains on the DOFs of dofs_name it names, as ({dof: kp}, {dof: kd}).
A default_dofs_kp / default_dofs_kd that robot leaves empty (G1Dofs23's,
which trains on the asset's gains), or a robot with no dofs_name at all (None, a
composite), resolves to {}: it declares no gains, and the deployed robot's apply.
:meth:DeploymentBase.wire_gains builds a slot's wire gains from this.
Needs no deployment, so eden deploy runs it for every slot before constructing one: a
raise here must land before the entity swap and before an operator device is opened.
Raises ValueError when a non-empty gain map leaves out a joint of dofs_name the
robot has (and when a pattern in it matches none of the robot's joints): see "Each slot
runs at its own PD gains" in docs/guides/deployment.md.
Source code in eden/extensions/deployment/base.py
resolve_dofs_values
¶
Resolve a pattern-keyed per-DOF dict into an array aligned with dofs_name.
:func:resolve_matching_names_values with preserve_order=True returns
matches in query-pattern order, so the values must be scattered through
the returned indices — zipping them positionally with dofs_name
scrambles any dict with wildcard keys. DOFs not covered by any pattern
resolve to 0.0 (with a warning: a zero kp/kd reaching a real robot means a
limp joint).
Source code in eden/extensions/deployment/base.py
Options¶
Base configuration options for real-robot deployment backends.
Classes¶
DeploymentOptions
¶
Bases: DynamicConfigurableOptions
Options for deployment extensions.
| PARAMETER | DESCRIPTION |
|---|---|
entity_name
|
The name of the entity to deploy
|
control_dt
|
The control time step in seconds. Defaults to None, in which case it is determined by the control frequency, fallback to sim dt * decimation.
|
control_freq
|
The control frequency in Hz. Defaults to None, in which case it is determined by 1/control_dt.
|
decimation
|
The decimation factor. Defaults to None, in which case sim decimation is used.
|
sync
|
Whether to synchronize the control loop. Defaults to True.
|
connect_timeout_s
|
The timeout for connecting to the robot. Defaults to 5.0 seconds.
|
auto_yaw_align
|
Capture the robot's IMU yaw at START (the MuJoCo backend also captures
it on
|
Source code in eden/options/options.py
Attributes¶
Hardware backends¶
Unitree G1¶
Unitree G1 real-robot deployment backend.
Attributes¶
G1_JOINT_IDX
module-attribute
¶
G1_JOINT_IDX: dict[str, int] = {'left_hip_pitch_joint': 0, 'left_hip_roll_joint': 1, 'left_hip_yaw_joint': 2, 'left_knee_joint': 3, 'left_ankle_pitch_joint': 4, 'left_ankle_roll_joint': 5, 'right_hip_pitch_joint': 6, 'right_hip_roll_joint': 7, 'right_hip_yaw_joint': 8, 'right_knee_joint': 9, 'right_ankle_pitch_joint': 10, 'right_ankle_roll_joint': 11, 'waist_yaw_joint': 12, 'waist_roll_joint': 13, 'waist_pitch_joint': 14, 'left_shoulder_pitch_joint': 15, 'left_shoulder_roll_joint': 16, 'left_shoulder_yaw_joint': 17, 'left_elbow_joint': 18, 'left_wrist_roll_joint': 19, 'left_wrist_pitch_joint': 20, 'left_wrist_yaw_joint': 21, 'right_shoulder_pitch_joint': 22, 'right_shoulder_roll_joint': 23, 'right_shoulder_yaw_joint': 24, 'right_elbow_joint': 25, 'right_wrist_roll_joint': 26, 'right_wrist_pitch_joint': 27, 'right_wrist_yaw_joint': 28}
Classes¶
UnitreeG1Deployment
¶
UnitreeG1Deployment(env: EnvBase, options: DeploymentOptions)
Bases: _UnitreeDdsDeployment
Drive a Unitree G1 over the unitree_hg DDS low-level interface.
Configurable options (in addition to :class:~eden.extensions.deployment.unitree_dds._UnitreeDdsDeployment's):
publish_rate: float | None
Rate at which the held LowCmd_ is re-sent between control ticks (default
500 Hz, Unitree's own low-level rate). None sends one packet per control tick.
The G1's ankle *and* -- on the 3-DOF-waist 29-DOF build -- waist roll/pitch are
parallel A/B linkages, driven here in PR (pitch/roll) mode via ``mode_pr``. That is
the same joint class whose per-packet PD made the LimX Oli's waist buzz at one
packet per 50 Hz tick: the derivative term acts on a 20 ms-old velocity,
which at 25 Hz is anti-damping. 500 Hz is what both vendor references use --
Unitree's own ``g1_low_level_example`` publishes at ``control_dt_ = 0.002``, and
NVIDIA's SONIC G1 deploy runs a dedicated 500 Hz ``LowCommandWriter`` thread beside
its 50 Hz control thread -- so it is the rate the robot is expected to be fed at,
independent of how fast any policy runs.
Unlike the Oli case this has **not** been measured on a G1: the mechanism and the
rate are taken from those two references, not from a hardware fit. ``--publish_rate 0``
sends one packet per tick, which is the A/B to run before concluding anything.
Source code in eden/extensions/deployment/unitree_g1.py
Functions:¶
Unitree Go2¶
Unitree Go2 real-robot deployment backend.
Attributes¶
GO2_JOINT_IDX
module-attribute
¶
GO2_JOINT_IDX: dict[str, int] = {'FR_hip_joint': 0, 'FR_thigh_joint': 1, 'FR_calf_joint': 2, 'FL_hip_joint': 3, 'FL_thigh_joint': 4, 'FL_calf_joint': 5, 'RR_hip_joint': 6, 'RR_thigh_joint': 7, 'RR_calf_joint': 8, 'RL_hip_joint': 9, 'RL_thigh_joint': 10, 'RL_calf_joint': 11}
Classes¶
UnitreeGo2Deployment
¶
UnitreeGo2Deployment(env: EnvBase, options: DeploymentOptions)
Bases: _UnitreeDdsDeployment
Drive a Unitree Go2 over the unitree_go DDS low-level interface.
Configurable options (in addition to :class:~eden.extensions.deployment.unitree_dds._UnitreeDdsDeployment's):
publish_rate: float | None
Rate at which the held LowCmd_ is re-sent between control ticks (default 500 Hz).
None sends one packet per control tick.
This is vendor-rate parity, not a fault fix. The Go2's twelve joints are all serial --
its ``LowCmd_`` has no ``mode_pr``, as there is no parallel linkage to transform -- so
the per-packet parallel-linkage fault the republisher exists for on the Oli and G1
does not apply here. 500 Hz is what Unitree's own Go2 low-level example feeds the robot
at (``go2_stand_example.py``: ``dt = 0.002`` and a 2 ms ``RecurrentThread`` writer),
independent of any policy rate; one packet per 50 Hz tick is outside that example's own
``0.001~0.01`` s range. ``--publish_rate 0`` sends one packet per tick.
Source code in eden/extensions/deployment/unitree_dds.py
Functions:¶
Unitree Go1¶
Unitree Go1 real-robot deployment backend (legacy robot_interface UDP SDK).
Mirrors the structure of :mod:eden.extensions.deployment.unitree_go2 — same
registry/binding, _dof_index Eden→SDK map, and Eden-order
RobotState/RobotCommand contract — but the transport is the legacy
unitree_legged_sdk (synchronous UDP.Recv/Send + Safety) rather
than the DDS unitree_sdk2py stack Go2/G1 use.
Attributes¶
GO1_JOINT_IDX
module-attribute
¶
GO1_JOINT_IDX: dict[str, int] = {'FR_hip_joint': 0, 'FR_thigh_joint': 1, 'FR_calf_joint': 2, 'FL_hip_joint': 3, 'FL_thigh_joint': 4, 'FL_calf_joint': 5, 'RR_hip_joint': 6, 'RR_thigh_joint': 7, 'RR_calf_joint': 8, 'RL_hip_joint': 9, 'RL_thigh_joint': 10, 'RL_calf_joint': 11}
Classes¶
UnitreeGo1Deployment
¶
UnitreeGo1Deployment(env: EnvBase, options: DeploymentOptions)
Bases: SoftShutdownMixin, DeploymentBase
Source code in eden/extensions/deployment/unitree_go1.py
Attributes¶
Methods:¶
connect
¶
Source code in eden/extensions/deployment/unitree_go1.py
close
¶
Source code in eden/extensions/deployment/unitree_go1.py
read_state
¶
Source code in eden/extensions/deployment/unitree_go1.py
send_payload
¶
Source code in eden/extensions/deployment/unitree_go1.py
Functions:¶
LimX Oli¶
LimX Dynamics Oli (HU_D04) real-robot deployment backend.
Talks to the robot through the LimX low-level SDK (limxsdk, the Python
wheel shipped in https://github.com/limxdynamics/limxsdk-lowlevel): joint
state and IMU arrive on subscription callbacks, commands go out via
publishRobotCmd. Command/state arrays are indexed in the robot's motor
order, which the backend maps to/from Eden's dofs_name order by motor
name read live from the robot (getMotorNames()) — LimX's own docs and
shipped controller YAMLs disagree on the head-joint order, so a positional
map would be silently wrong.
Commands are sent in PR (pitch/roll, serial-joint) space with the SDK's
parallel_solve_required flag set, so the SDK converts to the physical
A/B motors of the parallel ankle/waist linkages. This matches Eden's
:class:~eden.options.robots.limx_oli.Oli model (the serial
HU_D04_01.urdf) and LimX's own RL deployment stack; state read back in
this mode is PR-space as well.
Attributes¶
LIMX_OLI_CLAMP_TORQUE_LIMITS
module-attribute
¶
LIMX_OLI_CLAMP_TORQUE_LIMITS: dict[str, float] = {'*_ankle_*_joint': 80.0, 'waist_roll_joint': 80.0, 'waist_pitch_joint': 80.0}
Classes¶
LimxOliDeployment
¶
LimxOliDeployment(env: EnvBase, options: DeploymentOptions)
Bases: CommandRepublisherMixin, SoftShutdownMixin, DeploymentBase
LimX Oli deployment backend on top of :class:DeploymentBase.
Adds, on top of the base options:
robot_ip: IP the LimX SDK connects to.None(default) resolves to theROBOT_IPenvironment variable, then to the real robot's10.192.1.2; point it at127.0.0.1for the LimX MuJoCo simulator.shutdown_damping_steps/shutdown_damping_kd_scale: soft shutdown — cycles to hold a damping (kp=0, kd) state before releasing.soft_torque_limit: fraction of each joint's torque budget the position term of the implied PD torque may use; every commanded target is clamped toq ± soft_torque_limit * budget / kparound the measured pose. Same 0.95 default as LimX's own walk controller, but without the velocity term theirs includes -- see :meth:_clamp_torque_feasiblefor the hardware measurements behind that.0disables the clamp.clamp_torque_limits: DOF-name pattern -> N·m overrides on top of the robot'sdefault_dofs_force_limitsfor that budget. Defaults to LimX's deploy-timeuser_torque_limitwhere it differs from Eden's training limits: 80 N·m on the six parallel ankle/waist PR joints (two 42 N·m A/B motors each). Training keeps 42 -- the shipped action scale derives from it -- so this is a deploy-only budget.publish_rate: rate at which the current wire command is re-sent between control ticks (default 1000 Hz, LimX's own rate). Load-bearing on this robot: in PR mode the robot evaluates the PD law of the six parallel-linkage joints (ankle pitch/roll, waist roll/pitch) once per received packet, on that packet's state -- measured on hardware, where their torque fitskp*(q_des-q) - kd*dq[previous packet]at R^2 0.92-0.99 while the serial joints fit the same-instant velocity. At 50 Hz that damping term acts on a 20 ms-old velocity, which is anti-damping at 25 Hz: the waist-roll linkage rings at exactly that frequency once a foot strike kicks it.Nonesends one packet per control tick, for A/B comparison only.motor_name_map: Eden DOF name → SDK motor name overrides for robots whosegetMotorNames()vocabulary doesn't match the URDF joint names (unmapped DOFs still match by suffix-normalized name).verbose_diagnostics: also log warning/OK robot diagnostics as they change. By default only error-level (2) diagnostics are logged — the robot's diagnostic stream is chatty and the OK/warning transitions are commissioning detail, but a fault must never be silent.
Source code in eden/extensions/deployment/limx_oli.py
Attributes¶
clamp_torque_limits
class-attribute
instance-attribute
¶
clamp_torque_limits: dict = dict(LIMX_OLI_CLAMP_TORQUE_LIMITS)
Methods:¶
connect
¶
Source code in eden/extensions/deployment/limx_oli.py
close
¶
Source code in eden/extensions/deployment/limx_oli.py
read_state
¶
Source code in eden/extensions/deployment/limx_oli.py
send_payload
¶
Source code in eden/extensions/deployment/limx_oli.py
Functions:¶
ensure_limx_sdk
¶
Raise a descriptive error if the LimX SDK is not importable.
Source code in eden/extensions/deployment/limx_oli.py
resolve_clamp_torque_limits
¶
resolve_clamp_torque_limits(robot_options: RobotOptions, dofs_name: list[str], overrides: dict[str, float]) -> ndarray
Per-DOF torque budget for the deploy clamp: the robot's force limits with overrides on top.
overrides maps DOF-name patterns to N·m. It is resolved on its own and scattered onto the
force-limit array instead of being merged into one dict, because
:func:~eden.utils.string.resolve_matching_names_values refuses a DOF matched by two patterns
(waist_*_joint from the robot options and waist_roll_joint from the overrides would
collide). A pattern matching no DOF raises, so a typo cannot silently leave a joint at the
robot's limit.
Source code in eden/extensions/deployment/limx_oli.py
Franka Panda¶
Franka Panda real-robot deployment backend.
Attributes¶
PANDA_ARM_JOINT_IDX
module-attribute
¶
PANDA_ARM_JOINT_IDX: dict[str, int] = {'joint1': 0, 'joint2': 1, 'joint3': 2, 'joint4': 3, 'joint5': 4, 'joint6': 5, 'joint7': 6}
PANDA_FINGER_JOINTS
module-attribute
¶
Classes¶
FrankaPandaDeployment
¶
FrankaPandaDeployment(env: EnvBase, options: DeploymentOptions)
Bases: DeploymentBase
Deployment backend for Franka Emika Panda / FR3 via panda-py.
The 7-DOF arm is driven through panda-py's real-time
:class:controllers.JointPosition controller running at 1 kHz inside
libfranka. The Franka Hand gripper is driven through libfranka's
blocking Gripper API, dispatched from a worker thread so the
per-step :meth:send_payload call never blocks the control loop.
The arm is fixed-base with no IMU, so :meth:read_state returns an
identity base_quat, zero base angular velocity and zero linear acceleration.
Requirements
panda-pythonmust be installed with a libfranka build that matches the controller's reported server version. See https://github.com/JeanElsner/panda-py/releases for per-firmware wheels.- FCI must be activated in Desk and the joint brakes released.
- A PREEMPT_RT kernel is strongly recommended for stable 1 kHz control;
without it expect occasional
communication_constraints_violation.
| PARAMETER | DESCRIPTION |
|---|---|
robot_ip
|
IP address of the Franka Control box. Defaults to the factory IP.
|
arm_stiffness
|
Optional 7-element per-joint gains for the JointPosition controller.
If
|
arm_damping
|
Optional 7-element per-joint gains for the JointPosition controller.
If
|
arm_filter_coeff
|
Exponential low-pass coefficient applied to the JointPosition
setpoint inside panda-py's 1 kHz control loop.
|
velocity_safety_factor
|
Fraction of Panda's per-joint velocity limits that the deployment
will allow per control step.
|
use_grasp_on_close
|
When
|
close_width_threshold
|
Target width (m) below which a command is treated as a close
(uses
|
gripper_speed
|
Default gripper move speed (m/s).
|
gripper_force
|
Default grasp force (N) for the :meth:
|
gripper_epsilon_inner
|
Tolerance (m) for :meth:
|
gripper_epsilon_outer
|
Tolerance (m) for :meth:
|
skip_gripper_homing
|
If
|
Like
|
|
command
|
|
until
|
|
velocity
|
|
the
|
|
it
|
|
since
|
|
Source code in eden/extensions/deployment/franka_panda.py
Attributes¶
Methods:¶
connect
¶
Source code in eden/extensions/deployment/franka_panda.py
close
¶
Source code in eden/extensions/deployment/franka_panda.py
read_state
¶
Source code in eden/extensions/deployment/franka_panda.py
send_payload
¶
Source code in eden/extensions/deployment/franka_panda.py
grasp
¶
Grasp (synchronously) an object of approximately width meters.
Returns True when the firmware reports the fingers clamped on
something within the configured tolerance. Sets the background
gripper worker's target to width first, so the worker does not
countermand the grasp with a move of its own.
Source code in eden/extensions/deployment/franka_panda.py
recover
¶
Clear an error state on the Franka controller (e.g. after a reflex).
Functions:¶
RobotEra XHand¶
RobotEra XHand real-robot deployment backend.
Attributes¶
XHAND_FINGER_IDX
module-attribute
¶
XHAND_FINGER_IDX: dict[str, int] = {'right_hand_thumb_bend_joint': 0, 'right_hand_thumb_rota_joint1': 1, 'right_hand_thumb_rota_joint2': 2, 'right_hand_index_bend_joint': 3, 'right_hand_index_joint1': 4, 'right_hand_index_joint2': 5, 'right_hand_mid_joint1': 6, 'right_hand_mid_joint2': 7, 'right_hand_ring_joint1': 8, 'right_hand_ring_joint2': 9, 'right_hand_pinky_joint1': 10, 'right_hand_pinky_joint2': 11, 'left_hand_thumb_bend_joint': 0, 'left_hand_thumb_rota_joint1': 1, 'left_hand_thumb_rota_joint2': 2, 'left_hand_index_bend_joint': 3, 'left_hand_index_joint1': 4, 'left_hand_index_joint2': 5, 'left_hand_mid_joint1': 6, 'left_hand_mid_joint2': 7, 'left_hand_ring_joint1': 8, 'left_hand_ring_joint2': 9, 'left_hand_pinky_joint1': 10, 'left_hand_pinky_joint2': 11}
Classes¶
RoboTeraXHandDeployment
¶
RoboTeraXHandDeployment(env: EnvBase, options: DeploymentOptions)
Bases: DeploymentBase
Deployment backend for the RoboTera XHand1 dexterous hand.
Supports both EtherCAT and RS485 communication protocols. For RS485 you
must also set serial_port and baud_rate to match your hardware.
The hand is treated as a fixed-base robot: read_state returns an
identity quaternion and zero angular velocity because the hand has no IMU.
Joint velocity is a finite difference. The SDK's FingerState_t has a position and
no velocity, so read_state differences consecutive positions over the wall-clock time
between the two reads (monotonic clock). The first read after connect has no previous
sample and reports zero. It is a control-period average, not the instantaneous value the
simulator reports, so expect it to be noisier and a period late.
The hand runs its own PD, with the gains below -- not the command's. finger_command
carries kp / ki / kd as int16 values in firmware units, which have no known
mapping to the N·m/rad of :attr:RobotCommand.dofs_kp (the xhand1 sysid example fits the
effective sim-side gains for that reason). So send_payload forwards positions only:
RobotCommand.dofs_kp / dofs_kd / dofs_vel / dofs_torque are not sent, and the
firmware gains are set once at connect from dofs_kp / dofs_ki / dofs_kd.
A payload with kp = 0 therefore changes nothing on the hand. The two base options that
work through command gains, deploy_kp_kd_scale and disable_joints, are refused at
construction rather than accepted and ignored. Change these options instead.
connect logs the gains it programs.
| PARAMETER | DESCRIPTION |
|---|---|
protocol
|
Bus to open. RS485 also needs
TYPE:
|
serial_port
|
RS485 device path.
TYPE:
|
baud_rate
|
RS485 baud rate.
TYPE:
|
hand_id
|
Device ID of the hand to drive.
TYPE:
|
dofs_kp
|
Firmware PD(I) gains programmed on every finger, in firmware units.
TYPE:
|
dofs_ki
|
Firmware PD(I) gains programmed on every finger, in firmware units.
TYPE:
|
dofs_kd
|
Firmware PD(I) gains programmed on every finger, in firmware units.
TYPE:
|
tor_max
|
Firmware torque limit on every finger, in firmware units.
TYPE:
|
finger_mode
|
Firmware finger mode:
TYPE:
|
crc_retries
|
Attempts per
TYPE:
|
Source code in eden/extensions/deployment/robotera_xhand.py
Attributes¶
Methods:¶
connect
¶
Source code in eden/extensions/deployment/robotera_xhand.py
close
¶
read_state
¶
Source code in eden/extensions/deployment/robotera_xhand.py
send_payload
¶
Source code in eden/extensions/deployment/robotera_xhand.py
reset_sensor
¶
reset_sensor(sensor_id: int) -> None
Reset a fingertip sensor by its ID.
Source code in eden/extensions/deployment/robotera_xhand.py
set_hand_id
¶
set_hand_id(new_id: int) -> None
Change the hand ID on the device.
Source code in eden/extensions/deployment/robotera_xhand.py
Functions:¶
Wuji Hand¶
Wuji Hand 1 / Hand 2 real-robot deployment backend.
Both generations are 20-DOF hands (5 fingers x 4 joints) whose SDKs speak joint angles in radians,
finger-major (thumb first), in the MJCF joint directions -- no sign or offset conversion. The SDK
order is the same for a right and a left hand; the side is a property of the joint names, which
:data:WUJI_JOINT_IDX maps for both sides and both naming schemes in Eden:
{right,left}_finger{1..5}_joint{1..4}(:class:~eden.options.robots.wuji_hand1.WujiHand1_Rand thewuji-mjlabtask models), and{r,l}_{thumb_cmc_flex, ..., pinky_dip}(:class:~eden.options.robots.wuji_hand2beta2.WujiHand2Beta2_R), whose declaration order is the same finger-major order (upstreamWUJI_HAND2_TASK_NAMES).
A :class:HandTransport is the wire to one hand: :class:Hand1Transport (wujihandpy) or
:class:Hand2Transport (wuji_sdk). It streams targets the way upstream wuji-mjlab does:
"sync"(upstream reorient): :meth:HandTransport.commandblocks for one control period, writingservo_hzlinearly interpolated sub-targets from the previous command to the new one."thread"(upstream pen spin): :meth:HandTransport.commandstarts a new ramp and returns; a background thread streams it atservo_hzand holds the target once the ramp ends.
:class:WujiHandDeployment drives one hand through a transport it builds at :meth:connect, or
one handed to it with :meth:WujiHandDeployment.use_transport (a simulator, for sim-to-sim).
Attributes¶
Classes¶
DeployStop
¶
Bases: RuntimeError
A safety guard ended the run (hardware fault, stale feedback, lost tracking); the message says which.
StreamOptions
dataclass
¶
StreamOptions(mode: Literal['sync', 'thread'] = 'sync', servo_hz: float = 200.0, interp_s: float = 0.02)
HandTransport
¶
HandTransport(stream: StreamOptions, joint_names: Sequence[str] | None = None)
The contract every Wuji transport keeps (upstream HandDriverBase): 20 joint angles, SDK order.
| PARAMETER | DESCRIPTION |
|---|---|
stream
|
How :meth:
TYPE:
|
joint_names
|
The 20 joints' names in SDK order, for error messages (:attr: |
Source code in eden/extensions/deployment/wuji_hand.py
Attributes¶
joint_names
instance-attribute
¶
joint_names = _SDK_JOINTS if joint_names is None else tuple(joint_names)
Methods:¶
connect
¶
write
¶
read
¶
clock
¶
clock() -> float
The time joint readings are stamped with (wall clock; a simulation's own time in sim-to-sim).
disconnect
¶
command
¶
Stream target from the previous command, as upstream does for this task.
Source code in eden/extensions/deployment/wuji_hand.py
hold
¶
Let one control period pass with no new target, holding the last one.
"sync" re-streams the last command across the period through :meth:_ramp, so a tick
that sends nothing still lasts one control period (on a simulator's clock too); before any
command there is nothing to hold, and it only waits. "thread" returns at once: the servo
thread is already holding, and the base rate limiter paces the loop.
Source code in eden/extensions/deployment/wuji_hand.py
ease_to
¶
ease_to(goal: ndarray, duration_s: float, rate_hz: float, profile: str, guard: Callable[[], None] | None = None) -> None
Move from the measured pose to goal: upstream's home (cosine, 100 Hz) or start-pose smoothstep.
guard runs before every waypoint, so a safety check that raises stops a multi-second
move where it is, holding the last waypoint, instead of only before it starts.
Source code in eden/extensions/deployment/wuji_hand.py
stop_servo
¶
Source code in eden/extensions/deployment/wuji_hand.py
close
¶
Hand1Transport
¶
Hand1Transport(stream: StreamOptions, effort_limit_a: float = 0.5, lowpass_cutoff_hz: float = 3.0, joint_names: Sequence[str] | None = None, stale_after_s: float = 0.5)
Bases: HandTransport
Wuji Hand 1 via wujihandpy's realtime position controller (upstream WujiHand1Driver).
The hand runs its own position loop behind a low-pass; command gains do not apply. Every
:meth:read is a checked bus round-trip (read_joint_actual_position), so the feedback cannot
freeze silently the way a subscribed feed can: a read that gets no answer within
stale_after_s raises :class:DeployStop, and :meth:stale reports it from then on.
| PARAMETER | DESCRIPTION |
|---|---|
stream
|
How targets are streamed.
TYPE:
|
effort_limit_a
|
Joint current limit, A.
TYPE:
|
lowpass_cutoff_hz
|
Cutoff of the controller's target low-pass.
TYPE:
|
joint_names
|
See :class: |
stale_after_s
|
Timeout of each joint-position read.
TYPE:
|
Source code in eden/extensions/deployment/wuji_hand.py
Attributes¶
Methods:¶
connect
¶
Source code in eden/extensions/deployment/wuji_hand.py
write
¶
read
¶
Source code in eden/extensions/deployment/wuji_hand.py
disconnect
¶
Source code in eden/extensions/deployment/wuji_hand.py
Hand2Gains
dataclass
¶
MIT impedance and current limit programmed on a Wuji Hand 2 at connect.
| PARAMETER | DESCRIPTION |
|---|---|
kp
|
Per-joint stiffness and damping, SDK (finger-major) order. |
kd
|
Per-joint stiffness and damping, SDK (finger-major) order. |
effort_limit_a
|
Per-joint current limit, A. |
Hand2Transport
¶
Hand2Transport(stream: StreamOptions, gains: Hand2Gains, serial_number: str = '', require_all_joints: bool = True, feedback_timeout_s: float = 2.0, stale_after_s: float = 0.5, joint_names: Sequence[str] | None = None)
Bases: HandTransport
Wuji Hand 2 via wuji_sdk (upstream WujiHand2Driver and pen spin's Hand2Compat).
Connects to serial_number (or the single hand on the bus), programs the MIT gains and the
current limit, enables the motors, and streams JointCommand(q, 0, 0). Joint positions
arrive on the SDK's joint_states callback; joint_diagnostics feeds :meth:fault, and
the age of the joint feed feeds :meth:stale.
| PARAMETER | DESCRIPTION |
|---|---|
stream
|
How targets are streamed.
TYPE:
|
gains
|
Programmed once at connect.
TYPE:
|
serial_number
|
The hand; empty connects the only hand on the bus.
TYPE:
|
require_all_joints
|
Refuse a hand with fewer than 20 joints online (upstream reorient). Otherwise (pen spin) the
joints that report during :meth:
TYPE:
|
feedback_timeout_s
|
How long :meth:
TYPE:
|
stale_after_s
|
A watched joint (or the whole feed) older than this is stale.
TYPE:
|
joint_names
|
See :class: |
Source code in eden/extensions/deployment/wuji_hand.py
Attributes¶
Methods:¶
connect
¶
Source code in eden/extensions/deployment/wuji_hand.py
fault
¶
fault() -> str | None
Source code in eden/extensions/deployment/wuji_hand.py
stale
¶
stale() -> str | None
Source code in eden/extensions/deployment/wuji_hand.py
write
¶
read
¶
disconnect
¶
Source code in eden/extensions/deployment/wuji_hand.py
WujiHandDeployment
¶
WujiHandDeployment(env: EnvBase, options: DeploymentOptions)
Bases: DeploymentBase
A Wuji Hand 1 or Hand 2, right or left, on a fixed mount.
The side comes from the robot's joint names (see :data:WUJI_JOINT_IDX); the deployment DOF
order may be any permutation of the 20 joints, and is scattered into the SDK's order on the way
out and gathered back on the way in. The hand has no IMU: :meth:read_state reports the mount
orientation (the robot's default_root_quat) as the base's.
Joint velocity is a finite difference of consecutive reads over the transport's clock, like
the XHand's (neither SDK reports one), optionally low-passed (vel_lowpass_hz).
Gains are programmed once at connect, never per command. Hand 2 takes MIT kp / kd
(mit_kp / mit_kd, default the payload's nominal gains: the robot's default_dofs_kp /
default_dofs_kd times deploy_kp_kd_scale). Hand 1 runs its own position loop and has
no gain to set. So disable_joints (which works by zeroing command gains) is refused for
both, and deploy_kp_kd_scale / mit_kp / mit_kd for Hand 1.
Loop order. The base control tick (:meth:DeploymentBase.step, the operator state machine
every backend runs) with this backend's hooks: :meth:check_safety first, then the command is
sent and the period waited out, then the env's episode clock advances (episode_length_buf:
the scene is never stepped here, and action warm-ups and clip clocks read it), then the hand is
read and :meth:sync_scene -- the hook a subclass uses to write anything else it observes (a
camera-tracked object) into the scene -- runs, and only then do the commands publish
(sync_before_commands), so a command that grades the new state sees it this tick. In
stream_mode="sync" sending the command is the wait (it streams sub-targets across one
control period, as upstream does), so the base's rate limiter is not used on top of it: a tick
takes one control period, and the inference time is absorbed into it (each period's deadline
is chained from the previous one, as upstream anchors its loop before inference). A tick with
nothing to send (action=None before a slot has written targets) holds the last target for
the period instead (:meth:HandTransport.hold), so it still lasts one period and the episode
clock and the reference cannot race ahead at the speed of the loop.
Boot, INIT and START. :meth:reset restarts the episode clock and the velocity estimate and
boots in STOP, holding the measured pose. The operator's INIT ramps from the pose measured at
the press to init_target over init_steps ticks and holds it; START hands the hand to the
policy. :meth:check_safety runs before every one of those ticks, and :meth:ease_to runs
:meth:check_hand before every waypoint.
| PARAMETER | DESCRIPTION |
|---|---|
hand
|
Hardware generation.
TYPE:
|
serial_number
|
Hand 2 to connect; empty connects the only hand on the bus.
TYPE:
|
stream_mode
|
How targets are streamed (see the module docstring).
TYPE:
|
servo_hz
|
Rate of the interpolated sub-targets.
TYPE:
|
effort_limit_a
|
Joint current limit, A.
TYPE:
|
lowpass_cutoff_hz
|
Hand 1's target low-pass cutoff.
TYPE:
|
mit_kp
|
Hand 2's MIT gains: one value for every joint, or a pattern-keyed dict covering all 20.
|
mit_kd
|
Hand 2's MIT gains: one value for every joint, or a pattern-keyed dict covering all 20.
|
require_all_joints
|
Hand 2: refuse a hand with fewer than 20 joints online.
TYPE:
|
stop_on_fault
|
:meth:
TYPE:
|
stale_after_s
|
Hand 2: joint feedback older than this is stale.
TYPE:
|
vel_lowpass_hz
|
First-order low-pass on the finite-difference joint velocity;
TYPE:
|
clip_targets_to_joint_limits
|
Clamp every sent target (and every :meth:
TYPE:
|
auto_yaw_align
|
Off: the hand has no IMU, so its reported base orientation is the fixed mount's and there is no yaw to align.
TYPE:
|
Source code in eden/extensions/deployment/wuji_hand.py
Attributes¶
clip_targets_to_joint_limits
class-attribute
instance-attribute
¶
clip_targets_to_joint_limits: bool = False
Methods:¶
programmed_gains
¶
programmed_gains() -> tuple[ndarray, ndarray] | None
Hand 2's MIT gains (mit_kp / mit_kd, else the nominal ones), in a policy's units; Hand 1 has none.
Source code in eden/extensions/deployment/wuji_hand.py
to_sdk
¶
Scatter a deployment-order vector into the SDK's finger-major order.
Source code in eden/extensions/deployment/wuji_hand.py
from_sdk
¶
Gather an SDK-order vector into the deployment DOF order.
use_transport
¶
use_transport(transport: HandTransport) -> None
Drive transport (e.g. a simulator for sim-to-sim) instead of the hardware one :meth:connect builds.
Source code in eden/extensions/deployment/wuji_hand.py
connect
¶
Source code in eden/extensions/deployment/wuji_hand.py
close
¶
Source code in eden/extensions/deployment/wuji_hand.py
read_state
¶
Source code in eden/extensions/deployment/wuji_hand.py
reset_velocity_filter
¶
Start the finite-difference velocity afresh from the next read (upstream does, entering a new phase).
send_payload
¶
Source code in eden/extensions/deployment/wuji_hand.py
check_hand
¶
Raise :class:DeployStop on stale joint feedback, or a latched hand fault (stop_on_fault).
Source code in eden/extensions/deployment/wuji_hand.py
check_safety
¶
Run before every policy tick: :meth:check_hand. Subclasses extend it with their own guards.
sync_scene
¶
observe
¶
Read the hand and return the observation, advancing nothing.
ease_to
¶
Move the hand from its measured pose to target (deployment DOF order), "cosine" or "smoothstep".
:meth:check_hand runs before every waypoint. The eased path is not low-passed (it is smooth
already), but target_lpf restarts from target, so the next command filters from where
the hand was eased to rather than pulling it back toward the target before the ease.
Source code in eden/extensions/deployment/wuji_hand.py
Functions:¶
sdk_index_map
¶
Each of names' index in the SDK's joint array, raising (as owner) unless they cover the 20 once each.
Source code in eden/extensions/deployment/wuji_hand.py
ease
¶
Progress along an :data:EASE_PROFILES curve at x in [0, 1]; 0 and 1 at the ends, zero slope at both.
Source code in eden/extensions/deployment/wuji_hand.py
Composite rigs¶
Deploy a composite rig -- an arm with a hand welded on, and kin -- as one robot.
A composite (:class:~eden.options.entities.CompositeEntityOptions, e.g.
:class:~eden.options.robots.FrankaXHand) sits in the task's scene under one name and expands into
several entities when the env builds -- the Franka arm as robot, the XHand as hand. Each
part is its own piece of hardware with its own SDK, so :class:CompositeDeployment drives each
through the backend that part's robot is bound to (:func:resolve_deployment_for) and presents the
set as one robot to everything above it: the action manager runs once per tick and its targets are
split across the parts, the parts' readings are joined into one :class:RobotState, and one
observation is computed from all of them.
The parts' DOFs are laid end to end parent first -- a welded part always after the part it is welded
to, otherwise in the rig's declaration order -- so dofs_name -- the layout of every command, of
--disable_joints and of --debug_record -- is the arm's joints followed by the hand's.
The rig runs the one operator state machine every backend runs (boot in STOP, INIT, START, STOP, KILL), once, for all of its parts: one START hands the whole rig to the policy.
Each part's wire -- connect, read_state, send_payload, _idle_tick, close --
runs on that part's own thread, every part at once, so a tick's I/O takes as long as its slowest
part rather than the sum of them all. The policy tick stays one: the loop waits for every part's
reading before it observes, and for every part's send before it moves on.
A part runs as it does deployed alone: its backend's own option defaults, its own wire, its own
hooks. The composite adds the joining and changes nothing on a part. The one thing it cannot keep
per part is a backend's loop setting that acts on the env every part shares
(sync_before_commands, advances_episode_clock): the rig follows one only when every part
does, and otherwise runs the standard loop and says which parts it does not follow.
Attributes¶
FORWARDED_OPTIONS
module-attribute
¶
PART_HOOKS
module-attribute
¶
PART_HOOKS: frozenset[str] = frozenset({'_resolve_robot_options', '_resolve_control_dt', '_resolve_dofs', '_resolve_disabled_joints', '_resolve_options', '_resolve_feet_links', '_validate_init_target', '_validate_on_motion_end', '_build_term_action_maps', '_swap_in_state_entity', '_make_lin_vel_estimator', '_undo_swaps', 'connect', 'close', 'read_state', 'send_payload', '_before_step', '_idle_tick', '_prepare_reset', '_sync_state', '_update_lin_vel_estimate', '_remote_inputs', '_print_control_guide', '_motion_start_dofs', 'programmed_gains', 'command_gain_dofs', 'blocks_for_the_period', 'provides_base_lin_vel', '_publishes_base_lin_vel'})
Classes¶
CompositeDeployment
¶
CompositeDeployment(env: RLEnvBase, options: DeploymentOptions)
Bases: DeploymentBase
Drive every part of a composite rig through its own backend, as one robot.
eden deploy picks this backend for any composite (:func:resolve_deployment_for), so a task
whose scene holds robot=FrankaXHand(...) deploys onto a Franka and an XHand with no extra
flags, assuming both are connected. Each part with joints gets the backend its robot is bound to
(FrankaPandaDeployment, RoboTeraXHandDeployment), constructed against its own scene
entity; a part with none (a mount) is not deployed.
What the composite owns, and the parts do not:
- The loop and the state machine. One action-manager pass and one observation per tick,
and one INIT / START / STOP / KILL state machine for the whole rig: it boots in STOP holding
every part's measured pose, INIT ramps every joint, and START hands the whole rig to the
policy. The operator's inputs are every part's remote (the Oli's joystick, the G1's) plus
one keyboard. :meth:
connect, :meth:read_state, :meth:send_payload,_idle_tickand :meth:closerun on every part at once, each on the part's own thread, and the loop waits for all of them: a tick's I/O takes as long as its slowest part. The scene sync and the other per-tick hooks (_before_step-- a Wuji hand's safety check --_prepare_reset) run on the loop's thread, part by part, every welded part after its parent (whatever orderentity_partslists them in), so a welded part's FK reads its parent's fresh pose. - The pace. The parts are built with
sync=Falseand the composite's rate limiter paces the loop -- unless a part's send itself waits out the control period (a Wuji hand's defaultstream_mode="sync", :attr:~DeploymentBase.blocks_for_the_period). Then that part paces the rig, as it paces itself deployed alone, and the rig runs no rate limiter on top; the parts' sends overlap, so two such hands still take one period, not two. - The loop settings. A backend's
sync_before_commands/advances_episode_clockact on the env every part shares, so they cannot be kept per part: the rig follows one only when every part sets it, and otherwise runs the standard loop -- the order and clock a part without it runs deployed alone -- and warns which parts it does not follow. No part takes on another part's loop. - The gains. Commands carry the concatenation of the parts' robot gains, scaled by
deploy_kp_kd_scaleand zeroed ondisable_joints. A part whose robot ignores each command's gains (the Franka's controller, the XHand's and Wuji's firmware) cannot go limp that way, so a disabled joint there is refused. Each part takes the rig's scale unless itsparts_optionssets its own, and its backend also applies it its own way (a Wuji Hand 2 programs its MIT gains from it) or refuses it (an XHand, anything but 1.0). - The base reading. Base orientation, angular velocity and acceleration come from the rig's
root part (its primary entity), as does the velocity estimator (
estimate_lin_vel), on the rig's feet (a :class:~eden.options.robots.composite.base.HumanoidOptions'feet_links). A welded part's own base pose is read off FK through the weld, not off its (IMU-less) reading. - Commands, recording, the motion clock -- everything else :class:
DeploymentBasedoes.
Every other option of a part's backend keeps its own default, as when the part is deployed
alone. Each part also runs the composite's :data:FORWARDED_OPTIONS (no backend changes their
defaults); parts_options overrides those, and sets each backend's own options, per part::
CompositeDeployment.configure(parts_options={
"robot": {"robot_ip": "172.16.0.2"},
"hand": {"protocol": "EtherCAT"},
})
(eden deploy --part_option robot.robot_ip=172.16.0.2 --part_option hand.protocol=EtherCAT.)
Not supported yet, and refused at construction: a part whose backend overrides a method the
composite never calls on a part (step, reset, the hold payload -- anything outside
:data:PART_HOOKS), two parts with a joint of the same name, and --mujoco sim-to-sim of a
composite.
| PARAMETER | DESCRIPTION |
|---|---|
parts_options
|
Backend options per part, keyed by the part's scene entity name. A key may be any option of that part's backend except the composite-owned base options. |
Source code in eden/extensions/deployment/composite.py
Attributes¶
Methods:¶
part_backends
classmethod
¶
part_backends(rig: CompositeEntityOptions) -> dict[str, type[DeploymentBase]]
The backend each jointed part of rig deploys through, parent first.
Raises as construction would for a part that has none or cannot run inside a rig. eden
deploy asks before constructing, to hand a backend-specific flag (--publish_rate) to
the parts that declare it.
Source code in eden/extensions/deployment/composite.py
connect
¶
Connect every part at once. If one fails, the ones that came up are closed again, then it raises.
Source code in eden/extensions/deployment/composite.py
close
¶
Close every part at once, each even if another raises; then raise the first failure.
Source code in eden/extensions/deployment/composite.py
read_state
¶
Read every part at once and join the readings, with the base fields from the base part.
Each part keeps its own reading as its state too, which its backend reads back (the
Franka clamps its next command against it). A part's extra entries are prefixed with
the part's name, so --debug_record keeps them apart.
Source code in eden/extensions/deployment/composite.py
send_payload
¶
Send every part its slice of payload at once; payload.dofs_pos then holds what was sent (filtered).
Each part sends it as the rig does: a hold of the rig is a hold of every part
(:attr:~DeploymentBase.sending_hold), so a Franka part keeps its gripper where the policy left it.
Source code in eden/extensions/deployment/composite.py
command_gain_dofs
¶
Each part's own answer on its slice: a body that takes each command's gains next to hands that do not.
programmed_gains
¶
programmed_gains() -> tuple[ndarray, ndarray] | None
The programmed gains of every part that runs its own, end to end; None if one has none in our units.
A part that takes each command's gains runs no fixed set, so its slice holds what it is
commanded now; :meth:command_gain_dofs tells the two apart.
Source code in eden/extensions/deployment/composite.py
Functions:¶
MuJoCo sim-to-sim¶
MuJoCo sim-to-sim deployment backend.
A :class:~eden.extensions.deployment.base.DeploymentBase whose "robot" is a MuJoCo
simulation instead of hardware. It runs the exact deploy control loop
(eden.utils.cli.tasks._deploy_control_loop) — policy loading, the RobotStateEntity
swap, observation assembly, command terms — but reads state from and writes torques to a
MuJoCo model rather than a Unitree DDS wire. That makes it a faithful sim-to-sim harness
for validating a Genesis-trained policy against an independent physics engine before it
ever touches a real robot.
Inspired by Unitree's unitree_mujoco bridge (github.com/unitreerobotics/unitree_mujoco),
which stands up a MuJoCo world speaking the Unitree LowState/LowCmd DDS protocol.
This backend takes the simpler, robot-agnostic route: it drives MuJoCo directly (no DDS,
no unitree_sdk2py), so it works for any robot Eden can describe — the two Unitree
families here (G1, Go2) and anything else with a RobotOptions and a loadable
MJCF/URDF. The engine details live in
:class:~eden.extensions.deployment.utils.mujoco_sim.MujocoSim; this class is the glue
between it and the Eden env.
Selected at deploy time with eden deploy ... --mujoco (see deploy_task), which
overrides the usual robot→backend binding so any task/robot can be routed through MuJoCo.
Physics can run in lockstep with the control loop (the default: each command advances
the simulation exactly one control period, so a rollout is deterministic and a late policy
costs nothing) or asynchronously (--async / async_physics=True: the simulation
advances on its own thread in wall-clock time, like a robot's controller, so a late policy
is late for real). See :class:~eden.extensions.deployment.utils.realtime_physics.RealtimePhysics.
Attributes¶
Classes¶
MujocoDeployment
¶
MujocoDeployment(env: EnvBase, options: DeploymentOptions)
Bases: DeploymentBase
Deploy a policy against a MuJoCo simulation of the robot (sim-to-sim).
Configurable options (in addition to :class:DeploymentBase's):
mjcf_path: str | None
Explicit model file to load into MuJoCo, bypassing mjcf_source resolution.
Point this at any hand-authored MuJoCo scene for one-off sim-to-sim runs.
mjcf_source: str
Which model to load when mjcf_path is unset (default "eden"):
- ``"eden"``: the robot's own Genesis training asset (``robot_options.file``).
Always available; the MuJoCo backend reconstructs armature/floor for it.
- ``"menagerie"``: a community-tuned `MuJoCo Menagerie
<https://github.com/google-deepmind/mujoco_menagerie>`_ model (with the
armature/damping/contact params MuJoCo needs), fetched from an HF dataset — the
higher-fidelity, more independent sim-to-sim target. Errors if the robot has no
Menagerie mapping or the download fails. Selected by ``eden deploy --menagerie``.
- ``"auto"``: Menagerie when the robot is mapped and the download succeeds,
otherwise fall back to the Eden asset. Not reachable from the CLI, which keeps
the loaded model deterministic.
menagerie_registry: str
HF org for the Menagerie dataset (default "Kashu7100").
menagerie_dataset: str
HF dataset name mirroring MuJoCo Menagerie (default "mujoco_menagerie").
sim_dt: float | None
MuJoCo integration timestep. None keeps the asset's timestep, or in lockstep, when it
does not divide the control period, takes the largest step under it that does. In
lockstep each step advances control_dt / sim_dt substeps, so a given sim_dt must
divide the control period: one that does not is refused rather than rounded, which would
run the physics on a clock ahead of or behind the policy's.
integrator: str
MuJoCo integrator ("implicitfast" default; also "implicit", "euler",
"rk4"). implicitfast keeps a stiff joint PD stable.
add_floor: bool
Add a ground plane if the model has none (default True). It stands where the scene's ground
plane does, raised or tilted alike, with its friction
(:func:~eden.extensions.deployment.utils.mujoco_scene.read_floor), but keeps MuJoCo's
default solver parameters: the robot's own geoms keep its asset's, and a contact's mix of a
Genesis value and a MuJoCo one would be neither engine's.
provide_ground_truth_base: bool
Feed MuJoCo's exact base position and linear velocity to the observation pipeline
(default True). Unlike hardware — where these must be estimated — the simulator
knows them, so policies that observe base position / linear velocity work without
--estimate_lin_vel. Set False to emulate a hardware-blind base.
Combining it with ``estimate_lin_vel`` is the useful sim-to-sim rehearsal of the
estimator, not a conflict: the estimate is installed *after* the ground-truth
reading, so it is what the policy observes, while the truth stays alongside it in
``state.extra["base_lin_vel"]`` for the estimate
(``state.extra["base_lin_vel_estimate"]``) to be scored against.
imu_acc_noise, imu_gyro_noise: float
Standard deviation of white noise added to every accelerometer (m/s^2) and gyro (rad/s)
reading, per axis. Default 0: MuJoCo's IMU is otherwise exact, which hides how an
IMU-driven estimate behaves on hardware. Under estimate_lin_vel an exact accelerometer
barely drifts even with no foot contact, so a contact dropout that sends the real robot's
estimate to -0.5 m/s stays at 0.03 m/s in sim. Turn these on to rehearse the
estimator against a hardware-like IMU.
imu_acc_bias, imu_gyro_bias: tuple[float, float, float]
Constant bias added to the accelerometer (m/s^2) and gyro (rad/s) readings, body frame.
Default zero.
imu_noise_seed: int | None
Seed for the IMU noise generator, so a noisy run can be repeated. None (default)
draws a fresh seed.
show_viewer: bool
Open MuJoCo's native passive viewer to watch the sim-to-sim rollout.
record_video: str | None
Write the rollout from the policy's first START on to this mp4, rendered offscreen from MuJoCo
(:class:~eden.extensions.deployment.utils.mujoco_video.MujocoVideo: two views, one frame per
control tick, a STOP's holds included). Lockstep only. Headless, it needs MUJOCO_GL=egl.
gantry: bool
Hang the robot from a simulated overhead hoist -- the sim counterpart of the bring-up
rig, so a biped can be held upright while a controller is commissioned instead of
falling every time control pauses. See
:meth:~eden.extensions.deployment.utils.mujoco_sim.MujocoSim.set_gantry for the rope
model; it is a tension-only spring-damper to a fixed anchor, attached above the centre
of mass so it produces a real restoring torque rather than just cancelling weight.
Re-rigged on every :meth:`reset` from wherever the robot then stands, so the anchor
follows a re-placed robot instead of yanking it back to the first episode's spot.
gantry_body: tuple[str, ...]
Bodies the ropes are tied to, one rope each. Empty (default) uses the robot's own
:attr:~eden.options.entities.RobotOptions.gantry_links_name — the two shoulders for a
humanoid that declares them — and falls back to the floating base for a robot that
declares none.
gantry_attach: tuple[float, float, float] | None
Attachment point within each rope's body frame, metres. None (default) picks it from
how the robot is rigged: the link origin for declared mount links, which for a shoulder
sits on the pitch axis at the top of the torso, or 25 cm up for a single base rope,
which needs the height to have any moment arm at all.
gantry_height: float | None
World height of the anchor. Defaults to 1 m above the attachment point.
gantry_step: float
Metres the operator's + / - keys move the anchor per press.
gantry_draw: bool
Draw each rope and the force it is pulling with in the --vis viewer (default on).
Seeing the pull is most of the value of a simulated rig: a rope that is quietly carrying
the robot looks exactly like a robot that is standing, right up until you unrig it.
gantry_force_scale: float
Metres of force arrow per newton (default 1e-3, so 1 kN draws as 1 m).
gantry_stiffness, gantry_damping:
Rope spring-damper, N/m and N.s/m.
gantry_slack: float
Metres of slack. 0 (default) is taut and holds station; a positive value is a
safety tether that does nothing until the robot drops that far.
gantry_support: float
Scale on the rope force, 0-1. Ramp down to hand weight back to the controller.
init_steps: int
Control cycles the INIT ramp takes to reach the start pose — the sim-to-sim
counterpart of the hardware backends' INIT state, inherited from
:class:DeploymentBase along with its default of 250 (5 s at 50 Hz). The ramp runs
when the operator presses INIT, or on its own in a headless run. The policy does
not drive straight from the reset pose: this backend runs the same INIT/START
sequence the robot does.
init_target: str
Inherited from :class:DeploymentBase; this backend is the one that also accepts
"motion_rsi", which places joints and base at the tracked reference's frame 0
on reset. "motion" and "default" are the same joint-space ramps here as on
the wire.
async_physics: bool
Run the simulation on its own thread in wall-clock time (default False). Off, physics
advances control_dt / sim_dt substeps inside each send_payload and then
waits for the next command, so the control loop's timing has no effect on the rollout
-- deterministic, and the right mode for comparing two runs. On, the simulation is
the robot: it steps at 1 / sim_dt whether or not a new command has arrived,
send_payload only replaces the command it holds, and read_state returns the
latest state it has published. A policy that overruns the control period therefore
leaves the robot executing its previous command for the overrun, and observes a
correspondingly older state -- the behaviour hardware has and lockstep cannot show.
Selected by eden deploy --mujoco --async.
The simulated robot has no command latency. A task's
:class:~eden.managers.modifiers.actions.actuators.ActionDelay models the Genesis actuator's
latency, and no deployment backend applies it: each command acts from the substep it
arrives on. A policy trained against a delay therefore runs here with less latency than it trained
with. This matters most for a fixed delay (both ends of the range equal), which the policy expects
on every step. --async adds only the control loop's real lateness, not the trained delay.
eden inference --async is the run that keeps the delay.
Being robot-agnostic, this backend knows nothing robot-specific — including the feet the
estimate_lin_vel filter needs. It does not have to: the robot declares them itself
(RobotOptions.feet_links_name), so eden deploy --mujoco --estimate_lin_vel needs no
further flag for any robot that names its feet.
Source code in eden/extensions/deployment/mujoco.py
Attributes¶
menagerie_registry
class-attribute
instance-attribute
¶
menagerie_registry: str = MENAGERIE_REGISTRY
provide_ground_truth_base
class-attribute
instance-attribute
¶
provide_ground_truth_base: bool = True
gantry_attach
class-attribute
instance-attribute
¶
imu_acc_bias
class-attribute
instance-attribute
¶
imu_gyro_bias
class-attribute
instance-attribute
¶
operator_guide_title
class-attribute
¶
operator_guide_title: str = 'MUJOCO SIM-TO-SIM DEPLOY CONTROLS (keyboard — same sequence as the robot)'
Methods:¶
connect
¶
Source code in eden/extensions/deployment/mujoco.py
close
¶
Source code in eden/extensions/deployment/mujoco.py
rig_gantry
¶
rig_gantry(enabled: bool) -> None
Rig or release the gantry mid-run, anchored over where the robot stands now.
The switch a caller driving its own loop needs to hand the robot's weight back to the
controller (and to catch it again); gantry itself only sets how a reset rigs it.
Source code in eden/extensions/deployment/mujoco.py
read_state
¶
Source code in eden/extensions/deployment/mujoco.py
send_payload
¶
Source code in eden/extensions/deployment/mujoco.py
Functions:¶
MuJoCo sim-to-sim of a composite rig, through the composite deployment itself.
eden deploy --mujoco on a scene whose --entity_name is a composite rig (KukaSharpa,
FrankaXHand, OliWuji) runs :class:MujocoCompositeDeployment: the very
:class:~eden.extensions.deployment.composite.CompositeDeployment that drives the rig's hardware --
one backend per part, each on its own thread, one state machine, the parts' readings joined into one
observation -- with every part on a :class:MujocoPartDeployment in place of its hardware backend.
The parts share one MuJoCo model of the whole rig (:class:~eden.extensions.deployment.utils.mujoco_rig.MujocoRig)
plus the scene's props (a table, a tool), so a manipulation policy acts on a simulated object and
observes it.
The MuJoCo model is assembled from the built Genesis env, so it is the rig the policy trained on:
- each part's own asset, welded where the env welds it -- the mount read off the env's forward kinematics, not re-derived -- and checked link by link against that kinematics at connect;
- each part's armature and joint damping as built, its material friction (
default_geoms_frictionper link), its links' contact solver parameters as built (aset_contact_sol_paramsstartup event included) and gravity compensation; - each prop at the pose the env spawned it, with its friction and contact parameters.
Each tick every part writes its command to the model on its own thread, then the composite steps the
physics once for the control period (lockstep, as MujocoDeployment), reads the parts back on
their threads, and writes each prop's pose into its env entity -- what a mocap system does on
hardware -- before the observation is computed. Where hardware has a camera instead (an AprilTag
pose estimate), prop_pos_noise / prop_rot_noise / prop_camera_hz / prop_latency_s
make what the policy reads of each free prop one: noisy, late and at the camera's rate (the exact
pose stays in the state as <prop>.true_pos / <prop>.true_quat for --debug_record).
record_video renders the rollout from MuJoCo, as a single robot's backend does, and the base's
policy_seconds ends a headless run. The env is read into the rig's description by
:mod:~eden.extensions.deployment.utils.mujoco_scene; this class keeps the lifecycle, the checks
and the camera.
Attributes¶
Classes¶
MujocoPartDeployment
¶
MujocoPartDeployment(env: RLEnvBase, options: DeploymentOptions)
Bases: DeploymentBase
One part of a composite rig on the rig's shared MuJoCo model -- the sim's stand-in for a part's hardware backend.
Built only by :class:MujocoCompositeDeployment, which binds it to the model at connect. Its
wire is the model: :meth:send_payload buffers the part's PD command (the composite steps the
physics once every part has sent), :meth:read_state reads the part's joints, and for the rig's
root its base -- the IMU, and the ground-truth pose and velocity of a free base when
provide_ground_truth_base, as MujocoDeployment gives a single robot.
Source code in eden/extensions/deployment/mujoco_composite.py
Attributes¶
provide_ground_truth_base
class-attribute
instance-attribute
¶
provide_ground_truth_base: bool = True
Methods:¶
bind
¶
connect
¶
close
¶
read_state
¶
Source code in eden/extensions/deployment/mujoco_composite.py
send_payload
¶
Source code in eden/extensions/deployment/mujoco_composite.py
MujocoCompositeDeployment
¶
MujocoCompositeDeployment(env: RLEnvBase, options: DeploymentOptions)
Bases: CompositeDeployment
A composite rig deployed onto MuJoCo: :class:CompositeDeployment with a :class:MujocoPartDeployment per part.
Options (in addition to :class:CompositeDeployment's):
sim_dt: float | None
MuJoCo timestep. None (default) takes the largest step of at most 2 ms that divides the
control period exactly, so each tick advances the physics by exactly one control period. A
given one must divide it too (eden deploy --mujoco_sim_dt): one that does not is refused.
integrator: str
"implicitfast" (default), "implicit", "euler" or "rk4".
cone: str
Friction cone, "elliptic" (default -- grasps hold far better on it) or "pyramidal".
props: tuple[str, ...] | None
Scene entities to simulate besides the rig (eden deploy --mujoco_props). None
(default) takes every colliding entity of the scene that is not the rig or the ground. Each
must be a box primitive or a rigid body loaded from a URDF / MJCF, without joints of its own:
only its pose reaches the env, which a mesh file's (MuJoCo cannot load one as a body) or an
articulated object's joints would not. Anything else is refused, naming it. A kinematic entity
(a goal marker: not simulated, no contacts) is never a prop; record_video draws it.
prop_pos_noise, prop_rot_noise: float
A camera's error on every free prop's pose the policy reads: per-axis white noise on the
position (m, standard deviation), and a rotation about a random axis by a normal angle (rad,
standard deviation). Default 0. With either on, the quaternion's sign is random too, as a
pose estimator's is. Only what the policy observes changes; MuJoCo's physics does not, and the
exact pose stays in the state as <prop>.true_pos / <prop>.true_quat.
prop_camera_hz: float | None
The camera's frame rate: a free prop's observed pose updates at it and holds between frames,
on the simulation's clock (a tick that sends nothing advances neither). None (default):
every control tick.
prop_latency_s: float
How old a frame is when it arrives, rounded half up to whole control ticks (with a warning
when that changes it). Default 0.
prop_noise_seed: int | None
Seed for the camera model's noise, so a run can be repeated. None draws a fresh one.
record_video: str | None
Write the rollout from the policy's first START on to this mp4, rendered offscreen from MuJoCo
(two views side by side, one frame per control tick, a STOP's holds included so the video
never skips). Headless, it needs MUJOCO_GL=egl.
kinematics_tolerance: float
Largest link-pose disagreement (metres plus radians) between the MuJoCo model and the env's
forward kinematics accepted at connect. Above it the assembly is wrong and connect raises.
show_viewer: bool
Open MuJoCo's passive viewer on the rig.
Source code in eden/extensions/deployment/mujoco_composite.py
Attributes¶
operator_guide_title
class-attribute
¶
operator_guide_title: str = 'MUJOCO RIG SIM-TO-SIM DEPLOY CONTROLS (keyboard — same sequence as the robot)'
Methods:¶
connect
¶
Source code in eden/extensions/deployment/mujoco_composite.py
close
¶
Source code in eden/extensions/deployment/mujoco_composite.py
send_payload
¶
Every part writes its command (each on its thread), then the physics advances one control period.
Source code in eden/extensions/deployment/mujoco_composite.py
read_state
¶
Source code in eden/extensions/deployment/mujoco_composite.py
The MuJoCo world itself (MujocoSim, MujocoRig, the MujocoFloor both add), what both MuJoCo backends read off
the built env (mujoco_scene.py), the offscreen recording (MujocoVideo, mujoco_video.py), the
Eden-robot-to-Menagerie map (menagerie_path_for), the
state and command dataclasses (RobotState, RobotCommand) and the loop helpers (RateLimiter,
StreamingRecorder, LinVelKalmanEstimator) live in eden/extensions/deployment/utils/, which has no
__init__.py, so they are not rendered here; read them in the source.