Skip to content

Action Terms

Action terms turn the policy's action vector into commands for an entity: joint-space PD and velocity controllers, task-space controllers (differential IK, absolute-pose IK, operational-space control), floating-base pose control, grippers and welds. The actions guide explains how to choose and configure them; the term catalog lists every parameter.

from eden import actions

# excerpted from eden/tasks/benchmark/reacher/config.py
actions.ImplicitPDController.configure(entity_name="robot", dofs_name="joint*", scale=0.05)

The action-term base class ActionTerm is on the managers page.

Joint-space controllers

ImplicitPDController lets Genesis integrate the PD law (position mode); ExplicitPDController computes the torque itself (control_dofs_force), so its damping is explicit under every integrator.

Joint-space PD and velocity action terms (implicit/explicit controllers).

Attributes

Classes

ImplicitPDController

ImplicitPDController(env: EnvBase, options: ActionTermOptions)

Bases: _JointPDControllerBase

Implicit joint PD controller using Genesis's built-in position control.

Uses control_dofs_pos which leverages Genesis's implicit integration scheme: the PD target is recomputed at every physics substep and an implicit damping term (kd * substep_dt) is added to the mass matrix for numerical stability. This matches the behavior of MuJoCo, making it suitable for sim2sim transfer.

Source code in eden/managers/terms/actions/joint_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._scale: float | torch.Tensor | None = None
    self._offset: torch.Tensor | None = None
    self._motion_ref_dofs_idx: torch.Tensor | None = None
    self._delta_reference: torch.Tensor | None = None
    # Per-env (num_envs, n_dofs) position offset from set_dofs_pos_offset; None until first set.
    self._dofs_pos_offset: torch.Tensor | None = None
    # ReferenceSource.ACCUMULATE state: the running target integrated across steps,
    # its default (init/reset) value, and the soft-limit clamp bounds. None otherwise.
    self._accumulated_target: torch.Tensor | None = None
    self._default_target: torch.Tensor | None = None
    self._target_lower: torch.Tensor | None = None
    self._target_upper: torch.Tensor | None = None
    # ``mimic_dofs`` state, set by ``_build_mimic``: the driver count (the action width), the drivers'
    # Genesis index (DELTA captures only those), and per follower its driver's column plus the coupling.
    self._n_action_dofs: int | None = None
    self._driver_genesis_dofs_idx: range | torch.Tensor | None = None
    self._mimic_cols: torch.Tensor | None = None
    self._mimic_mult: torch.Tensor | None = None
    self._mimic_offset: torch.Tensor | None = None
    self._mimic_follower_dofs_name: list[str] = []
    self._modifier: ActionModifier | None = None
    if self.modifier is not None:
        self._modifier = ACTION_MODIFIER_REGISTRY.get(self.modifier.name)(env=env, options=self.modifier)

Methods:

apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def apply_actions(self) -> None:
    target = self._applied_command()
    self.verify_command_shape(target)
    self.entity.control_dofs_pos(target, dofs_idx_local=self.genesis_dofs_idx)

ExplicitPDController

ExplicitPDController(env: EnvBase, options: ActionTermOptions)

Bases: _JointPDControllerBase

Explicit joint PD controller using manual torque computation.

Computes PD torques explicitly and applies them via control_dofs_force. This is more physically accurate as it matches real-world motor control where only torque commands are valid. Better suited for sim2real transfer and easier to extend with domain randomization (e.g., randomizing kp/kd, adding motor delays).

An optional modifier can be provided to compose post-processing steps (delay, backlash, DC saturation, friction, effort clipping, motor strength scaling) on top of the PD controller::

ExplicitPDController.configure(
    entity_name="robot",
    dofs_name=["joint1", "joint2"],
    modifier=Compose.configure(
        modifiers=[
            ActionDelay.configure(delay_range_steps=(1, 3)),
            FrictionModel.configure(static_friction=2.4, dynamic_friction=0.24),
            EffortClip.configure(driving_torque_limit=111.0, ...),
            MotorStrength.configure(motor_strength=0.8),
        ]
    ),
)
Note

Requires sim_substeps=1 so that PD torques are recomputed at every physics substep. Adjust decimation accordingly to maintain the desired control frequency.

The -kd * qdot term is explicit under every EnvOptions.integrator: Genesis' implicit passes only damp the DOFs it drives itself (:class:ImplicitPDController), and the entity's kd * substep_dt armature fold runs under "euler" only. A kd stable through ImplicitPDController at a given substep can therefore diverge here.

Source code in eden/managers/terms/actions/joint_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._scale: float | torch.Tensor | None = None
    self._offset: torch.Tensor | None = None
    self._motion_ref_dofs_idx: torch.Tensor | None = None
    self._delta_reference: torch.Tensor | None = None
    # Per-env (num_envs, n_dofs) position offset from set_dofs_pos_offset; None until first set.
    self._dofs_pos_offset: torch.Tensor | None = None
    # ReferenceSource.ACCUMULATE state: the running target integrated across steps,
    # its default (init/reset) value, and the soft-limit clamp bounds. None otherwise.
    self._accumulated_target: torch.Tensor | None = None
    self._default_target: torch.Tensor | None = None
    self._target_lower: torch.Tensor | None = None
    self._target_upper: torch.Tensor | None = None
    # ``mimic_dofs`` state, set by ``_build_mimic``: the driver count (the action width), the drivers'
    # Genesis index (DELTA captures only those), and per follower its driver's column plus the coupling.
    self._n_action_dofs: int | None = None
    self._driver_genesis_dofs_idx: range | torch.Tensor | None = None
    self._mimic_cols: torch.Tensor | None = None
    self._mimic_mult: torch.Tensor | None = None
    self._mimic_offset: torch.Tensor | None = None
    self._mimic_follower_dofs_name: list[str] = []
    self._modifier: ActionModifier | None = None
    if self.modifier is not None:
        self._modifier = ACTION_MODIFIER_REGISTRY.get(self.modifier.name)(env=env, options=self.modifier)

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def build(self) -> None:
    super().build()

    # The solver's flag: Genesis batches a heterogeneous entity's DOF info by itself in some scenes.
    self._batch_dofs_info = self.entity.has_batched_dofs_info

    # Cache PD gains, already shaped (num_envs, n_dofs) whatever batch_dofs_info says.
    self._kp = self.entity.get_dofs_kp(dofs_idx_local=self.genesis_dofs_idx)
    self._kd = self.entity.get_dofs_kd(dofs_idx_local=self.genesis_dofs_idx)
reset
reset(envs_idx: slice | Tensor | None = None) -> None
Source code in eden/managers/terms/actions/joint_actions.py
def reset(self, envs_idx: (slice | torch.Tensor) | None = None) -> None:
    super().reset(envs_idx)
    if envs_idx is None:
        envs_idx = slice(None)
    if self._batch_dofs_info:
        # Re-read the per-env gains so gain randomization is picked up. Skipped when DOF
        # info is unbatched purely to save the read: every env shares one row there, so
        # the write would be a no-op — the shapes line up either way.
        self._kp[envs_idx] = self.entity.get_dofs_kp(dofs_idx_local=self.genesis_dofs_idx, envs_idx=envs_idx)
        self._kd[envs_idx] = self.entity.get_dofs_kd(dofs_idx_local=self.genesis_dofs_idx, envs_idx=envs_idx)
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def apply_actions(self) -> None:
    target = self._applied_command()
    dofs_vel = self.entity.get_dofs_vel(dofs_idx_local=self.genesis_dofs_idx)
    pos_err = target - self.entity.get_dofs_pos(dofs_idx_local=self.genesis_dofs_idx)

    # PD control
    ctrl_torque = self._kp * pos_err - self._kd * dofs_vel

    # Apply modifiers to the computed torques (e.g., backlash, friction, effort clip)
    if self._modifier is not None:
        ctrl_torque = self._modifier.modify_ctrl_torque(ctrl_torque, dofs_vel, pos_err=pos_err)

    self.verify_command_shape(ctrl_torque)
    self.entity.control_dofs_force(ctrl_torque, dofs_idx_local=self.genesis_dofs_idx)

VelocityFeedforwardPDController

VelocityFeedforwardPDController(env: EnvBase, options: ActionTermOptions)

Bases: _JointPDControllerBase

Explicit joint PD controller with velocity feedforward.

Extends the explicit PD controller by accepting both target joint positions and target joint velocities as actions. The torque is computed as:

.. math:: \tau = k_p (q_{target} + q_{offset} - q_{current} + q_{motor_offset}) + k_d (-\dot{q}{current} + \dot{q})} \cdot r_{ff

where :math:r_{ff} is the velocity feedforward ratio. Post-processing (motor strength scaling, torque offset injection, effort clipping, etc.) is opt-in via the modifier parameter — see the composable modifiers in :mod:eden.managers.modifiers.actions.actuators.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

dofs_name

The names of the DOFs to control.

reference_source

Source of the reference offset added on top of the position action.

scale

The scale to apply to the position actions; None (the default) applies none.

offset

The offset to apply to the position actions.

velocity_scale

The scale to apply to the velocity actions.

feed_forward_ratio

The ratio for the velocity feedforward term.

Source code in eden/managers/terms/actions/joint_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._scale: float | torch.Tensor | None = None
    self._offset: torch.Tensor | None = None
    self._motion_ref_dofs_idx: torch.Tensor | None = None
    self._delta_reference: torch.Tensor | None = None
    # Per-env (num_envs, n_dofs) position offset from set_dofs_pos_offset; None until first set.
    self._dofs_pos_offset: torch.Tensor | None = None
    # ReferenceSource.ACCUMULATE state: the running target integrated across steps,
    # its default (init/reset) value, and the soft-limit clamp bounds. None otherwise.
    self._accumulated_target: torch.Tensor | None = None
    self._default_target: torch.Tensor | None = None
    self._target_lower: torch.Tensor | None = None
    self._target_upper: torch.Tensor | None = None
    # ``mimic_dofs`` state, set by ``_build_mimic``: the driver count (the action width), the drivers'
    # Genesis index (DELTA captures only those), and per follower its driver's column plus the coupling.
    self._n_action_dofs: int | None = None
    self._driver_genesis_dofs_idx: range | torch.Tensor | None = None
    self._mimic_cols: torch.Tensor | None = None
    self._mimic_mult: torch.Tensor | None = None
    self._mimic_offset: torch.Tensor | None = None
    self._mimic_follower_dofs_name: list[str] = []
    self._modifier: ActionModifier | None = None
    if self.modifier is not None:
        self._modifier = ACTION_MODIFIER_REGISTRY.get(self.modifier.name)(env=env, options=self.modifier)

Attributes

velocity_scale class-attribute instance-attribute
velocity_scale: float = 1.0
feed_forward_ratio class-attribute instance-attribute
feed_forward_ratio: float = 1.0
action_dim property
action_dim: int

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def build(self) -> None:
    if self.reference_source == ReferenceSource.ACCUMULATE:
        # ACCUMULATE is honored only by controllers that use the base ``compute``;
        # this one overrides it, so accumulation would silently never happen.
        raise ValueError(
            f"{term_subject(self)}: reference_source='accumulate' is not supported "
            f"(its overridden compute() does not integrate the action)."
        )
    super().build()

    # Re-allocate _processed_action to hold both position and velocity
    self._processed_action = torch.zeros(self.num_envs, self._n_dofs * 2, device=self.device)

    # The solver's flag: Genesis batches a heterogeneous entity's DOF info by itself in some scenes.
    self._batch_dofs_info = self.entity.has_batched_dofs_info

    # Cache PD gains
    self._kp = self.entity.get_dofs_kp(dofs_idx_local=self.genesis_dofs_idx)
    self._kd = self.entity.get_dofs_kd(dofs_idx_local=self.genesis_dofs_idx)
reset
reset(envs_idx: slice | Tensor | None = None) -> None
Source code in eden/managers/terms/actions/joint_actions.py
def reset(self, envs_idx: (slice | torch.Tensor) | None = None) -> None:
    super().reset(envs_idx)
    if envs_idx is None:
        envs_idx = slice(None)
    if self._batch_dofs_info:
        # Re-read the per-env gains so gain randomization is picked up. Skipped when DOF
        # info is unbatched purely to save the read: every env shares one row there, so
        # the write would be a no-op — the shapes line up either way.
        self._kp[envs_idx] = self.entity.get_dofs_kp(dofs_idx_local=self.genesis_dofs_idx, envs_idx=envs_idx)
        self._kd[envs_idx] = self.entity.get_dofs_kd(dofs_idx_local=self.genesis_dofs_idx, envs_idx=envs_idx)
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/joint_actions.py
def compute(self, actions: torch.Tensor) -> None:
    self._prev_action[:] = self._raw_action
    self._raw_action[:] = actions
    n_dofs = self._n_dofs
    if self._delta_reference is not None:
        # Capture once per env step; constant across decimation substeps.
        self._delta_reference[:] = self.entity.get_dofs_pos(dofs_idx_local=self.genesis_dofs_idx)
    joint_pos = self._compute_pos_target(self._raw_action[:, :n_dofs])
    joint_vel = self._raw_action[:, n_dofs:] * self.velocity_scale

    # Apply the control-step modifiers to the position half (e.g., envelope clip); a delay acts
    # on the whole command per physics step, in `apply_actions`.
    if self._modifier is not None:
        joint_pos = self._modifier.modify_processed_action(joint_pos)

    self._processed_action[:, :n_dofs] = joint_pos
    self._processed_action[:, n_dofs:] = joint_vel
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def apply_actions(self) -> None:
    n_dofs = self._n_dofs

    command = self._applied_command()
    target_pos = command[:, :n_dofs]
    target_vel = command[:, n_dofs:]
    dofs_vel = self.entity.get_dofs_vel(dofs_idx_local=self.genesis_dofs_idx)
    pos_err = target_pos - self.entity.get_dofs_pos(dofs_idx_local=self.genesis_dofs_idx)

    ctrl_torque = self._kp * pos_err + self._kd * (target_vel * self.feed_forward_ratio - dofs_vel)

    # Apply modifier to ctrl torque (e.g., backlash, friction, effort clip)
    if self._modifier is not None:
        ctrl_torque = self._modifier.modify_ctrl_torque(ctrl_torque, dofs_vel, pos_err=pos_err)

    self.verify_command_shape(ctrl_torque)
    self.entity.control_dofs_force(ctrl_torque, dofs_idx_local=self.genesis_dofs_idx)

ImplicitVelocityController

ImplicitVelocityController(env: EnvBase, options: ActionTermOptions)

Bases: _JointPDControllerBase

Joint velocity control through Genesis's built-in velocity mode.

Source code in eden/managers/terms/actions/joint_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._scale: float | torch.Tensor | None = None
    self._offset: torch.Tensor | None = None
    self._motion_ref_dofs_idx: torch.Tensor | None = None
    self._delta_reference: torch.Tensor | None = None
    # Per-env (num_envs, n_dofs) position offset from set_dofs_pos_offset; None until first set.
    self._dofs_pos_offset: torch.Tensor | None = None
    # ReferenceSource.ACCUMULATE state: the running target integrated across steps,
    # its default (init/reset) value, and the soft-limit clamp bounds. None otherwise.
    self._accumulated_target: torch.Tensor | None = None
    self._default_target: torch.Tensor | None = None
    self._target_lower: torch.Tensor | None = None
    self._target_upper: torch.Tensor | None = None
    # ``mimic_dofs`` state, set by ``_build_mimic``: the driver count (the action width), the drivers'
    # Genesis index (DELTA captures only those), and per follower its driver's column plus the coupling.
    self._n_action_dofs: int | None = None
    self._driver_genesis_dofs_idx: range | torch.Tensor | None = None
    self._mimic_cols: torch.Tensor | None = None
    self._mimic_mult: torch.Tensor | None = None
    self._mimic_offset: torch.Tensor | None = None
    self._mimic_follower_dofs_name: list[str] = []
    self._modifier: ActionModifier | None = None
    if self.modifier is not None:
        self._modifier = ACTION_MODIFIER_REGISTRY.get(self.modifier.name)(env=env, options=self.modifier)

Attributes

reference_source class-attribute instance-attribute
reference_source: ReferenceSource = ReferenceSource.ZERO

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def build(self) -> None:
    if self.reference_source != ReferenceSource.ZERO:
        raise ValueError(
            f"{term_subject(self)}: `reference_source='{self.reference_source}'` is not supported; "
            f"the action is interpreted as a target velocity."
        )
    super().build()
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def apply_actions(self) -> None:
    target = self._applied_command()
    self.verify_command_shape(target)
    self.entity.control_dofs_vel(target, dofs_idx_local=self.genesis_dofs_idx)

ExplicitVelocityController

ExplicitVelocityController(env: EnvBase, options: ActionTermOptions)

Bases: _JointPDControllerBase

Explicit joint velocity controller using manual torque computation.

The policy action is interpreted as a target joint velocity. Torque is computed as a proportional law on the velocity error:

.. math:: \tau = k_p (\dot{q}_{target} - \dot{q})

This matches the behavior of MuJoCo's velocity actuator and the implicit counterpart :class:ImplicitVelocityController. Use this variant when an explicit, auditable torque is required (e.g., sim2real, modifier composition, domain randomization of the gain).

The gain is derived from the DOF force range so it is automatically scaled to each joint's effort capacity:

.. math:: k_p = (F_{max} - F_{min}) \cdot \text{velocity_gain}

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

TYPE: str

dofs_name

The names of the DOFs to control. Supports glob patterns.

TYPE: str | list[str]

velocity_gain

Scales the force range to produce :math:k_p.

TYPE: float

modifier

Optional composable action modifier (e.g., effort clip, friction).

TYPE: ActionModifierOptions | None

Source code in eden/managers/terms/actions/joint_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._scale: float | torch.Tensor | None = None
    self._offset: torch.Tensor | None = None
    self._motion_ref_dofs_idx: torch.Tensor | None = None
    self._delta_reference: torch.Tensor | None = None
    # Per-env (num_envs, n_dofs) position offset from set_dofs_pos_offset; None until first set.
    self._dofs_pos_offset: torch.Tensor | None = None
    # ReferenceSource.ACCUMULATE state: the running target integrated across steps,
    # its default (init/reset) value, and the soft-limit clamp bounds. None otherwise.
    self._accumulated_target: torch.Tensor | None = None
    self._default_target: torch.Tensor | None = None
    self._target_lower: torch.Tensor | None = None
    self._target_upper: torch.Tensor | None = None
    # ``mimic_dofs`` state, set by ``_build_mimic``: the driver count (the action width), the drivers'
    # Genesis index (DELTA captures only those), and per follower its driver's column plus the coupling.
    self._n_action_dofs: int | None = None
    self._driver_genesis_dofs_idx: range | torch.Tensor | None = None
    self._mimic_cols: torch.Tensor | None = None
    self._mimic_mult: torch.Tensor | None = None
    self._mimic_offset: torch.Tensor | None = None
    self._mimic_follower_dofs_name: list[str] = []
    self._modifier: ActionModifier | None = None
    if self.modifier is not None:
        self._modifier = ACTION_MODIFIER_REGISTRY.get(self.modifier.name)(env=env, options=self.modifier)

Attributes

reference_source class-attribute instance-attribute
reference_source: ReferenceSource = ReferenceSource.ZERO
velocity_gain class-attribute instance-attribute
velocity_gain: float = 0.25

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def build(self) -> None:
    if self.reference_source != ReferenceSource.ZERO:
        raise ValueError(
            f"{term_subject(self)}: `reference_source='{self.reference_source}'` is not supported; "
            f"the action is interpreted as a target velocity."
        )

    super().build()

    # The solver's flag: Genesis batches a heterogeneous entity's DOF info by itself in some scenes.
    self._batch_dofs_info = self.entity.has_batched_dofs_info

    self._kp = self._compute_gain()
reset
reset(envs_idx: slice | Tensor | None = None) -> None
Source code in eden/managers/terms/actions/joint_actions.py
def reset(self, envs_idx: (slice | torch.Tensor) | None = None) -> None:
    super().reset(envs_idx)
    if envs_idx is None:
        envs_idx = slice(None)
    if self._batch_dofs_info:
        self._kp[envs_idx] = self._compute_gain(envs_idx=envs_idx)
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def apply_actions(self) -> None:
    dofs_vel = self.entity.get_dofs_vel(dofs_idx_local=self.genesis_dofs_idx)
    ctrl_torque = self._kp * (self._applied_command() - dofs_vel)

    if self._modifier is not None:
        ctrl_torque = self._modifier.modify_ctrl_torque(ctrl_torque, dofs_vel)

    self.verify_command_shape(ctrl_torque)
    self.entity.control_dofs_force(ctrl_torque, dofs_idx_local=self.genesis_dofs_idx)

NullJointAction

NullJointAction(env: EnvBase, options: ActionTermOptions)

Bases: ActionTerm

Virtually fix the DoFs to the default positions.

Consumes no policy output: the DOFs are held at default_dofs_pos for the whole episode, so there is nothing for the policy to decide and :attr:action_dim is 0. The term still commands its DOFs — processed_action stays (num_envs, num_dofs) and reaches hardware through the usual deployment map — it just draws none of the action vector.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

dofs_name

The names of the DOFs to control.

Source code in eden/managers/terms/actions/joint_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._processed_action: torch.Tensor | None = None
    self._raw_action: torch.Tensor | None = None
    self._prev_action: torch.Tensor | None = None

Attributes

action_dim property
action_dim: int

No policy output: this term pins its DOFs rather than steering them.

The base class returns the DOF count, which would have ActionManager reserve a slice of the action vector per pinned DOF — columns the policy would learn to emit and compute would discard, since it is a no-op. num_dofs is left alone: the term commands those DOFs, it just takes no input for them. That is also what keeps this term out of ActionManager._reject_no_op_terms, which rejects any term whose action_dim and num_dofs are both zero — a dofs_name-less term that would command nothing.

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def build(self) -> None:
    super().build()

    # Selected by name on the *entity*: the term's DOFs are columns of the entity's `dofs_name`, not its
    # first `num_dofs` — a term controlling any subset but `joint_0` onwards would otherwise hold the wrong
    # joints' defaults. The getter's `(num_envs, n)` is what the command below is written to, whether the
    # stored tensor has one row or one per env.
    #
    # `_raw_action` / `_prev_action` are left as `super().build()` allocated them —
    # `(num_envs, 0)`, matching `action_dim` — so this term's `action` / `prev_action`
    # observations report the empty slice it actually receives rather than a width of
    # permanently-zero columns.
    self._processed_action = self.entity.get_default_dofs_pos(self.dofs_name)
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/joint_actions.py
def compute(self, actions: torch.Tensor) -> None:
    pass
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/joint_actions.py
def apply_actions(self) -> None:
    # The term's DOFs may be a subset of the entity's, or ordered differently: pass its own index.
    self.verify_command_shape(self._processed_action)
    self.entity.control_dofs_pos(self._processed_action, dofs_idx_local=self.genesis_dofs_idx)

Functions:

is_joint_position_controller

is_joint_position_controller(term: type | ActionTerm) -> bool

Whether term (a term class or instance) is a joint PD controller that commands positions.

True for :class:ImplicitPDController, :class:ExplicitPDController, :class:VelocityFeedforwardPDController and their subclasses; False for the velocity controllers, which share the PD base for its scale/offset plumbing but command a velocity, and for every other action term. This is the set a robot's position action scale, and a joint-zero offset such as :class:~eden.managers.terms.events.domain_rand.RandomizeDofsPosOffset, apply to.

PARAMETER DESCRIPTION
term

An action term class, or a built term instance.

TYPE: type | ActionTerm

Source code in eden/managers/terms/actions/joint_actions.py
def is_joint_position_controller(term: type | ActionTerm) -> bool:
    """Whether ``term`` (a term class or instance) is a joint PD controller that commands positions.

    True for :class:`ImplicitPDController`, :class:`ExplicitPDController`,
    :class:`VelocityFeedforwardPDController` and their subclasses; False for the velocity
    controllers, which share the PD base for its scale/offset plumbing but command a velocity, and
    for every other action term. This is the set a robot's position action scale, and a joint-zero
    offset such as :class:`~eden.managers.terms.events.domain_rand.RandomizeDofsPosOffset`, apply to.

    Parameters
    ----------
    term: type | ActionTerm
        An action term class, or a built term instance.
    """
    cls = term if isinstance(term, type) else type(term)
    return issubclass(cls, _JointPDControllerBase) and cls._targets_position_

Task-space controllers

Task-space action terms: differential IK, absolute pose IK, and operational-space control.

Attributes

Classes

DifferentialIKController

DifferentialIKController(env: EnvBase, options: ActionTermOptions)

Bases: ActionTerm

Action term that maps task-space commands to joint targets via differential IK.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

dofs_name

The names of the DOFs to control.

ee_link_name

The name of the end-effector link.

ee_local_pos

Control-point position in the end-effector link frame, in metres: the point the position rows move, and the point a rotation command turns the link about. The default is the link origin.

position_scale

Metres per normalized action unit for the position axes. A scalar applies to x, y, and z; a length-3 list configures the axes independently.

orientation_scale

Radians per normalized action unit for the orientation axes. A scalar applies to rx, ry, and rz; a length-3 list configures the axes independently.

damping

Levenberg-Marquardt damping lambda for the differential-IK least squares Jt (J Jt + lambda I)^-1. Larger values trade tracking accuracy for stability near singularities, where an under-damped solve produces large delta_q spikes (visible as end-effector jitter). mjlab's DifferentialIKActionCfg uses 0.1 (the default here); an under-damped value like 1e-4 is effectively undamped.

max_dq

Optional per-step clamp (rad) on the IK joint delta, mirroring mjlab's max_dq. Caps how far any joint may move in a single control step, which bounds end-effector speed and smooths motion. None disables it.

position_weight

Task-space weight on the position (x, y, z) rows of the IK solve.

orientation_weight

Task-space weight on the orientation (rx, ry, rz) rows of the IK solve, mirroring mjlab's DifferentialIKActionCfg.orientation_weight. The damped least squares weights both the Jacobian rows and the task error per axis, so orientation_weight=0.0 makes it position-only — the wrist orientation is left uncontrolled (free in the nullspace) rather than driven by the policy, which removes wrist jitter without pinning the wrist. Defaults to 1.0 (full 6-DOF). Relies on damping > 0 to keep zero-weighted rows of J Jt invertible.

Source code in eden/managers/terms/actions/task_space_actions.py
def __init__(
    self,
    env: EnvBase,
    options: ActionTermOptions,
):
    super().__init__(env=env, options=options)
    self._ee_link = None
    self._ee_local_pos = None
    self._diag = None
    self._scale = None
    self._task_weight = None

Attributes

ee_link_name: str = ''
ee_local_pos class-attribute instance-attribute
ee_local_pos: tuple[float, float, float] = (0.0, 0.0, 0.0)
position_scale class-attribute instance-attribute
position_scale: float | list[float] = _DEFAULT_POSITION_SCALE
orientation_scale class-attribute instance-attribute
orientation_scale: float | list[float] = _DEFAULT_ORIENTATION_SCALE
damping class-attribute instance-attribute
damping: float = 0.1
max_dq class-attribute instance-attribute
max_dq: float | None = None
position_weight class-attribute instance-attribute
position_weight: float = 1.0
orientation_weight class-attribute instance-attribute
orientation_weight: float = 1.0
action_dim property
action_dim
effective_scale property
effective_scale: Tensor

Return the resolved task-space scale in (x, y, z, rx, ry, rz) order.

diag property
diag

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def build(self) -> None:
    super().build()

    self._ee_link = self.entity.get_link(self.ee_link_name)
    if len(self.ee_local_pos) != 3 or not all(math.isfinite(value) for value in self.ee_local_pos):
        raise ValueError(
            f"{term_subject(self)}: ee_local_pos must contain three finite values, got {self.ee_local_pos}"
        )
    self._ee_local_pos = torch.tensor(self.ee_local_pos, dtype=gs.tc_float, device=self.device)

    # Jacobian columns are in dofs_name order; build indices to select
    # this action's DOF subset from the reordered Jacobian. A welded child (a
    # composite's gripper / dex hand) has no base columns either — Genesis ``attach``
    # removed its root joint — so gate on ``has_immobile_base``, matching ``_jac_perm``.
    base_offset = 0 if self.entity.has_immobile_base else 6
    self._jac_dofs_idx = torch.as_tensor(
        [base_offset + self.entity.dofs_idx_map[idx] for idx in self.dofs_idx_local.tolist()],
        dtype=gs.tc_int,
        device=self.device,
    )

    self._scale = torch.cat(
        (
            _resolve_axis_parameter(self.position_scale, 3, device=self.device),
            _resolve_axis_parameter(self.orientation_scale, 3, device=self.device),
        )
    )

    # Per-axis task weights; None means uniform 1.0 and skips the weighting.
    if self.position_weight != 1.0 or self.orientation_weight != 1.0:
        self._task_weight = torch.tensor(
            [self.position_weight] * 3 + [self.orientation_weight] * 3,
            device=self.device,
            dtype=gs.tc_float,
        ).unsqueeze(0)  # (1, 6)
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def compute(self, actions: torch.Tensor) -> None:
    assert actions.shape == (
        self.num_envs,
        self.action_dim,
    ), f"Expected actions shape: (num_envs, {self.action_dim}), got {actions.shape=}"
    self._prev_action[:] = self._raw_action
    self._raw_action[:] = actions

    delta_target = self._scale * torch.tanh(actions)  # (n_envs, 6)
    # (n_envs, 6, n_dofs), the linear rows at the control point.
    J = self.entity.get_jacobian(link=self._ee_link, local_point=self._ee_local_pos)[:, :, self._jac_dofs_idx]
    if self._task_weight is not None:
        # Weighted damped least squares: weight the Jacobian rows and task
        # error per axis. orientation_weight=0 zeros the orientation rows
        # (damping keeps J Jt invertible) -> position-only IK, wrist free.
        J = J * self._task_weight.unsqueeze(-1)  # (n_envs, 6, n_dofs)
        delta_target = delta_target * self._task_weight  # (n_envs, 6)
    J_T = J.transpose(1, 2)  # (n_envs, n_dofs, 6)
    # `solve_ex`, not `solve`: `solve` reads the LU info back to raise on a singular system, a GPU->CPU
    # sync every control step. `J Jt + damping * I` is positive definite, so there is nothing to catch.
    delta_q = J_T @ torch.linalg.solve_ex(J @ J_T + self.diag, delta_target)[0].unsqueeze(-1)
    delta_q = delta_q.squeeze(-1)  # (n_envs, n_dofs)

    # Per-step joint-delta clamp (mjlab's max_dq): bounds joint speed.
    if self.max_dq is not None:
        delta_q = delta_q.clamp(-self.max_dq, self.max_dq)

    self._processed_action = delta_q + self.entity.get_dofs_pos(dofs_idx_local=self.genesis_dofs_idx)
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        envs_idx = slice(None)
    self._prev_action[envs_idx] = 0.0
    self._raw_action[envs_idx] = 0.0
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def apply_actions(self) -> None:
    self.entity.control_dofs_pos(self._processed_action, dofs_idx_local=self.genesis_dofs_idx)

OperationalSpaceController

OperationalSpaceController(env: EnvBase, options: ActionTermOptions)

Bases: ActionTerm

Operational-space control to a persistent relative position/pose target.

Policy actions are clipped, scaled, and composed with the measured world-frame control-point pose in :meth:compute. The resulting absolute target stays fixed while :meth:apply_actions recomputes feedback, Jacobian, inertia, nullspace, and DOF force on every physics substep. Gravity compensation is configured on the rigid material of the kinematic-tree root, including the root parent rather than only the controlled child in a welded tree. This controller does not add Coriolis or centrifugal bias forces. The entity's configured force range saturates the controller command, not the total effective effort including material gravity compensation.

The controller supports fixed entities and entities welded through a static parent chain. dofs_name defines one ordered controller space shared by the Jacobian, mass matrix, DOF state, nullspace objective, and output force. When dynamics are enabled, build rejects a selected DOF set that omits an ancestor DOF of the control-point link. The selected mass submatrix further assumes remaining excluded branch DOFs are fixed or held so their acceleration can be neglected, or are dynamically decoupled; assigning them another controller does not by itself remove dynamic coupling. Operational inertia uses a relative Tikhonov approximation to the Moore-Penrose pseudoinverse, so a rank-deficient Jacobian has a finite damped minimum-norm response.

Planned extensions

TODO(generic-osc-extensions): Add absolute pose targets, configurable task/root/world command frames, per-axis motion selection, absolute wrench and hybrid motion-force control, variable impedance, and command interpolation as concrete Eden callers require them. Extend the dynamics contract to floating bases and passive coupled coordinates only with an explicit generalized-coordinate index space. Keep multi-end-effector, mobile-manipulator, and whole-body coordination in a composite layer above this single-control-point action term.

PARAMETER DESCRIPTION
ee_link_name

Link carrying the controlled point.

TYPE: str

ee_local_pos

Control-point position in the end-effector link frame, in metres.

TYPE: tuple[float, float, float]

ee_local_quat

Control-point orientation in the end-effector link frame as a normalized wxyz quaternion.

TYPE: tuple[float, float, float, float]

control_mode

Whether actions command three world-frame translation deltas or translation plus world-frame axis-angle rotation deltas.

TYPE: ('position', 'pose') DEFAULT: "position"

position_scale

Metres per translation action unit, scalar or axis-wise.

TYPE: float | tuple[float, float, float]

orientation_scale

Radians per axis-angle rotation action unit, scalar or axis-wise. Ignored in position mode.

TYPE: float | tuple[float, float, float]

action_clip

Symmetric pre-scaling action limit for each controlled task axis.

TYPE: float | tuple[float, ...]

kp

Nonnegative task-space proportional gains for the controlled axes.

TYPE: float | tuple[float, ...]

damping_ratio

Nonnegative damping ratios. Derivative gains are 2 * damping_ratio * sqrt(kp).

TYPE: float | tuple[float, ...]

inertial_decoupling

none maps the feedback wrench directly, full multiplies it by the coupled operational inertia, and position_orientation uses separate translational and rotational operational inertias.

TYPE: ('none', 'full', 'position_orientation') DEFAULT: "none"

operational_inertia_damping

Positive dimensionless Tikhonov ridge coefficient relative to the squared largest absolute entry of each task-space inverse-inertia matrix.

TYPE: float

nullspace_kp

Positive DOF-space posture gain, or None to disable the secondary posture objective. Enabling it requires more selected DOFs than controlled task axes.

TYPE: float | None

nullspace_damping_ratio

Nonnegative damping ratio for the nullspace posture objective.

TYPE: float

nullspace_reference

Track the entity default DOF positions or the measured DOF positions at the most recent reset.

TYPE: ('default', 'reset_pose') DEFAULT: "default"

Source code in eden/managers/terms/actions/task_space_actions.py
def __init__(self, env: EnvBase, options: ActionTermOptions) -> None:
    super().__init__(env=env, options=options)
    self._ee_link_idx: int | None = None
    self._ee_local_pos: torch.Tensor | None = None
    self._ee_local_quat: torch.Tensor | None = None
    self._dofs_idx_name_order: torch.Tensor | None = None
    self._position_scale: torch.Tensor | None = None
    self._orientation_scale: torch.Tensor | None = None
    self._action_clip: torch.Tensor | None = None
    self._scaled_action: torch.Tensor | None = None
    self._kp: torch.Tensor | None = None
    self._kd: torch.Tensor | None = None
    self._task_identity: torch.Tensor | None = None
    self._dofs_identity: torch.Tensor | None = None
    self._nullspace_acceleration: torch.Tensor | None = None
    self._nullspace_reference_pos: torch.Tensor | None = None
    self._goal_pos: torch.Tensor | None = None
    self._goal_quat: torch.Tensor | None = None
    self._nullspace_kd = 0.0
    self._needs_mass_matrix = False

Attributes

ee_link_name: str = ''
ee_local_pos class-attribute instance-attribute
ee_local_pos: tuple[float, float, float] = (0.0, 0.0, 0.0)
ee_local_quat class-attribute instance-attribute
ee_local_quat: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)
control_mode class-attribute instance-attribute
control_mode: Literal['position', 'pose'] = 'pose'
position_scale class-attribute instance-attribute
position_scale: float | tuple[float, float, float] = _DEFAULT_POSITION_SCALE
orientation_scale class-attribute instance-attribute
orientation_scale: float | tuple[float, float, float] = _DEFAULT_ORIENTATION_SCALE
action_clip class-attribute instance-attribute
action_clip: float | tuple[float, ...] = 1.0
kp class-attribute instance-attribute
kp: float | tuple[float, ...] = 100.0
damping_ratio class-attribute instance-attribute
damping_ratio: float | tuple[float, ...] = 1.0
inertial_decoupling class-attribute instance-attribute
inertial_decoupling: Literal['none', 'full', 'position_orientation'] = 'none'
operational_inertia_damping class-attribute instance-attribute
operational_inertia_damping: float = 1e-06
nullspace_kp class-attribute instance-attribute
nullspace_kp: float | None = None
nullspace_damping_ratio class-attribute instance-attribute
nullspace_damping_ratio: float = 1.0
nullspace_reference class-attribute instance-attribute
nullspace_reference: Literal['default', 'reset_pose'] = 'default'
action_dim property
action_dim: int
effective_scale property
effective_scale: Tensor

Return the resolved scale in position-then-orientation axis order.

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def build(self) -> None:
    self._resolve_controller_parameters()
    super().build()
    self._resolve_control_topology()

    tensor_options = {"dtype": gs.tc_float, "device": self.device}
    self._scaled_action = torch.zeros((self.num_envs, self.action_dim), **tensor_options)
    if self._needs_mass_matrix:
        self._task_identity = torch.eye(self.action_dim, **tensor_options).unsqueeze(0)
    if self.nullspace_kp is not None:
        self._dofs_identity = torch.eye(self.num_dofs, **tensor_options).unsqueeze(0)
        self._nullspace_acceleration = torch.zeros((self.num_envs, self.num_dofs), **tensor_options)
        self._nullspace_reference_pos = torch.zeros((self.num_envs, self.num_dofs), **tensor_options)
        self._nullspace_kd = 2.0 * self.nullspace_damping_ratio * self.nullspace_kp**0.5
    self._goal_pos = torch.zeros((self.num_envs, 3), **tensor_options)
    self._goal_quat = torch.zeros((self.num_envs, 4), **tensor_options)

    self._set_goal_to_current(slice(None))
    if self._nullspace_reference_pos is not None:
        self._refresh_nullspace_reference(slice(None))
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def compute(self, actions: torch.Tensor) -> None:
    expected_shape = (self.num_envs, self.action_dim)
    if tuple(actions.shape) != expected_shape:
        raise ValueError(
            f"{term_subject(self)}: actions must have shape {expected_shape}, got {tuple(actions.shape)}"
        )
    self._prev_action[:] = self._raw_action
    self._raw_action[:] = actions
    self._scaled_action.copy_(actions)
    torch.clamp(self._scaled_action, min=-self._action_clip, max=self._action_clip, out=self._scaled_action)
    self._scaled_action[:, :3].mul_(self._position_scale)
    if self.control_mode == "pose":
        self._scaled_action[:, 3:].mul_(self._orientation_scale)

    entity = self.resolve_entity(self.entity_name)
    current_pos, current_quat = self._control_point_pose(entity)
    self._update_goal_from_action(current_pos, current_quat, self._scaled_action)
reset
reset(envs_idx: slice | Tensor | None = None) -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def reset(self, envs_idx: slice | torch.Tensor | None = None) -> None:
    if envs_idx is None:
        envs_idx = slice(None)
    self._prev_action[envs_idx] = 0.0
    self._raw_action[envs_idx] = 0.0
    self._processed_action[envs_idx] = 0.0
    self._reset_goal(envs_idx)
    if self._nullspace_reference_pos is not None:
        self._refresh_nullspace_reference(envs_idx)
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def apply_actions(self) -> None:
    entity = self.resolve_entity(self.entity_name)
    current_pos, current_quat = self._control_point_pose(entity)
    full_jacobian = entity.get_jacobian(link=entity.links[self._ee_link_idx], local_point=self._ee_local_pos)[
        :, : self.action_dim
    ]
    jacobian = full_jacobian.index_select(-1, self._dofs_idx_name_order)
    full_dofs_vel = entity.get_dofs_vel()
    task_velocity = (full_jacobian @ full_dofs_vel.unsqueeze(-1)).squeeze(-1)
    if self.control_mode == "position":
        task_error = self._goal_pos - current_pos
    else:
        task_error = torch.cat(
            [self._goal_pos - current_pos, self._orientation_error(self._goal_quat, current_quat)],
            dim=-1,
        )
    desired_acceleration = self._kp * task_error - self._kd * task_velocity
    invalid_envs = (
        ~torch.isfinite(full_jacobian).all(dim=(-2, -1))
        | ~torch.isfinite(full_dofs_vel).all(dim=-1)
        | ~torch.isfinite(desired_acceleration).all(dim=-1)
    )

    if self._needs_mass_matrix:
        mass_matrix = entity.get_mass_mat()
        mass_matrix = mass_matrix.index_select(1, self.dofs_idx_local).index_select(2, self.dofs_idx_local)
        mass_solve_jacobian_t, invalid_mass_solve = _solve_mass_matrix_jacobian_transpose(
            mass_matrix,
            jacobian,
        )
        invalid_envs |= invalid_mass_solve
        mass_solve_jacobian_t.masked_fill_(invalid_envs[:, None, None], 0.0)
        if self.inertial_decoupling == "full" or self.control_mode == "position" or self.nullspace_kp is not None:
            task_inertia, invalid_task_inertia = _damped_pseudoinverse(
                jacobian @ mass_solve_jacobian_t,
                self._task_identity,
                self.operational_inertia_damping,
            )
            invalid_envs |= invalid_task_inertia
        if self.nullspace_kp is not None:
            jacobian_bar = mass_solve_jacobian_t @ task_inertia
            nullspace = self._dofs_identity - jacobian_bar @ jacobian
    if self.inertial_decoupling == "none":
        desired_wrench = desired_acceleration
    elif self.inertial_decoupling == "full" or self.control_mode == "position":
        desired_wrench = (task_inertia @ desired_acceleration.unsqueeze(-1)).squeeze(-1)
    else:
        block_identity = self._task_identity[:, :3, :3]
        position_inertia, invalid_position_inertia = _damped_pseudoinverse(
            jacobian[:, :3] @ mass_solve_jacobian_t[:, :, :3],
            block_identity,
            self.operational_inertia_damping,
        )
        orientation_inertia, invalid_orientation_inertia = _damped_pseudoinverse(
            jacobian[:, 3:] @ mass_solve_jacobian_t[:, :, 3:],
            block_identity,
            self.operational_inertia_damping,
        )
        invalid_envs |= invalid_position_inertia | invalid_orientation_inertia
        desired_wrench = torch.cat(
            [
                (position_inertia @ desired_acceleration[:, :3].unsqueeze(-1)).squeeze(-1),
                (orientation_inertia @ desired_acceleration[:, 3:].unsqueeze(-1)).squeeze(-1),
            ],
            dim=-1,
        )
    dofs_force = (jacobian.transpose(-2, -1) @ desired_wrench.unsqueeze(-1)).squeeze(-1)

    if self.nullspace_kp is not None:
        dofs_pos = entity.get_dofs_pos(dofs_idx_local=self.genesis_dofs_idx)
        dofs_vel = entity.get_dofs_vel(dofs_idx_local=self.genesis_dofs_idx)
        self._nullspace_acceleration[:] = (
            self.nullspace_kp * (self._nullspace_reference_pos - dofs_pos) - self._nullspace_kd * dofs_vel
        )
        nullspace_force = mass_matrix @ self._nullspace_acceleration.unsqueeze(-1)
        dofs_force += (nullspace.transpose(-2, -1) @ nullspace_force).squeeze(-1)

    invalid_envs |= ~torch.isfinite(dofs_force).all(dim=-1)
    dofs_force.masked_fill_(invalid_envs[:, None], 0.0)
    self._processed_action.copy_(dofs_force)
    self.verify_command_shape(self._processed_action)
    entity.control_dofs_force(self._processed_action, dofs_idx_local=self.genesis_dofs_idx)

IKPoseController

IKPoseController(env: EnvBase, options: ActionTermOptions)

Bases: ActionTerm

Absolute task-space pose controller: solve IK for a base-frame target pose each step.

The action is a base-frame target pose [x, y, z, qw, qx, qy, qz] (wxyz) for the end-effector link, optionally expressed at a virtual tool frame offset from that link. Each control step the target is transformed to the world frame, the tool offset is removed, and Genesis' IK is solved from the measured joint configuration so the resulting joint targets stay on the current elbow branch. Policies that emit accumulated Cartesian targets (RobotArena-style delta accumulation, DROID Cartesian velocity effectors) drive this term; a policy that emits per-step deltas relative to the measured pose should use :class:DifferentialIKController instead.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

dofs_name

The arm DOFs the IK solution is applied to.

ee_link_name

The end-effector link the target pose refers to.

ee_link_offset_pos

Position of the virtual tool frame in the end-effector link frame. The link is driven to the pose that places this offset frame at the target.

ee_link_offset_quat

Orientation (wxyz) of the virtual tool frame in the end-effector link frame.

max_joint_delta

Per-step clamp (rad) on every joint's change from its measured position; also turns an IK branch discontinuity into a bounded step. None disables it.

joint_limit_margin

Keep commanded joints at least this far (rad) inside their limits.

max_ik_position_error

Reject an IK solution whose position residual (m) exceeds this and hold the measured arm pose instead. None disables the check.

max_ik_rotation_error

Same for the rotation residual (rad).

Source code in eden/managers/terms/actions/task_space_actions.py
def __init__(self, env: EnvBase, options: ActionTermOptions):
    super().__init__(env=env, options=options)
    self._ee_link = None
    self._ee_link_offset_pos_inv = None
    self._ee_link_offset_quat_inv = None

Attributes

ee_link_name: str = 'gripper_link'
ee_link_offset_pos: tuple[float, float, float] = (0.0, 0.0, 0.0)
ee_link_offset_quat: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)
max_joint_delta class-attribute instance-attribute
max_joint_delta: float | None = None
joint_limit_margin class-attribute instance-attribute
joint_limit_margin: float = 0.0
max_ik_position_error class-attribute instance-attribute
max_ik_position_error: float | None = None
max_ik_rotation_error class-attribute instance-attribute
max_ik_rotation_error: float | None = None
action_dim property
action_dim: int

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def build(self) -> None:
    super().build()
    if self.entity.n_qs != self.entity.n_dofs:
        # The IK seed and solution are full Genesis qpos vectors; indexing them by DOF index
        # is only valid when qpos and DOF indices coincide (no free/ball joints).
        raise ValueError(
            f"{term_subject(self)} requires a fixed-base entity with one qpos per DOF; "
            f"it has n_qs={self.entity.n_qs}, n_dofs={self.entity.n_dofs}."
        )
    self._all_qs_idx = torch.arange(self.entity.n_qs, device=self.device)
    self._ee_link = self.entity.get_link(self.ee_link_name)
    offset_pos = torch.as_tensor(self.ee_link_offset_pos, dtype=torch.float32, device=self.device).unsqueeze(0)
    offset_quat = torch.as_tensor(self.ee_link_offset_quat, dtype=torch.float32, device=self.device).unsqueeze(0)
    offset_quat = offset_quat / offset_quat.norm(dim=-1, keepdim=True).clamp_min(1e-8)
    self._ee_link_offset_quat_inv = inv_quat(offset_quat)
    self._ee_link_offset_pos_inv = transform_by_quat(-offset_pos, self._ee_link_offset_quat_inv)
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def compute(self, actions: torch.Tensor) -> None:
    assert actions.shape == (
        self.num_envs,
        self.action_dim,
    ), f"Expected actions shape: (num_envs, {self.action_dim}), got {actions.shape=}"
    self._prev_action[:] = self._raw_action
    self._raw_action[:] = actions

    target_pos_b = actions[:, :3]
    target_quat_b = actions[:, 3:7]
    target_quat_b = target_quat_b / target_quat_b.norm(dim=-1, keepdim=True).clamp_min(1e-8)
    # The target refers to the virtual tool frame; drive the EE link to the
    # pose that places that offset frame at the requested target.
    target_pos_w, target_quat_w = combine_frame_transforms(
        self.entity.get_pos(),
        self.entity.get_quat(),
        target_pos_b,
        target_quat_b,
    )
    target_pos_w, target_quat_w = combine_frame_transforms(
        target_pos_w,
        target_quat_w,
        self._ee_link_offset_pos_inv,
        self._ee_link_offset_quat_inv,
    )

    # Genesis-local indices: read through dofs_idx_local, never index a dofs_name-ordered array.
    current_arm = self.entity.get_dofs_pos(dofs_idx_local=self.genesis_dofs_idx)
    # Full qpos in Genesis-native order, the layout inverse_kinematics expects for init_qpos.
    current_qpos = self.entity.get_qpos(qs_idx_local=self._all_qs_idx)
    ik_result = self.entity.inverse_kinematics(
        self.ee_link_name,
        poss=target_pos_w,
        quats=target_quat_w,
        # Explicitly seed from the measured configuration. This prevents
        # random-restart IK from selecting a distant Franka elbow branch.
        init_qpos=current_qpos,
        dofs_idx_local=self.genesis_dofs_idx,
        return_error=True,
    )
    target_q, ik_error = ik_result
    if not isinstance(target_q, torch.Tensor):
        target_q = torch.as_tensor(target_q, dtype=torch.float32, device=self.device)
    target_q = target_q.to(device=self.device, dtype=torch.float32)
    if target_q.ndim == 1:
        target_q = target_q.unsqueeze(0)

    # inverse_kinematics returns the full Genesis-native qpos (n_qs == n_dofs, checked in build()).
    processed = target_q[:, self.dofs_idx_local]
    if not isinstance(ik_error, torch.Tensor):
        ik_error = torch.as_tensor(ik_error, dtype=torch.float32, device=self.device)
    ik_error = ik_error.to(device=self.device, dtype=torch.float32)
    if ik_error.ndim == 1:
        ik_error = ik_error.unsqueeze(0)

    finite = torch.isfinite(processed).all(dim=-1) & torch.isfinite(ik_error).all(dim=-1)
    acceptable = finite.clone()
    if self.max_ik_position_error is not None:
        acceptable &= ik_error[..., :3].norm(dim=-1) <= float(self.max_ik_position_error)
    if self.max_ik_rotation_error is not None:
        acceptable &= ik_error[..., 3:6].norm(dim=-1) <= float(self.max_ik_rotation_error)
    if not acceptable.all():
        # Never pass a failed IK result into the rigid solver: one NaN
        # position target permanently poisons the simulation state. Hold
        # the affected arm at its measured joints and continue instead.
        processed = torch.where(acceptable.unsqueeze(-1), processed, current_arm)
        rejected = ~acceptable
        position_residual = ik_error[..., :3].norm(dim=-1)
        rotation_residual = ik_error[..., 3:6].norm(dim=-1)
        rejected_position_residual = position_residual[rejected].detach().cpu().tolist()
        rejected_rotation_residual = rotation_residual[rejected].detach().cpu().tolist()
        rejected_target_pos_b = target_pos_b[rejected].detach().cpu().tolist()
        rejected_target_quat_b = target_quat_b[rejected].detach().cpu().tolist()
        en.logger.warning(
            f"{term_subject(self)} rejected an invalid or excessive-residual IK target; "
            f"holding the current arm pose for {int(rejected.sum().item())} environment(s). "
            f"position_residual_m={rejected_position_residual} "
            f"(limit={self.max_ik_position_error}), "
            f"rotation_residual_rad={rejected_rotation_residual} "
            f"(limit={self.max_ik_rotation_error}), "
            f"target_pos_base_m={rejected_target_pos_b}, "
            f"target_quat_base_wxyz={rejected_target_quat_b}."
        )

    # DROID's RobotIKSolver limits every Franka joint to 0.2 rad per
    # Cartesian command. Applying the same bound here also makes an IK
    # branch discontinuity harmless instead of a full-arm jump.
    if self.max_joint_delta is not None:
        max_delta = float(self.max_joint_delta)
        processed = current_arm + torch.clamp(processed - current_arm, -max_delta, max_delta)

    # DROID's dm_robotics Cartesian effector keeps 0.3 rad away from joint
    # limits. Clamp the commanded target to the same interior band. If the
    # measured state is already outside it, max_joint_delta above moves it
    # back gradually rather than teleporting it.
    if self.joint_limit_margin > 0.0:
        lower, upper = self.entity.get_dofs_limit(dofs_idx_local=self.genesis_dofs_idx)
        lower = torch.as_tensor(lower, dtype=torch.float32, device=self.device)
        upper = torch.as_tensor(upper, dtype=torch.float32, device=self.device)
        safe_lower = lower + float(self.joint_limit_margin)
        safe_upper = upper - float(self.joint_limit_margin)
        processed = torch.maximum(safe_lower, torch.minimum(processed, safe_upper))
        if self.max_joint_delta is not None:
            max_delta = float(self.max_joint_delta)
            processed = current_arm + torch.clamp(processed - current_arm, -max_delta, max_delta)
    self._processed_action = processed
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        envs_idx = slice(None)
    self._prev_action[envs_idx] = 0.0
    self._raw_action[envs_idx] = 0.0
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/task_space_actions.py
def apply_actions(self) -> None:
    self.entity.control_dofs_pos(self._processed_action, dofs_idx_local=self.genesis_dofs_idx)

Functions:

Floating-base pose control

Pose control of a floating base: a hand or gripper that flies, with no arm under it.

Attributes

Classes

FreeBasePoseController

FreeBasePoseController(env: EnvBase, options: ActionTermOptions)

Bases: ActionTerm

Drive a floating base to a pose near its default root pose (Genesis free-joint PD).

For an entity that stands on nothing (a dexterous hand or a gripper with no arm: a "flying" end effector), this term is the arm. Its action is six numbers per environment: a translation along the world axes, at position_scale metres per unit, then a rotation vector about the world axes, at orientation_scale radians per unit. Both are applied to the entity's default root pose, about its authored origin (a hand's wrist), so the target is absolute: a zero action holds the default pose.

The target orientation is composed as a quaternion and only then written as the free joint's intrinsic xyz Euler angles, which Genesis turns straight back into a quaternion to take a rotation-vector error. Nothing is added in Euler space, so a default orientation at the Euler singularity (pitch at 90 degrees, which a palm-down hand is) costs nothing.

Genesis position control places the solver frame, which for a re-origined body is not the authored frame that default_root_pos / default_root_quat describe (see docs/guides/entities.md). The offset between them is measured once at build and applied to every target.

The gains below are written to the six free-joint DOFs at build. Nothing else sets them: Eden applies default_dofs_kp / default_dofs_kd to the joints in dofs_name only, and Genesis leaves a free joint at zero gain with an unbounded force range. Gravity is not in the loop. Give the entity RigidMaterialOptions(gravity_compensation=1.0), as a real arm's controller would, or it hangs m * g / position_kp below its target and the orientation loop carries the weight's moment about the origin.

PARAMETER DESCRIPTION
entity_name

The floating-base entity. It must have exactly one free joint and must not be attached to another entity.

TYPE: str

position_scale

Metres per unit of translation action, scalar or per world axis.

TYPE: float | tuple[float, float, float]

orientation_scale

Radians per unit of rotation-vector action, scalar or per world axis.

TYPE: float | tuple[float, float, float]

position_kp

Translational stiffness, N/m.

TYPE: float

position_kd

Translational damping, N·s/m.

TYPE: float

orientation_kp

Rotational stiffness, N·m/rad.

TYPE: float

orientation_kd

Rotational damping, N·m·s/rad.

TYPE: float

force_limit

Bound on each translational DOF's control force, N.

TYPE: float

torque_limit

Bound on each rotational DOF's control torque, N·m.

TYPE: float

Source code in eden/managers/action_manager.py
def __init__(self, env: EnvBase, options: ActionTermOptions):
    super().__init__(env=env, options=options)

    self._entity = None
    self.dofs_idx_local = None
    # The memo behind ``genesis_dofs_idx``: its value, and the ``dofs_idx_local`` it was derived from.
    self._genesis_dofs_idx_source: torch.Tensor | None = None
    self._genesis_dofs_idx: range | torch.Tensor | None = None
    self._n_dofs = 0
    self._raw_action = None
    self._prev_action = None
    self._processed_action = None
    # Set once the first-step command-shape check has run (see ``verify_command_shape``).
    self._command_shape_checked = False

Attributes

position_scale class-attribute instance-attribute
position_scale: float | tuple[float, float, float] = 0.05
orientation_scale class-attribute instance-attribute
orientation_scale: float | tuple[float, float, float] = 0.2
position_kp class-attribute instance-attribute
position_kp: float = 1000.0
position_kd class-attribute instance-attribute
position_kd: float = 60.0
orientation_kp class-attribute instance-attribute
orientation_kp: float = 40.0
orientation_kd class-attribute instance-attribute
orientation_kd: float = 2.0
force_limit class-attribute instance-attribute
force_limit: float = 100.0
torque_limit class-attribute instance-attribute
torque_limit: float = 10.0
action_dim property
action_dim: int

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/free_base_actions.py
def build(self) -> None:
    if self.dofs_name:
        raise ValueError(
            f"{term_subject(self)} drives the entity's free joint, so it takes no "
            f"`dofs_name`; got {self.dofs_name!r}. Drive the other joints with a joint controller of their own."
        )
    gains = {
        "position_kp": self.position_kp,
        "position_kd": self.position_kd,
        "orientation_kp": self.orientation_kp,
        "orientation_kd": self.orientation_kd,
        "force_limit": self.force_limit,
        "torque_limit": self.torque_limit,
    }
    negative = {name: value for name, value in gains.items() if value < 0.0}
    if negative:
        raise ValueError(f"{term_subject(self)}: gains must be >= 0, got {negative}.")

    self._entity = self.resolve_entity(self.entity_name)
    entity: RigidEntity = self._entity
    # Raises, naming the joints, unless there is exactly one free joint: a fixed or attached base has none.
    base_dofs = entity.find_free_base_dofs_idx_local()
    self.dofs_idx_local = torch.as_tensor(base_dofs, dtype=gs.tc_int, device=self.device)
    self._n_dofs = len(base_dofs)

    self._position_scale = self._per_axis(self.position_scale, "position_scale")
    self._orientation_scale = self._per_axis(self.orientation_scale, "orientation_scale")

    kp = torch.tensor([self.position_kp] * 3 + [self.orientation_kp] * 3, dtype=gs.tc_float, device=self.device)
    kd = torch.tensor([self.position_kd] * 3 + [self.orientation_kd] * 3, dtype=gs.tc_float, device=self.device)
    limit = torch.tensor([self.force_limit] * 3 + [self.torque_limit] * 3, dtype=gs.tc_float, device=self.device)
    if entity.has_batched_dofs_info:
        kp, kd, limit = (value.expand(self.num_envs, -1) for value in (kp, kd, limit))
    entity.set_dofs_kp(kp, dofs_idx_local=self.genesis_dofs_idx)
    entity.set_dofs_kd(kd, dofs_idx_local=self.genesis_dofs_idx)
    entity.set_dofs_force_range(-limit, limit, dofs_idx_local=self.genesis_dofs_idx)
    if entity._needs_explicit_damping_armature:
        # The same first-order implicit damping Eden folds into an actuated joint's armature (see RigidEntity).
        armature = entity.get_dofs_armature(dofs_idx_local=self.genesis_dofs_idx)
        substep_dt = self._env.scene.sim._substep_dt
        entity.set_dofs_armature(
            (armature if entity.has_batched_dofs_info else armature[0]) + kd * substep_dt,
            dofs_idx_local=self.genesis_dofs_idx,
        )

    # Solver frame relative to the authored frame, in the authored frame: a rigid offset of the body, so reading
    # it wherever the entity happens to be is exact.
    authored_pos, authored_quat = entity.get_pos(relative=True), entity.get_quat(relative=True)
    self._offset_pos = inv_transform_by_quat(entity.get_pos() - authored_pos, authored_quat)
    self._offset_quat = quat_mul(inv_quat(authored_quat), entity.get_quat())
    self._default_pos, self._default_quat = entity.get_default_root_pose()

    self._raw_action = torch.zeros(self.num_envs, self.action_dim, device=self.device)
    self._prev_action = torch.zeros(self.num_envs, self.action_dim, device=self.device)
    self._processed_action = torch.zeros(self.num_envs, self._n_dofs, device=self.device)
target_pose
target_pose(actions: Tensor) -> tuple[Tensor, Tensor]

The authored-frame pose actions asks for: the default root pose, moved and then turned.

PARAMETER DESCRIPTION
actions

(num_envs, 6): translation, then rotation vector, in action units.

TYPE: Tensor

RETURNS DESCRIPTION
tuple[Tensor, Tensor]

Position (num_envs, 3) and wxyz quaternion (num_envs, 4).

Source code in eden/managers/terms/actions/free_base_actions.py
def target_pose(self, actions: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
    """The authored-frame pose ``actions`` asks for: the default root pose, moved and then turned.

    Parameters
    ----------
    actions : torch.Tensor
        ``(num_envs, 6)``: translation, then rotation vector, in action units.

    Returns
    -------
    tuple[torch.Tensor, torch.Tensor]
        Position ``(num_envs, 3)`` and wxyz quaternion ``(num_envs, 4)``.
    """
    pos = self._default_pos + actions[:, :3] * self._position_scale
    quat = quat_mul(quat_from_axis_angle(actions[:, 3:] * self._orientation_scale), self._default_quat)
    return pos, quat
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/free_base_actions.py
def compute(self, actions: torch.Tensor) -> None:
    self._prev_action[:] = self._raw_action
    self._raw_action[:] = actions
    pos, quat = self.target_pose(self._raw_action)
    solver_pos = pos + transform_by_quat(self._offset_pos, quat)
    solver_quat = quat_mul(quat, self._offset_quat)
    # A free joint's DOF positions are its position and its intrinsic xyz Euler angles.
    self._processed_action = torch.cat((solver_pos, quat_to_xyz(solver_quat, rpy=False)), dim=-1)
reset
reset(envs_idx: slice | Tensor | None = None) -> None
Source code in eden/managers/terms/actions/free_base_actions.py
def reset(self, envs_idx: slice | torch.Tensor | None = None) -> None:
    if envs_idx is None:
        envs_idx = slice(None)
    self._prev_action[envs_idx] = 0.0
    self._raw_action[envs_idx] = 0.0
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/free_base_actions.py
def apply_actions(self) -> None:
    self.verify_command_shape(self._processed_action)
    self.entity.control_dofs_pos(self._processed_action, dofs_idx_local=self.genesis_dofs_idx)

Functions:

Grippers

Binary (open/close) gripper action terms.

Attributes

GripperInput module-attribute

GripperInput = Literal['sign', 'open_sign', 'closedness', 'openness']

Classes

BinaryJointController

BinaryJointController(env: EnvBase, options: ActionTermOptions)

Bases: ActionTerm

A binary joint controller for a parallel-jaw gripper.

When the action is applied, the joints are opened or closed.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

dofs_name

The names of the DOFs to control.

open_action

The action to open the joints.

close_action

The action to close the joints.

scale

The scale to apply to the actions.

Note

action is a boolean tensor, True: close, False: open if float is used, >0: close, <=0: open

Source code in eden/managers/terms/actions/binary_actions.py
def __init__(self, env: EnvBase, options: ActionTermOptions):
    super().__init__(env=env, options=options)
    self._open_action: torch.Tensor | None = None
    self._close_action: torch.Tensor | None = None

Attributes

open_action class-attribute instance-attribute
open_action: float | dict[str, float] = 1.0
close_action class-attribute instance-attribute
close_action: float | dict[str, float] = 0.0
scale class-attribute instance-attribute
scale: float = 1.0
open_targets property
open_targets: Tensor

(1, num_dofs) joint targets sent to open the gripper.

Column i is dofs_name[i], driven at genesis_dofs_idx[i].

close_targets property
close_targets: Tensor

(1, num_dofs) joint targets sent to close the gripper.

Column i is dofs_name[i], driven at genesis_dofs_idx[i].

action_dim property
action_dim: int

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/binary_actions.py
def build(self) -> None:
    super().build()

    self._open_action = torch.zeros(1, self.num_dofs, device=self.device)
    self._close_action = torch.zeros(1, self.num_dofs, device=self.device)

    # Keys resolve to positions in this term's own `dofs_name` (the layout of the two rows), but a key that is
    # exactly a joint's name is literal against every joint of the entity: another term's or a passive
    # `pin.1` selects nothing here, rather than this term's `pin_1` as a regex.
    every_joint = [joint.name for joint in self.entity.joints]

    if isinstance(self.open_action, float):
        self._open_action[:] = self.open_action
    elif isinstance(self.open_action, dict):
        for dofs_name, open_action in self.open_action.items():
            term_idx, _ = resolve_matching_names(
                dofs_name, self.dofs_name, preserve_order=True, literal_names=every_joint
            )
            self._open_action[:, term_idx] = open_action
    else:
        raise ValueError(f"{term_subject(self)}: unsupported open_action type {type(self.open_action)}.")

    if isinstance(self.close_action, float):
        self._close_action[:] = self.close_action
    elif isinstance(self.close_action, dict):
        for dofs_name, close_action in self.close_action.items():
            term_idx, _ = resolve_matching_names(
                dofs_name, self.dofs_name, preserve_order=True, literal_names=every_joint
            )
            self._close_action[:, term_idx] = close_action
    else:
        raise ValueError(f"{term_subject(self)}: unsupported close_action type {type(self.close_action)}.")
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/binary_actions.py
def compute(self, actions: torch.Tensor) -> None:
    self._prev_action[:] = self._raw_action
    self._raw_action[:] = actions
    if actions.dtype == torch.bool:
        close_mask = actions
    else:
        close_mask = (actions * self.scale) > 0
    self._processed_action = (
        self._open_action.repeat(self.num_envs, 1) * (~close_mask)
        + self._close_action.repeat(self.num_envs, 1) * close_mask
    )
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/actions/binary_actions.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        envs_idx = slice(None)
    self._prev_action[envs_idx] = 0.0
    self._raw_action[envs_idx] = 0.0
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/binary_actions.py
def apply_actions(self) -> None:
    self.entity.control_dofs_pos(self._processed_action, dofs_idx_local=self.genesis_dofs_idx)

GripperDecisionController

GripperDecisionController(env: EnvBase, options: ActionTermOptions)

Bases: BinaryJointController

Gripper controller that decides open/close from a policy scalar.

Policy servers describe the gripper in different units and conventions (> 0 means close for some, open for others; a closedness or an openness in [0, 1] for others), often with noise that wants hysteresis, a hold, or a latch. This term takes the raw scalar and decides, so a client only slices its action vector. With the defaults it behaves exactly like :class:BinaryJointController.

PARAMETER DESCRIPTION
input

Meaning of the scalar: sign closes on > 0 (parent behaviour), open_sign opens on > 0, closedness / openness are [0, 1] amounts.

threshold

Binarisation point for closedness / openness: a closedness strictly above it closes; an openness strictly above it opens.

open_threshold

Hysteresis on closedness: once closed, open only below open_threshold; once open, close only above close_threshold. Set both to enable (they replace threshold); a value exactly on a threshold keeps the current state.

close_threshold

Hysteresis on closedness: once closed, open only below open_threshold; once open, close only above close_threshold. Set both to enable (they replace threshold); a value exactly on a threshold keeps the current state.

hold_steps

After a state change, the new state lasts at least this many decisions, counting the one that changed it (hysteresis mode only; 0 and 1 do not hold).

sticky_repeats

Fractal / Google-robot latch (SimplerEnv CogACT / SpatialVLA google_robot): the relative command is prev - openness (> 0 closes), with prev taken from the first command and updated only when the latch triggers. A jump of more than 0.5 triggers the latch, which repeats the jump for this many steps; otherwise the small relative command itself is sent. The command then drives ManiSkill's gripper_pd_joint_target_delta_pos controller: the target (in closedness) starts at the measured gripper, moves by 1.31 * clip(command, -1, 1) / 1.3 per step, clipped to the joint range [-0.0101, 1.31] / 1.3, and holds when the move is below 1e-3 / 1.3. The fingers follow that target continuously. Needs an openness / closedness input. 0 disables.

zero_holds

With a sign / open_sign input, a command of exactly 0 keeps the previous state instead of opening (robosuite's gripper steps by sign(action)).

continuous

Interpolate the DOF targets between open_action and close_action by the closedness instead of binarising; snap_threshold promotes closedness above it to fully closed.

snap_threshold

See continuous.

on_missing

What a NaN command (the server omitted the gripper) does: hold keeps the previous target; measured commands the gripper's measured closedness every such step, as the DROID Galaxea reference client does.

Notes

Under on_missing="hold", a NaN before any command was received is seeded from the measured DOF position (the gripper is where it physically is). In place of a NaN, :attr:action (and so the ActionManager's buffers) records the command acted on, in the input's units: that measured seed, or the held previous command. Per-env decision state resets with the env (reset(envs_idx)).

Source code in eden/managers/terms/actions/binary_actions.py
def __init__(self, env: EnvBase, options: ActionTermOptions):
    super().__init__(env=env, options=options)
    self._open_action: torch.Tensor | None = None
    self._close_action: torch.Tensor | None = None

Attributes

input class-attribute instance-attribute
input: GripperInput = 'sign'
threshold class-attribute instance-attribute
threshold: float = 0.5
open_threshold class-attribute instance-attribute
open_threshold: float | None = None
close_threshold class-attribute instance-attribute
close_threshold: float | None = None
hold_steps class-attribute instance-attribute
hold_steps: int = 0
sticky_repeats class-attribute instance-attribute
sticky_repeats: int = 0
zero_holds class-attribute instance-attribute
zero_holds: bool = False
continuous class-attribute instance-attribute
continuous: bool = False
snap_threshold class-attribute instance-attribute
snap_threshold: float | None = None
on_missing class-attribute instance-attribute
on_missing: Literal['hold', 'measured'] = 'hold'
last_decision property
last_decision: dict[str, Any] | None

Env 0's last decision, for debugging (reading it syncs with the device).

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/binary_actions.py
def build(self) -> None:
    super().build()
    if self.input not in ("sign", "open_sign", "closedness", "openness"):
        raise ValueError(
            f"{term_subject(self)}: input={self.input!r}; expected sign, open_sign, closedness or openness."
        )
    if (self.open_threshold is None) != (self.close_threshold is None):
        raise ValueError(f"{term_subject(self)}: set both open_threshold and close_threshold or neither.")
    if self.open_threshold is not None and self.close_threshold < self.open_threshold:
        raise ValueError(
            f"{term_subject(self)}: close_threshold ({self.close_threshold}) must be >= "
            f"open_threshold ({self.open_threshold})."
        )
    if self.hold_steps < 0 or self.sticky_repeats < 0:
        raise ValueError(f"{term_subject(self)}: hold_steps and sticky_repeats must be non-negative.")
    if self.continuous and self.input in ("sign", "open_sign"):
        raise ValueError(f"{term_subject(self)}: continuous mode needs a closedness or openness input.")
    if self.snap_threshold is not None and not 0.0 <= self.snap_threshold <= 1.0:
        raise ValueError(f"{term_subject(self)}: snap_threshold must be in [0, 1].")
    if self.on_missing not in ("hold", "measured"):
        raise ValueError(f"{term_subject(self)}: on_missing={self.on_missing!r}; expected hold or measured.")
    # Settings that the selected mode would silently ignore.
    hysteresis = self.open_threshold is not None
    if self.hold_steps > 0 and not hysteresis:
        raise ValueError(
            f"{term_subject(self)}: hold_steps only applies with open_threshold/close_threshold (hysteresis)."
        )
    if hysteresis and self.input in ("sign", "open_sign"):
        raise ValueError(
            f"{term_subject(self)}: hysteresis thresholds need a closedness or openness input, not {self.input!r}."
        )
    if self.snap_threshold is not None and not self.continuous:
        raise ValueError(f"{term_subject(self)}: snap_threshold only applies with continuous=True.")
    if self.continuous and (hysteresis or self.sticky_repeats > 0):
        raise ValueError(
            f"{term_subject(self)}: continuous mode cannot be combined with hysteresis or sticky_repeats."
        )
    if self.sticky_repeats > 0 and (hysteresis or self.input in ("sign", "open_sign")):
        raise ValueError(
            f"{term_subject(self)}: sticky_repeats needs an openness/closedness input and no hysteresis."
        )
    if self.zero_holds and self.input not in ("sign", "open_sign"):
        raise ValueError(f"{term_subject(self)}: zero_holds only applies to a sign or open_sign input.")
    if self.on_missing == "measured" and not self.continuous:
        # Re-deciding from a stalled grasp's measured position would re-binarize (or re-latch) it open.
        raise ValueError(f"{term_subject(self)}: on_missing='measured' needs continuous=True.")
    n = self.num_envs
    dev = self.device
    self._closed = torch.zeros(n, dtype=torch.bool, device=dev)  # starts open
    self._hold_remaining = torch.zeros(n, dtype=torch.long, device=dev)
    self._sticky_prev = torch.full((n,), float("nan"), device=dev)  # unset until the first command
    self._sticky_on = torch.zeros(n, dtype=torch.bool, device=dev)
    self._sticky_count = torch.zeros(n, dtype=torch.long, device=dev)
    self._sticky_action = torch.zeros(n, device=dev)
    self._sticky_target = torch.zeros(n, device=dev)  # delta-controller target, in closedness
    self._seen_command = torch.zeros(n, dtype=torch.bool, device=dev)
    self._last_processed = self._open_action.repeat(n, 1).clone()
    # Fixed at build time; resolved once so the per-step measured read never syncs with the device.
    self._measure_span_zero = bool(torch.isclose(self._open_action[0, 0], self._close_action[0, 0]))
    self.last_closedness = torch.zeros(n, device=dev)
    self._last_raw: torch.Tensor | None = None
    self._last_valid: torch.Tensor | None = None
compute
compute(actions: Tensor) -> None
Source code in eden/managers/terms/actions/binary_actions.py
def compute(self, actions: torch.Tensor) -> None:
    if actions.dtype == torch.bool:
        actions = actions.float() * 2.0 - 1.0  # True -> +1 (close), False -> -1 (open) in sign terms
        if self.input != "sign":
            raise ValueError(f"{term_subject(self)}: boolean commands are only valid with input='sign'.")
    raw = actions[:, 0].to(torch.float32)
    self._prev_action[:] = self._raw_action
    valid = ~torch.isnan(raw)
    missing = ~valid

    # Missing command: the gripper is wherever it physically is (first miss, or every miss
    # under on_missing="measured"). Computed unconditionally to avoid a host sync per step.
    seed = ~valid if self.on_missing == "measured" else ~valid & ~self._seen_command
    measured = self._measured_closedness()
    if self.input == "closedness":
        seeded = measured
    elif self.input == "openness":
        seeded = 1.0 - measured
    else:  # sign conventions: raw * scale > 0 means close for "sign", open for "open_sign"
        closed_sign = torch.where(measured > 0.5, torch.ones_like(raw), -torch.ones_like(raw))
        closed_sign = closed_sign if self.scale >= 0 else -closed_sign
        seeded = closed_sign if self.input == "sign" else -closed_sign
    raw = torch.where(seed, seeded, raw)
    valid = valid | seed
    self._seen_command |= valid
    # Record the command acted on, in the input's units: the seeded measured value, else (a held
    # miss) the previous command. The ActionManager stores this in place of the NaN
    # (_accepts_missing_command_), so last-action observations and recorders see what was done.
    self._raw_action[:, 0] = torch.where(missing & ~seed, self._prev_action[:, 0], raw)

    closedness = self._closedness_from_input(raw)
    if self.sticky_repeats > 0:
        cmd = torch.where(valid, self._sticky_step(closedness, measured, valid), self.last_closedness)
        processed = self._open_action + (self._close_action - self._open_action) * cmd.unsqueeze(-1)
        self._closed = cmd > 0.5
    elif self.continuous:
        cmd = closedness.clamp(0.0, 1.0)
        if self.snap_threshold is not None:
            cmd = torch.where(cmd > self.snap_threshold, torch.ones_like(cmd), cmd)
        cmd = torch.where(valid, cmd, self.last_closedness)
        processed = self._open_action + (self._close_action - self._open_action) * cmd.unsqueeze(-1)
        self._closed = cmd > 0.5
    else:
        self._closed = self._decide(raw, closedness, valid)
        cmd = self._closed.float()
        mask = self._closed.unsqueeze(-1)
        processed = torch.where(
            mask, self._close_action.expand(self.num_envs, -1), self._open_action.expand(self.num_envs, -1)
        )
    processed = torch.where(valid.unsqueeze(-1), processed, self._last_processed)
    self._last_processed = processed.clone()
    self.last_closedness = cmd
    self._processed_action = processed
    self._last_raw = raw
    self._last_valid = valid
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/actions/binary_actions.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    super().reset(envs_idx)
    idx = slice(None) if envs_idx is None else envs_idx
    self._closed[idx] = False
    self._hold_remaining[idx] = 0
    self._sticky_prev[idx] = float("nan")
    self._sticky_on[idx] = False
    self._sticky_count[idx] = 0
    self._sticky_action[idx] = 0.0
    self._sticky_target[idx] = 0.0
    self._seen_command[idx] = False
    self._last_processed[idx] = self._open_action
    self.last_closedness[idx] = 0.0

Functions:

Welding

Welding action terms for suction-cup and parallel-jaw attach/detach.

Attributes

Classes

SuctionCupWelding

SuctionCupWelding(env: EnvBase, options: ActionTermOptions)

Bases: _WeldingBase

Collision-aware welding of an object link to the suction-cup end-effector link.

When the action is applied and the end-effector link is in contact with an object link, the end-effector link and the object link are welded together.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control.

ee_link_name

The name of the end-effector link.

ignore_entities_name

The names of the entities to exclude from welding (e.g., "table", "floor").

ignore_links_idx

The rigid-solver (scene-global) link indices to exclude from welding, e.g. env.entities["table"].get_link("top").idx; an entity-local idx_local excludes the wrong link.

Note

action is a boolean tensor, True: suction, False: release if float is used, >0: suction, <=0: release

Source code in eden/managers/terms/actions/welding_actions.py
def __init__(self, env: EnvBase, options: ActionTermOptions):
    ConfigurableTermBase.__init__(self, env=env, options=options)

    assert not self.dofs_name, "This ActionTerm does not require any dofs for control."

    self._entity = None
    self.dofs_name = []
    self.dofs_idx_local = torch.tensor([], dtype=gs.tc_int, device=self.device)
    self._n_dofs = 0

    self._raw_action: torch.Tensor | None = None
    self._prev_action: torch.Tensor | None = None
    self._processed_action: torch.Tensor | None = None

    self._ee_link_idx = None
    self._welded = None
    self._ignore_links_idx: torch.Tensor | None = None

Attributes

action_dim property
action_dim: int

Methods:

apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/welding_actions.py
def apply_actions(self) -> None:
    contact_info = self._entity.get_contacts(exclude_self_contact=True)
    link_a = contact_info["link_a"]
    link_b = contact_info["link_b"]
    valid_mask = contact_info["valid_mask"]
    # NOTE: get object links in contact
    objects_link_idx = torch.unique(
        torch.cat(
            [
                link_a[(link_b == self._ee_link_idx) & valid_mask],
                link_b[(link_a == self._ee_link_idx) & valid_mask],
            ]
        )
    )
    suction_mask = self._processed_action & ~self._welded
    if not suction_mask.any():
        return
    for link_idx in self._weldable(objects_link_idx):
        # NOTE: only add weld constraint if the object is not welded and the action is true
        envs_weld = suction_mask & (
            ((link_a == link_idx) & (link_b == self._ee_link_idx) & valid_mask).any(dim=1)
            | ((link_b == link_idx) & (link_a == self._ee_link_idx) & valid_mask).any(dim=1)
        )
        if not envs_weld.any():
            continue
        self._env.rigid_solver.add_weld_constraint(link_idx, self._ee_link_idx, envs_idx=envs_weld)
        self._welded[envs_weld] = True

ParallelJawWelding

ParallelJawWelding(env: EnvBase, options: ActionTermOptions)

Bases: _WeldingBase

Weld the link a parallel-jaw gripper is squeezing to the end-effector link.

While the action is on, an env with no weld welds a link to ee_link_name once both pads press that same link with contact force along the closing direction above grasp_force_threshold (:func:~eden.managers.terms.utils.parallel_jaw_is_grasping with object_link_idx). A link touching only one pad is never welded, and neither are two links each pressed by one pad: a grasp that spans two links of a multi-link object welds nothing. Releasing the action deletes the weld. An env holds at most one weld; if several links pass in the same step, the one with the lowest solver index is welded.

The force test is sign-agnostic, so both pads scraping the same surface while closing can pass it: list support surfaces (table, floor) in ignore_entities_name.

PARAMETER DESCRIPTION
entity_name

The name of the entity to control. (e.g., "robot")

ee_link_name

The name of the end-effector link. (e.g., "hand")

ignore_entities_name

The names of the entities to exclude from welding (e.g., "table", "floor").

ignore_links_idx

The rigid-solver (scene-global) link indices to exclude from welding, e.g. env.entities["table"].get_link("top").idx; an entity-local idx_local excludes the wrong link.

left_gripper_link_name

The name of the left gripper link. (e.g., "left_gripper_pad")

right_gripper_link_name

The name of the right gripper link. (e.g., "right_gripper_pad")

grasp_force_threshold

Minimum projected contact force (N) on each pad from the candidate link; >= 0.

Source code in eden/managers/terms/actions/welding_actions.py
def __init__(self, env: EnvBase, options: ActionTermOptions):
    super().__init__(env=env, options=options)
    self._left_gripper_link_idx: int = -1
    self._right_gripper_link_idx: int = -1
    self._gripper_idx_local: list[int] = [-1, -1]

Attributes

left_gripper_link_name: str = ''
right_gripper_link_name: str = ''
grasp_force_threshold class-attribute instance-attribute
grasp_force_threshold: float = 1.0
action_dim property
action_dim: int

Methods:

build
build() -> None
Source code in eden/managers/terms/actions/welding_actions.py
def build(self) -> None:
    super().build()
    if not self.grasp_force_threshold >= 0.0:
        # The force test compares |projected force| > threshold, so a negative threshold passes every
        # contact, and NaN passes none: either way the weld no longer depends on a grasp.
        raise ValueError(
            f"{term_subject(self)}: grasp_force_threshold={self.grasp_force_threshold!r} must be >= 0 "
            "(newtons of contact force each pad must carry from the squeezed link). Use 0.0 to weld "
            "on any two-pad contact with that link."
        )

    self._left_gripper_link_idx = self._entity.get_link(self.left_gripper_link_name).idx
    self._right_gripper_link_idx = self._entity.get_link(self.right_gripper_link_name).idx
    self._gripper_idx_local = [
        self._entity.get_link(self.left_gripper_link_name).idx_local,
        self._entity.get_link(self.right_gripper_link_name).idx_local,
    ]
apply_actions
apply_actions() -> None
Source code in eden/managers/terms/actions/welding_actions.py
def apply_actions(self) -> None:
    # Only an env whose action is on and that holds no weld yet can weld this step.
    if not (self._processed_action & ~self._welded).any():
        return
    contact_info = self._entity.get_contacts(exclude_self_contact=True)
    link_a = contact_info["link_a"]
    link_b = contact_info["link_b"]
    valid_mask = contact_info["valid_mask"]

    # Candidates: every external link either pad touches. The grasp is checked per candidate below,
    # from that link's contacts alone, so contacts with different links never add up to one grasp.
    pad_a = (link_a == self._left_gripper_link_idx) | (link_a == self._right_gripper_link_idx)
    pad_b = (link_b == self._left_gripper_link_idx) | (link_b == self._right_gripper_link_idx)
    objects_link_idx = torch.unique(torch.cat([link_a[pad_b & valid_mask], link_b[pad_a & valid_mask]]))
    for link_idx in self._weldable(objects_link_idx):
        # Both pads carry force above the threshold (>= 0, checked in build) from this link, which
        # implies both touch it. Re-read _welded each time: an env welds at most one link per step.
        squeezed = parallel_jaw_is_grasping(
            self._entity,
            contact_info,
            self._left_gripper_link_idx,
            self._right_gripper_link_idx,
            self._gripper_idx_local,
            self.grasp_force_threshold,
            object_link_idx=link_idx,
        )
        envs_weld = self._processed_action & ~self._welded & squeezed
        if not envs_weld.any():
            continue

        self._env.rigid_solver.add_weld_constraint(link_idx, self._ee_link_idx, envs_idx=envs_weld)
        self._welded[envs_weld] = True

Functions:

See also