Skip to content

Reward Terms

Reward terms return one value per env per step; the reward manager multiplies each by its weight, sums them and scales the sum by the control step dt. The rewards and terminations guide explains weights, groups and logging; the term catalog lists every parameter.

from eden import rewards

The base classes RewardTerm and RewardTermFuncWrapper are on the managers page.

Common penalties

Action rate and magnitude, joint-limit, torque and contact penalties.

Common reward/penalty terms (action rate/magnitude, joint limits, torques, contacts, metric and success rewards).

Attributes

Classes

UndesiredContacts

UndesiredContacts(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalty count of links whose net contact-force magnitude exceeds threshold.

links_name accepts the same regex / glob patterns as :func:eden.utils.string.resolve_matching_names. Use a negative reward weight to penalise (e.g. -0.1 per offending link).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
links_name: list[str] = []
threshold class-attribute instance-attribute
threshold: float = 1.0

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/common.py
def build(self) -> None:
    entity = self.resolve_entity(self.entity_name)
    # Name-resolved link idx_local (matches the internal order get_links_net_contact_force
    # returns), so this stays correct across Genesis link reorders (BFS->DFS in 1.2.0).
    self._link_idx = resolve_link_idx(entity, self.links_name, self.device, term=type(self).__name__)
compute
compute(envs_idx: Tensor | None = None) -> Tensor
Source code in eden/managers/terms/rewards/common.py
def compute(self, envs_idx: torch.Tensor | None = None) -> torch.Tensor:
    entity = self.resolve_entity(self.entity_name)
    forces = entity.get_links_net_contact_force()  # (N, n_links, 3)
    magnitudes = torch.norm(forces[:, self._link_idx], dim=-1)  # (N, K)
    return (magnitudes > self.threshold).float().sum(dim=-1)

ActionRateL2Smooth

ActionRateL2Smooth(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize the 1st and 2nd derivatives of the actions using an L2 squared kernel.

The 2nd-derivative term needs the action from two steps ago. This term owns that buffer (the previous step's prev_action), zeroed per env on reset.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

action_term_name class-attribute instance-attribute
action_term_name: str | None = None

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/common.py
def build(self) -> None:
    # The action manager is built before the reward manager (``EnvBase._manager_order``), so
    # both the handle and the action width are known here: an unknown ``action_term_name``
    # raises at env construction, and the buffer is sized once against the real shape instead
    # of on the first ``compute()``.
    _, prev_action = self._action_and_prev()
    # ``inference_mode(False)`` states the invariant rather than inheriting it from whoever
    # called ``env.build()``: the buffer must be a *normal* tensor, because ``compute()`` runs
    # under ``RewardManager.compute``'s ``torch.inference_mode`` while ``reset()`` may be
    # called outside it (`eden smoke`'s plain step loop), and mutating an inference tensor
    # from a normal path raises.
    with torch.inference_mode(False):
        self._prev_prev_action: torch.Tensor = torch.zeros_like(prev_action)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/common.py
def compute(self) -> torch.Tensor:
    action, prev_action = self._action_and_prev()
    out = _compute_action_rate_l2_smooth(action, prev_action, self._prev_prev_action)
    # This step's ``prev_action`` is two-steps-old next step.
    self._prev_prev_action.copy_(prev_action)
    return out
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/rewards/common.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        self._prev_prev_action.zero_()
    else:
        self._prev_prev_action[envs_idx] = 0.0

DofsAccL2

DofsAccL2(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize joint accelerations approximated as velocity differences.

Computes sum((vel - prev_vel)^2) where prev_vel is the joint velocity from the previous step; this term owns that buffer, zeroed per env on reset. The config weight should account for the 1/dt^2 factor compared to raw simulator accelerations.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/common.py
def build(self) -> None:
    # Entities exist before the reward manager is built, so the buffer is
    # allocated here against the live joint-velocity shape.
    entity: Entity = self.resolve_entity(self.entity_name)
    self._prev_dofs_vel = torch.zeros_like(entity.get_dofs_vel())
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/common.py
def compute(self) -> torch.Tensor:
    entity: Entity = self.resolve_entity(self.entity_name)
    dofs_vel = entity.get_dofs_vel()
    out = _compute_dofs_acc_l2(dofs_vel - self._prev_dofs_vel)
    self._prev_dofs_vel.copy_(dofs_vel)
    return out
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/rewards/common.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        self._prev_dofs_vel.zero_()
    else:
        self._prev_dofs_vel[envs_idx] = 0.0

MetricReward

MetricReward(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Use a :class:~eden.managers.metric_manager.MetricTerm's cached value as the reward.

Reads the per-env value the metric manager already computed this step (metric_manager runs before the reward manager in env.step()), so a quantity shared across reward / observation / termination terms is computed once as a metric and pulled here by name — instead of being recomputed in every term or stashed on the env object. The configured reward weight scales it as usual (use a negative weight for a penalty).

Rejects a cumulative metric by default: its value is the running episode sum, so using it directly as a per-step reward re-integrates that sum and balloons over the episode — almost never intended. Pass allow_cumulative=True to opt in if you really want the running sum as the reward.

A missing metric manager, an unset or unknown metric_name (the error lists the active metrics) and a rejected cumulative metric all raise in :meth:build, i.e. at env construction.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

metric_name class-attribute instance-attribute
metric_name: str = ''
allow_cumulative class-attribute instance-attribute
allow_cumulative: bool = False

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/common.py
def build(self) -> None:
    metric_manager = resolve_metric_manager(self._env, term="metric_reward", metric_name=self.metric_name)
    if not self.allow_cumulative and metric_manager.is_cumulative(self.metric_name):
        raise ValueError(
            f"metric_reward(metric_name='{self.metric_name}') pulls a cumulative metric, whose value is the "
            f"running episode sum — using it as a per-step reward integrates that sum again and is almost never "
            f"intended. Use a non-cumulative metric, or pass allow_cumulative=True to opt into the running sum as "
            f"the reward."
        )
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/common.py
def compute(self) -> torch.Tensor:
    return self._env.metric_manager.get_value(self.metric_name)

TaskSuccess

TaskSuccess(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

1 on a step where every success metric holds.

The bonus for finishing, the reward counterpart of the on_success termination. Dense progress terms pay little for the last few percent of a task (the last cells of a stain, the final centimetres of a placement), so without it a policy settles just short of the success threshold. Pays metric_manager.success_buf, and raises in :meth:build when the task declares no per-step (INTERVAL) metric that contributes to success, since that buffer then never turns True.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/common.py
def build(self) -> None:
    metric_manager = self._env.metric_manager
    if metric_manager is None or metric_manager.num_step_success_terms == 0:
        raise ValueError(
            "task_success rewards the steps where every success metric holds, but this task declares no "
            "INTERVAL metric that contributes to success (no metric_options, or every metric is RESET-mode "
            "or has contributes_to_success=False). Declare the task's success metrics, or reward a metric's "
            "own value with metric_reward."
        )
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/common.py
def compute(self) -> torch.Tensor:
    return self._env.metric_manager.success_buf.float()

Functions:

action_rate_l2

action_rate_l2(env: EnvBase, *, action_term_name: str | None = None) -> Tensor

Penalize the rate of change of the actions using L2 squared kernel.

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def action_rate_l2(
    env: EnvBase,
    *,
    action_term_name: str | None = None,
) -> torch.Tensor:
    """Penalize the rate of change of the actions using L2 squared kernel."""
    if action_term_name is not None:
        action_term = env.action_manager.get_term(action_term_name)
        return _compute_action_rate_l2(action_term.action, action_term.prev_action)
    return _compute_action_rate_l2(env.action_manager.action, env.action_manager.prev_action)

action_l2

action_l2(env: EnvBase, *, action_term_name: str | None = None) -> Tensor

Penalize the actions using L2 squared kernel.

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def action_l2(
    env: EnvBase,
    *,
    action_term_name: str | None = None,
) -> torch.Tensor:
    """Penalize the actions using L2 squared kernel."""
    if action_term_name is not None:
        action_term = env.action_manager.get_term(action_term_name)
        return _compute_action_l2(action_term.action)
    return _compute_action_l2(env.action_manager.action)

dofs_pos_limits

dofs_pos_limits(env: EnvBase, *, entity_name: str) -> Tensor

Penalize joint positions if they cross the soft limits.

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def dofs_pos_limits(
    env: EnvBase,
    *,
    entity_name: str,
) -> torch.Tensor:
    """Penalize joint positions if they cross the soft limits."""
    entity: Entity = env.get_entity(entity_name)
    dofs_pos = entity.get_dofs_pos()
    return _compute_dofs_pos_limits(dofs_pos, entity.soft_dofs_pos_limits)

dofs_vel_limits

dofs_vel_limits(env: EnvBase, *, entity_name: str, max_vel: float | Tensor) -> Tensor

Quadratic hinge penalty on joint velocities exceeding a symmetric limit.

Penalizes only the amount by which |v| exceeds max_vel. Returns the (non-negative) squared L2 norm of the excess velocities; use a negative weight to penalise.

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def dofs_vel_limits(
    env: EnvBase,
    *,
    entity_name: str,
    max_vel: float | torch.Tensor,
) -> torch.Tensor:
    """Quadratic hinge penalty on joint velocities exceeding a symmetric limit.

    Penalizes only the amount by which |v| exceeds max_vel. Returns the (non-negative) squared L2
    norm of the excess velocities; use a negative weight to penalise.
    """
    entity: Entity = env.get_entity(entity_name)
    dofs_vel = entity.get_dofs_vel()
    return _compute_dofs_vel_limits(dofs_vel, max_vel)

self_collision_cost

self_collision_cost(env: EnvBase, *, entity_name: str) -> Tensor

Penalize self collisions.

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def self_collision_cost(
    env: EnvBase,
    *,
    entity_name: str,
) -> torch.Tensor:
    """Penalize self collisions."""
    entity: Entity = env.get_entity(entity_name)
    contacts = entity.get_contacts(with_entity=entity, exclude_self_contact=False)
    return contacts["valid_mask"].sum(dim=1)

dofs_torques

dofs_torques(env: EnvBase, *, entity_name: str) -> Tensor

Penalize joint torques: sqrt(sum(tau^2)) + sum(|tau|).

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def dofs_torques(
    env: EnvBase,
    *,
    entity_name: str,
) -> torch.Tensor:
    """Penalize joint torques: sqrt(sum(tau^2)) + sum(|tau|)."""
    entity: Entity = env.get_entity(entity_name)
    torques = entity.get_dofs_force()
    return _compute_dofs_torques(torques)

similar_to_default

similar_to_default(env: EnvBase, *, entity_name: str) -> Tensor

Penalize joint poses far away from the entity's default pose.

Source code in eden/managers/terms/rewards/common.py
@REWARD_TERM_REGISTRY.register()
def similar_to_default(env: EnvBase, *, entity_name: str) -> torch.Tensor:
    """Penalize joint poses far away from the entity's default pose."""
    robot = env.get_entity(entity_name)
    return torch.sum(torch.abs(robot.get_dofs_pos() - robot.default_dofs_pos), dim=1)

Locomotion

Reward terms for locomotion and velocity tracking tasks.

Most terms here are stateful class terms: they resolve their entity/command refs, link indices, and constant tensors (gravity, per-joint std, pose weights) once in :meth:build and reuse them every step, never stashing caches on the env object. Trivial penalties with nothing to cache are plain functions.

Attributes

Classes

TrackingLinVel

TrackingLinVel(env: EnvBase, options: _TermOptionsT)

Bases: _VelocityTracking

Tracking of linear velocity commands (xy axes).

Body frame: exp(-||cmd_xy - v_xy||^2 / sigma); sigma divides the squared error directly, so it is a variance, not a std.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Methods:

compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    base_lin_vel = self._robot.get_vel(frame="body")
    return _compute_tracking_lin_vel(self._command.command[:, :2], base_lin_vel[:, :2], self.sigma)

TrackingAngVel

TrackingAngVel(env: EnvBase, options: _TermOptionsT)

Bases: _VelocityTracking

Tracking of angular velocity commands (yaw).

Body frame: exp(-(cmd_wz - w_z)^2 / sigma).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Methods:

compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    base_ang_vel = self._robot.get_ang(frame="body")
    return _compute_tracking_ang_vel(self._command.command[:, 2], base_ang_vel[:, 2], self.sigma)

FlatOrientationL2

FlatOrientationL2(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize non-flat base orientation (projected gravity deviation in xy).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._grav = gravity_vec(self.num_envs, self.device)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    grav_in_base = projected_gravity(self._robot.get_quat(), self._grav)
    return torch.sum(torch.square(grav_in_base[:, :2]), dim=1)

Upright

Upright(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Reward for keeping a body link upright.

Projects gravity into the link's frame and returns exp(-sum(gravity_xy^2) / std^2) (matches mjlab's upright). Shared by the stand and velocity-tracking tasks. The link index and the constant world-frame gravity vector are resolved once in :meth:build.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
link_name: str = ''
std class-attribute instance-attribute
std: float = 1.0

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    # A one-link `range`, not a one-element tensor: Genesis reads the latter back with `.item()`,
    # a sync per call.
    self._ls_idx = as_genesis_index(resolve_link_idx(self._robot, [self.link_name], self.device))
    self._grav = gravity_vec(self.num_envs, self.device)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    link_quat = self._robot.get_links_quat(ls_idx_local=self._ls_idx).squeeze(1)  # (num_envs, 4)
    grav_in_link = projected_gravity(link_quat, self._grav)
    xy_squared = torch.sum(torch.square(grav_in_link[:, :2]), dim=1)
    return torch.exp(-xy_squared / (self.std**2))

BodyAngVelXYPenalty

BodyAngVelXYPenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize body angular velocity in xy (allow z rotation).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
link_name: str = ''

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    # A one-link `range`, not a one-element tensor: Genesis reads the latter back with `.item()`,
    # a sync per call.
    self._ls_idx = as_genesis_index(resolve_link_idx(self._robot, [self.link_name], self.device))
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    ang_vel = self._robot.get_links_ang(ls_idx_local=self._ls_idx)  # (num_envs, 1, 3)
    ang_vel = ang_vel.squeeze(1)  # (num_envs, 3)
    return torch.sum(torch.square(ang_vel[:, :2]), dim=1)

VariablePosture

VariablePosture(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Speed-dependent posture reward.

Uses per-joint standard deviations to control how much each joint can deviate from default pose. The reward is exp(-mean(error^2 / std^2)). Three speed regimes select the std set:

  • standing (speed < walking_threshold)
  • walking (walking_threshold <= speed < running_threshold)
  • running (speed >= running_threshold)

Each std dict must cover every DOF of the robot (a ".*" entry is the explicit "everything else" catch-all); DOFs no pattern matches raise at :meth:build instead of silently training against a default tolerance.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
command_name class-attribute instance-attribute
command_name: str = ''
std_standing class-attribute instance-attribute
std_standing: dict[str, float] = {}
std_walking class-attribute instance-attribute
std_walking: dict[str, float] = {}
std_running class-attribute instance-attribute
std_running: dict[str, float] = {}
walking_threshold class-attribute instance-attribute
walking_threshold: float = 0.1
running_threshold class-attribute instance-attribute
running_threshold: float = 1.5

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._command = self._env.command_manager.get_term(self.command_name)
    term = type(self).__name__
    self._std_standing = resolve_per_dof_std(
        self.std_standing, self._robot, self.device, field=f"{term}.std_standing"
    )
    self._std_walking = resolve_per_dof_std(self.std_walking, self._robot, self.device, field=f"{term}.std_walking")
    self._std_running = resolve_per_dof_std(self.std_running, self._robot, self.device, field=f"{term}.std_running")
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    command = self._command.command
    linear_speed = torch.norm(command[:, :2], dim=1)
    angular_speed = torch.abs(command[:, 2])
    total_speed = linear_speed + angular_speed

    standing_mask = (total_speed < self.walking_threshold).float()
    walking_mask = ((total_speed >= self.walking_threshold) & (total_speed < self.running_threshold)).float()
    running_mask = (total_speed >= self.running_threshold).float()

    std = (
        self._std_standing * standing_mask.unsqueeze(1)
        + self._std_walking * walking_mask.unsqueeze(1)
        + self._std_running * running_mask.unsqueeze(1)
    )

    current_dofs_pos = self._robot.get_dofs_pos()
    error_squared = torch.square(current_dofs_pos - self._robot.default_dofs_pos)
    return torch.exp(-torch.mean(error_squared / (std**2), dim=1))

FeetClearance

FeetClearance(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize deviation from target clearance height, weighted by foot xy velocity.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
target_height class-attribute instance-attribute
target_height: float = 0.0
command_name class-attribute instance-attribute
command_name: str = ''
command_threshold class-attribute instance-attribute
command_threshold: float = 0.1

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._command = self._env.command_manager.get_term(self.command_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    foot_pos = self._robot.get_links_pos(ls_idx_local=self._ls_idx)  # (num_envs, N, 3)
    foot_vel = self._robot.get_links_vel(ls_idx_local=self._ls_idx)  # (num_envs, N, 3)

    foot_z = foot_pos[:, :, 2]  # (num_envs, N)
    vel_norm = torch.norm(foot_vel[:, :, :2], dim=-1)  # (num_envs, N)
    delta = torch.abs(foot_z - self.target_height)
    cost = torch.sum(delta * vel_norm, dim=1)

    command = self._command.command
    total_command = torch.norm(command[:, :2], dim=1) + torch.abs(command[:, 2])
    active = (total_command > self.command_threshold).float()
    return torch.mul(cost, active)

FeetSlip

FeetSlip(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize foot sliding (xy velocity while in contact with ground).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
command_name class-attribute instance-attribute
command_name: str = ''
command_threshold class-attribute instance-attribute
command_threshold: float = 0.1
contact_height_threshold class-attribute instance-attribute
contact_height_threshold: float = 0.05

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._command = self._env.command_manager.get_term(self.command_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    foot_pos = self._robot.get_links_pos(ls_idx_local=self._ls_idx)  # (num_envs, N, 3)
    foot_vel = self._robot.get_links_vel(ls_idx_local=self._ls_idx)  # (num_envs, N, 3)

    # Simple ground contact check: foot z below threshold
    in_contact = (foot_pos[:, :, 2] < self.contact_height_threshold).float()  # (num_envs, N)
    vel_xy_norm_sq = torch.sum(torch.square(foot_vel[:, :, :2]), dim=-1)  # (num_envs, N)
    cost = torch.sum(vel_xy_norm_sq * in_contact, dim=1)

    command = self._command.command
    total_command = torch.norm(command[:, :2], dim=1) + torch.abs(command[:, 2])
    active = (total_command > self.command_threshold).float()
    return torch.mul(cost, active)

FeetAirTime

FeetAirTime(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Legged-gym style feet air-time reward for aperiodic gaits.

Tracks per-foot air time; on the control step a foot first makes contact it credits (air_time - target_s) (summed over feet), gated to when a movement command is active. Rewards proper swing cycles, so it discourages a leg that stays permanently planted or permanently airborne ("dragging" limps). Use a POSITIVE weight.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
command_name class-attribute instance-attribute
command_name: str = ''
feet_link_names: list[str] = []
target_s class-attribute instance-attribute
target_s: float = 0.35
contact_force_threshold class-attribute instance-attribute
contact_force_threshold: float = 2.0
command_threshold class-attribute instance-attribute
command_threshold: float = 0.1

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._command = self._env.command_manager.get_term(self.command_name)
    self._feet_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
    n = self._feet_idx.numel()
    self._air_time = torch.zeros(self.num_envs, n, device=self.device)
    self._last_contacts = torch.zeros(self.num_envs, n, dtype=torch.bool, device=self.device)
    self._dt = float(self._env.dt)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    forces = self.resolve_entity(self.entity_name).get_links_net_contact_force()[:, self._feet_idx]
    contact = torch.norm(forces, dim=-1) > self.contact_force_threshold
    contact_filt = contact | self._last_contacts
    self._last_contacts.copy_(contact)  # copy_ (not rebind): compute may run under inference_mode
    first_contact = (self._air_time > 0.0) & contact_filt
    self._air_time.add_(self._dt)
    reward = torch.sum((self._air_time - self.target_s) * first_contact.float(), dim=1)
    command = self._command.command
    total_command = torch.norm(command[:, :2], dim=1) + torch.abs(command[:, 2])
    active = (total_command > self.command_threshold).float()
    self._air_time.mul_((~contact_filt).float())  # reset air time on (filtered) contact
    return reward * active
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/rewards/locomotion.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        self._air_time.zero_()
        self._last_contacts.zero_()
    else:
        self._air_time[envs_idx] = 0.0
        self._last_contacts[envs_idx] = False

FeetContactBalancePenalty

FeetContactBalancePenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize uneven foot usage (a limp) via the variance of per-foot contact duty.

Keeps an EMA of each foot's contact state and returns the variance across the feet: a healthy gait uses all legs (~equal duty) -> low variance; a leg that is always planted or always in the air spikes the variance. Complements a mirror / symmetry loss by also catching front/rear imbalance. Use a NEGATIVE weight.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
contact_force_threshold class-attribute instance-attribute
contact_force_threshold: float = 2.0
ema_alpha class-attribute instance-attribute
ema_alpha: float = 0.02

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._feet_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
    self._ema = torch.full((self.num_envs, self._feet_idx.numel()), 0.5, device=self.device)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    forces = self.resolve_entity(self.entity_name).get_links_net_contact_force()[:, self._feet_idx]
    contact = (torch.norm(forces, dim=-1) > self.contact_force_threshold).float()
    self._ema.mul_(1.0 - self.ema_alpha).add_(contact, alpha=self.ema_alpha)
    return self._ema.var(dim=1)
reset
reset(envs_idx: Tensor | None = None) -> None
Source code in eden/managers/terms/rewards/locomotion.py
def reset(self, envs_idx: torch.Tensor | None = None) -> None:
    if envs_idx is None:
        self._ema.fill_(0.5)
    else:
        self._ema[envs_idx] = 0.5

FeetPhase

FeetPhase(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Reward for tracking desired foot height based on gait phase.

Uses a smoothstep profile to define the expected swing trajectory.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
gait_command_name class-attribute instance-attribute
gait_command_name: str = ''
swing_height class-attribute instance-attribute
swing_height: float = 0.08
tracking_sigma class-attribute instance-attribute
tracking_sigma: float = 0.25

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
    self._gait_cmd: GaitPhaseCommand = self._env.command_manager.get_term(self.gait_command_name)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    foot_pos = self._robot.get_links_pos(ls_idx_local=self._ls_idx)  # (num_envs, N, 3)
    phase = self._gait_cmd.command  # (num_envs, num_feet)

    if phase.shape[1] != foot_pos.shape[1]:
        raise ValueError(
            f"feet_phase: GaitPhaseCommand num_feet ({phase.shape[1]}) != "
            f"feet_link_names count ({foot_pos.shape[1]}). Check config."
        )
    expected_z = _expected_foot_height(phase, self.swing_height)  # (num_envs, num_feet)
    actual_z = foot_pos[:, :, 2]  # (num_envs, num_feet)
    total_error = torch.sum(torch.square(actual_z - expected_z), dim=1)

    return torch.exp(-total_error / self.tracking_sigma)

CloseFeetPenalty

CloseFeetPenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize when feet are too close together in the xy plane.

Returns 1.0 (penalty active) when the smallest pairwise xy distance between feet is < threshold, else 0.0.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
threshold class-attribute instance-attribute
threshold: float = 0.15

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
    n_feet = self._ls_idx.numel()
    # Diagonal mask (self-distance) precomputed once; None when <2 feet (penalty is a no-op).
    self._diag_mask = torch.eye(n_feet, device=self.device, dtype=torch.bool).unsqueeze(0) if n_feet >= 2 else None
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    if self._diag_mask is None:
        return torch.zeros(self.num_envs, device=self.device)
    foot_pos = self._robot.get_links_pos(ls_idx_local=self._ls_idx)  # (num_envs, N, 3)
    foot_xy = foot_pos[:, :, :2]  # (num_envs, N, 2)
    # Pairwise distances: (num_envs, N, N)
    diff = foot_xy.unsqueeze(2) - foot_xy.unsqueeze(1)
    pairwise_dist = torch.norm(diff, dim=-1)
    pairwise_dist = pairwise_dist.masked_fill(self._diag_mask, float("inf"))
    min_dist = pairwise_dist.min(dim=-1).values.min(dim=-1).values  # (num_envs,)
    return (min_dist < self.threshold).float()

FeetOrientationPenalty

FeetOrientationPenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize feet orientation deviation from flat (projected gravity in foot frame).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
    # One gravity row per (env, foot). Size by the resolved link count, not len(feet_link_names):
    # a glob/regex pattern can resolve to a different number of links.
    self._grav = gravity_vec(self.num_envs * len(self._ls_idx), self.device)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    foot_quat = self._robot.get_links_quat(ls_idx_local=self._ls_idx)  # (num_envs, N, 4)
    n_feet = foot_quat.shape[1]

    # Flatten (num_envs, N, 4) → (num_envs*N, 4) for batched quat ops
    flat_quat = foot_quat.reshape(-1, 4)
    projected = projected_gravity(flat_quat, self._grav)  # (num_envs*N, 3)
    # Deviation from vertical per foot, then sum across feet
    deviation = torch.sum(torch.square(projected[:, :2]), dim=1).sqrt()  # (num_envs*N,)
    return torch.sum(deviation.reshape(self.num_envs, n_feet), dim=1)

WeightedPosePenalty

WeightedPosePenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize deviation from default pose with per-joint weights.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
pose_weights class-attribute instance-attribute
pose_weights: list[float] = []

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    expected_dof_count = self._robot.get_dofs_pos().shape[1]
    if len(self.pose_weights) != expected_dof_count:
        raise ValueError(
            f"{term_subject(self)} expected {expected_dof_count} pose weights, got {len(self.pose_weights)}."
        )
    self._weights = torch.tensor(self.pose_weights, device=self.device, dtype=torch.float32).unsqueeze(0)
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    error = torch.square(self._robot.get_dofs_pos() - self._robot.default_dofs_pos)
    return torch.sum(error * self._weights, dim=1)

StumblePenalty

StumblePenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Compute an indicator penalty for feet hitting vertical surfaces (e.g. stairs riser, walls).

Returns 1.0 when any foot's lateral contact force exceeds horizontal_to_vertical_ratio * |z contact|, else 0.0. Mirrors holosoma/.../reward/terms/locomotion.py:316-329.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
horizontal_to_vertical_ratio class-attribute instance-attribute
horizontal_to_vertical_ratio: float = 4.0

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    forces = self._robot.get_links_net_contact_force()  # (num_envs, n_links, 3)
    foot_forces = forces[:, self._ls_idx, :]  # (num_envs, n_feet, 3)
    horiz = torch.norm(foot_forces[:, :, :2], dim=-1)
    vert = foot_forces[:, :, 2].abs()
    return (horiz > self.horizontal_to_vertical_ratio * vert).any(dim=1).float()

CloseFeetYawProjectedPenalty

CloseFeetYawProjectedPenalty(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Penalize feet closer than a lateral distance in the base's yaw frame.

Unlike the naive xy distance in :class:CloseFeetPenalty, this rotates the foot-pair offset into the robot's yaw-aligned frame and measures only the lateral component. This is more robust during in-place turns where the naive metric flips sign as the body yaws. Requires exactly two feet — humanoid convention. Mirrors holosoma/.../reward/terms/locomotion.py:191-215.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
feet_link_names: list[str] = []
threshold class-attribute instance-attribute
threshold: float = 0.05

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/locomotion.py
def build(self) -> None:
    self._robot = self.resolve_entity(self.entity_name)
    self._ls_idx = resolve_link_idx(
        self._robot, self.feet_link_names, self.device, term=type(self).__name__, field="feet_link_names"
    )
    if self._ls_idx.numel() != 2:
        raise ValueError(
            f"close_feet_yaw_projected_penalty expects exactly 2 feet, got {self._ls_idx.numel()}. "
            "Use close_feet_penalty for >2 feet."
        )
compute
compute() -> Tensor
Source code in eden/managers/terms/rewards/locomotion.py
def compute(self) -> torch.Tensor:
    foot_pos = self._robot.get_links_pos(ls_idx_local=self._ls_idx)  # (num_envs, 2, 3)
    left_xy = foot_pos[:, 0, :2]
    right_xy = foot_pos[:, 1, :2]
    yaw = quat_to_xyz(self._robot.get_quat(), rpy=True)[:, 2]
    cos_y = torch.cos(yaw)
    sin_y = torch.sin(yaw)
    # Lateral component of the foot-pair offset in the body's yaw-aligned frame:
    # rotating world (dx, dy) by -yaw gives lateral = -sin(yaw)*dx + cos(yaw)*dy.
    # The outer abs() makes the (left, right) pair ordering immaterial.
    lateral = torch.abs(cos_y * (left_xy[:, 1] - right_xy[:, 1]) - sin_y * (left_xy[:, 0] - right_xy[:, 0]))
    return (lateral < self.threshold).float()

Functions:

constant_base_height

constant_base_height(env: EnvBase, *, entity_name: str, target_height: float, terrain_entity_name: str | None = None, relative: bool = False) -> Tensor

Penalize base height away from target — :func:base_height_l2 without velocity gating.

terrain_entity_name and relative mean what they do there, and are forwarded unchanged.

Source code in eden/managers/terms/rewards/locomotion.py
@REWARD_TERM_REGISTRY.register()
def constant_base_height(
    env: EnvBase,
    *,
    entity_name: str,
    target_height: float,
    terrain_entity_name: str | None = None,
    relative: bool = False,
) -> torch.Tensor:
    """Penalize base height away from target — :func:`base_height_l2` without velocity gating.

    ``terrain_entity_name`` and ``relative`` mean what they do there, and are forwarded unchanged.
    """
    return base_height_l2(
        env,
        entity_name=entity_name,
        target_height=target_height,
        terrain_entity_name=terrain_entity_name,
        relative=relative,
    )

lin_vel_z

lin_vel_z(env: EnvBase, *, entity_name: str) -> Tensor

Penalize z axis base linear velocity.

Source code in eden/managers/terms/rewards/locomotion.py
@REWARD_TERM_REGISTRY.register()
def lin_vel_z(env: EnvBase, *, entity_name: str) -> torch.Tensor:
    """Penalize z axis base linear velocity."""
    robot: Entity = env.get_entity(entity_name)
    base_lin_vel_b = robot.get_vel(frame="body")
    return torch.square(base_lin_vel_b[:, 2])

track_lin_vel_with_z_penalty

track_lin_vel_with_z_penalty(env: EnvBase, *, command_name: str, entity_name: str, std: float) -> Tensor

Track commanded linear velocity with additional z-velocity penalty.

reward = exp(-(xy_error + 2 * z_vel^2) / std^2)

Source code in eden/managers/terms/rewards/locomotion.py
@REWARD_TERM_REGISTRY.register()
def track_lin_vel_with_z_penalty(
    env: EnvBase,
    *,
    command_name: str,
    entity_name: str,
    std: float,
) -> torch.Tensor:
    """Track commanded linear velocity with additional z-velocity penalty.

    reward = exp(-(xy_error + 2 * z_vel^2) / std^2)
    """
    command: CommandTerm = env.command_manager.get_term(command_name)
    robot: Entity = env.get_entity(entity_name)
    actual = robot.get_vel(frame="body")
    xy_error = torch.sum(torch.square(command.command[:, :2] - actual[:, :2]), dim=1)
    z_error = torch.square(actual[:, 2])
    lin_vel_error = xy_error + 2.0 * z_error
    return torch.exp(-lin_vel_error / (std**2))

track_ang_vel_with_xy_penalty

track_ang_vel_with_xy_penalty(env: EnvBase, *, command_name: str, entity_name: str, std: float) -> Tensor

Track commanded angular velocity with additional xy angular velocity penalty.

reward = exp(-(z_error + 0.05 * xy_error) / std^2)

Source code in eden/managers/terms/rewards/locomotion.py
@REWARD_TERM_REGISTRY.register()
def track_ang_vel_with_xy_penalty(
    env: EnvBase,
    *,
    command_name: str,
    entity_name: str,
    std: float,
) -> torch.Tensor:
    """Track commanded angular velocity with additional xy angular velocity penalty.

    reward = exp(-(z_error + 0.05 * xy_error) / std^2)
    """
    command: CommandTerm = env.command_manager.get_term(command_name)
    robot: Entity = env.get_entity(entity_name)
    actual = robot.get_ang(frame="body")
    z_error = torch.square(command.command[:, 2] - actual[:, 2])
    xy_error = torch.sum(torch.square(actual[:, :2]), dim=1)
    ang_vel_error = z_error + 0.05 * xy_error
    return torch.exp(-ang_vel_error / (std**2))

is_terminated_penalty

is_terminated_penalty(env: EnvBase) -> Tensor

Return 1 for terminated environments.

Source code in eden/managers/terms/rewards/locomotion.py
@REWARD_TERM_REGISTRY.register()
def is_terminated_penalty(env: EnvBase) -> torch.Tensor:
    """Return 1 for terminated environments."""
    return env.termination_manager.terminated.float()

base_height_l2

base_height_l2(env: EnvBase, *, entity_name: str, target_height: float, command_name: str | None = None, zero_vel_penalty_scale: float = 1.0, zero_vel_threshold: float = 0.1, terrain_entity_name: str | None = None, relative: bool = False) -> Tensor

L2 base-height penalty with optional zero-velocity amplification.

Squared deviation of the base height from target_height. When command_name is provided and the commanded planar velocity is below zero_vel_threshold, the penalty is multiplied by zero_vel_penalty_scale so the policy is held more strictly to the target height while standing. Mirrors holosoma/.../reward/terms/locomotion.py:218-250.

Height is read through :func:~eden.managers.terms.utils.base_height_above_ground, the same helper as the :func:~eden.managers.terms.observations.locomotion.base_height observation and the :func:~eden.managers.terms.terminations.locomotion.root_height_below_minimum termination, and it takes their two parameters for the same reasons. terrain_entity_name names a :class:~eden.entities.terrain.Terrain to measure against instead of the world-frame z: without it a target height on a heightfield is unreachable in a valley and undershot on a hill, and — the reason it belongs on this term rather than only on those two — a config that sets it on the observation and the termination but not here trains a policy whose reward pulls toward a height its own feature and its episode boundary do not measure. relative selects the frame the base pose is read in (see :func:~eden.managers.terms.observations.proprio.base_pos); the default False is the solver frame.

Equivalent to :func:constant_base_height when command_name is None. The stance-mode amplification from holosoma's variant is intentionally not ported here — that path reaches into env-set boolean attributes with no clear producer contract. If you need it, add a dedicated term that consumes a typed stance source (e.g. a phase-aware command term).

Source code in eden/managers/terms/rewards/locomotion.py
@REWARD_TERM_REGISTRY.register()
def base_height_l2(
    env: EnvBase,
    *,
    entity_name: str,
    target_height: float,
    command_name: str | None = None,
    zero_vel_penalty_scale: float = 1.0,
    zero_vel_threshold: float = 0.1,
    terrain_entity_name: str | None = None,
    relative: bool = False,
) -> torch.Tensor:
    """L2 base-height penalty with optional zero-velocity amplification.

    Squared deviation of the base height from ``target_height``. When ``command_name``
    is provided and the commanded planar velocity is below ``zero_vel_threshold``,
    the penalty is multiplied by ``zero_vel_penalty_scale`` so the policy is held
    more strictly to the target height while standing. Mirrors
    ``holosoma/.../reward/terms/locomotion.py:218-250``.

    Height is read through :func:`~eden.managers.terms.utils.base_height_above_ground`, the same
    helper as the :func:`~eden.managers.terms.observations.locomotion.base_height` observation and
    the :func:`~eden.managers.terms.terminations.locomotion.root_height_below_minimum` termination,
    and it takes their two parameters for the same reasons. ``terrain_entity_name`` names a
    :class:`~eden.entities.terrain.Terrain` to measure against instead of the world-frame ``z``:
    without it a target height on a heightfield is unreachable in a valley and undershot on a hill,
    and — the reason it belongs on *this* term rather than only on those two — a config that sets it
    on the observation and the termination but not here trains a policy whose reward pulls toward a
    height its own feature and its episode boundary do not measure. ``relative`` selects the frame
    the base pose is read in (see
    :func:`~eden.managers.terms.observations.proprio.base_pos`); the default ``False`` is the solver
    frame.

    Equivalent to :func:`constant_base_height` when ``command_name`` is ``None``.
    The stance-mode amplification from holosoma's variant is intentionally not
    ported here — that path reaches into env-set boolean attributes with no clear
    producer contract. If you need it, add a dedicated term that consumes a typed
    stance source (e.g. a phase-aware command term).
    """
    robot: Entity = resolve_named_entity(env.entities, entity_name)
    terrain = resolve_terrain(env.entities, terrain_entity_name)
    err = torch.square(base_height_above_ground(robot, terrain, relative=relative) - target_height)
    if command_name is not None:
        command: CommandTerm = env.command_manager.get_term(command_name)
        zero_vel_mask = torch.norm(command.command[:, :2], dim=1) < zero_vel_threshold
        err = torch.where(zero_vel_mask, err * zero_vel_penalty_scale, err)
    return err

Motion tracking

Rewards that score the robot against env.motion_manager reference data.

Reward terms that score robot state against env.motion_manager data.

All terms read motion-ref data through env.motion_manager.data[entity_name] (filled once per step at the start of MimicRLEnvBase.step()) — NOT reference_frame, which would force a recompute pass and a GPU sync.

Two families:

  • Direct world-frame tracking (function-style, registry keys motion_{base,link,dofs}_*_tracking_exp): scores robot state directly against world-frame motion-ref data. Use for free-floating objects (where world xy / yaw is meaningful), fixed-base entities, and DexMachina-style bimanual hand-object tasks.
  • Anchor-yaw-warped tracking (class-style, registry keys motion_{relative_body,global_body}_*): warps the motion targets through a yaw-only delta computed at an anchor link before scoring. Makes the reward invariant to the robot's heading drift on the floor — useful for free-floating humanoid imitation where absolute world yaw / xy is not the optimisation target. Requires both links_pos_w and links_quat_w.

Exp-kernel parameter convention: these terms use std with the form exp(-error / std**2), matching beyondmimic's tracking rewards. Two other conventions exist in the codebase that newcomers should be aware of:

  • locomotion.tracking_lin_vel uses sigma with the form exp(-error / sigma) (linear, not squared).
  • DexMachina papers use beta with exp(-beta * error). Mapping: std = 1 / sqrt(beta).

Each term calls :func:assert_motion_field_present at first read so a dataset that doesn't supply the required field (e.g. a fixed-base motion sequence with no base_pos_w, or an Arctic hand sequence with no links_quat_w) fails fast instead of silently scoring against the make-empty default (zeros / identity quat).

Attributes

Classes

MotionRelativeBodyPositionErrorExp

MotionRelativeBodyPositionErrorExp(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Yaw-warped multi-link position tracking (exp kernel, link mean).

The yaw-warp uses both anchor positions and quaternions (via :func:get_anchor_state / :func:get_links_state), so the dataset must populate both links_pos_w and links_quat_w. Use the direct :func:motion_link_position_tracking_exp for datasets that only have positions.

frame_offsets defaults to (0,) (current frame only). Multi-frame averages per-future-frame exp(...) masked by validity so past-clip frames contribute zero — pair with the obs-side :func:~eden.managers.terms.observations.tracking.motion_future_links_pos_b set to the same offsets.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
anchor_link_name: str = 'torso_link'
links_name: list[str] = []
std class-attribute instance-attribute
std: float = 0.1
frame_offsets class-attribute instance-attribute
frame_offsets: tuple[int, ...] = (0,)

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/tracking.py
def build(self) -> None:
    assert_motion_field_present(self._env, self.entity_name, "links_pos_w")
    assert_motion_field_present(self._env, self.entity_name, "links_quat_w")
compute
compute(envs_idx: Tensor | None = None) -> Tensor
Source code in eden/managers/terms/rewards/tracking.py
def compute(self, envs_idx: torch.Tensor | None = None) -> torch.Tensor:
    # Shared with the other half's reward and the matching metrics within a step's graded pass.
    links_pos_rel, _, robot_links_pos, _, validity = anchor_relative_links_once(
        self._env, self.entity_name, self.anchor_link_name, self.links_name, self.frame_offsets
    )
    # links_pos_rel: (N, K, L, 3), robot_links_pos: (N, L, 3) → broadcast over K.
    error = torch.sum(torch.square(links_pos_rel - robot_links_pos[:, None]), dim=-1)  # (N, K, L)
    per_frame = exp_kernel(error, self.std)  # (N, K)
    valid = None if validity is None else validity.to(per_frame.dtype)
    return validity_weighted_frame_mean(per_frame, valid)

MotionRelativeBodyOrientationErrorExp

MotionRelativeBodyOrientationErrorExp(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Yaw-warped multi-link orientation tracking (exp kernel, link mean).

Reads both links_pos_w and links_quat_w for the same reason as :class:MotionRelativeBodyPositionErrorExp.

frame_offsets matches :class:MotionRelativeBodyPositionErrorExp.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

entity_name class-attribute instance-attribute
entity_name: str = 'robot'
anchor_link_name: str = 'torso_link'
links_name: list[str] = []
std class-attribute instance-attribute
std: float = 0.1
frame_offsets class-attribute instance-attribute
frame_offsets: tuple[int, ...] = (0,)

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/tracking.py
def build(self) -> None:
    assert_motion_field_present(self._env, self.entity_name, "links_pos_w")
    assert_motion_field_present(self._env, self.entity_name, "links_quat_w")
compute
compute(envs_idx: Tensor | None = None) -> Tensor
Source code in eden/managers/terms/rewards/tracking.py
def compute(self, envs_idx: torch.Tensor | None = None) -> torch.Tensor:
    # Shared with the other half's reward and the matching metrics within a step's graded pass.
    _, links_quat_rel, _, robot_links_quat, validity = anchor_relative_links_once(
        self._env, self.entity_name, self.anchor_link_name, self.links_name, self.frame_offsets
    )
    K = links_quat_rel.shape[1]
    error = quat_error_magnitude(links_quat_rel, robot_links_quat[:, None].expand(-1, K, -1, -1)) ** 2  # (N, K, L)
    per_frame = exp_kernel(error, self.std)  # (N, K)
    valid = None if validity is None else validity.to(per_frame.dtype)
    return validity_weighted_frame_mean(per_frame, valid)

MotionGlobalBodyLinearVelocityErrorExp

MotionGlobalBodyLinearVelocityErrorExp(env: EnvBase, options: _TermOptionsT)

Bases: _MotionGlobalBodyVelocityErrorExp

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

MotionGlobalBodyAngularVelocityErrorExp

MotionGlobalBodyAngularVelocityErrorExp(env: EnvBase, options: _TermOptionsT)

Bases: _MotionGlobalBodyVelocityErrorExp

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Functions:

exp_kernel

exp_kernel(error: Tensor, std: float, *, mode: Literal['mean_of_exp', 'exp_of_mean', 'exp_of_sum'] = 'exp_of_mean') -> Tensor

Gaussian tracking kernel reducing the last (per-item) axis of error.

error is the squared per-item error (..., M) where M is the link or DOF axis; returns (...) with that axis collapsed. mode names the one valid way the exp and the per-item reduction compose — each value is the literal formula, so there is no silently-ignored argument:

  • "mean_of_exp" (DexMachina exp_kpt_first): per-item exp(-e/std²) then mean over items — soft per-item credit.
  • "exp_of_mean" (default): one exp on the mean per-item error — a single soft pass on the average.
  • "exp_of_sum" (DexMachina obj_arti_rew): one exp on the summed per-item error — a single hard pass/fail.

Mirrors the exp(-error / std²) convention documented at module top.

Source code in eden/managers/terms/rewards/tracking.py
def exp_kernel(
    error: torch.Tensor,
    std: float,
    *,
    mode: Literal["mean_of_exp", "exp_of_mean", "exp_of_sum"] = "exp_of_mean",
) -> torch.Tensor:
    """Gaussian tracking kernel reducing the last (per-item) axis of ``error``.

    ``error`` is the squared per-item error ``(..., M)`` where ``M`` is the link
    or DOF axis; returns ``(...)`` with that axis collapsed. ``mode`` names the
    one valid way the exp and the per-item reduction compose — each value is the
    literal formula, so there is no silently-ignored argument:

    * ``"mean_of_exp"`` (DexMachina ``exp_kpt_first``): per-item ``exp(-e/std²)``
      then mean over items — soft per-item credit.
    * ``"exp_of_mean"`` (default): one ``exp`` on the mean per-item error — a
      single soft pass on the average.
    * ``"exp_of_sum"`` (DexMachina ``obj_arti_rew``): one ``exp`` on the summed
      per-item error — a single hard pass/fail.

    Mirrors the ``exp(-error / std²)`` convention documented at module top.
    """
    if mode == "mean_of_exp":
        return torch.exp(-error / std**2).mean(-1)
    if mode == "exp_of_mean":
        return torch.exp(-error.mean(-1) / std**2)
    if mode == "exp_of_sum":
        return torch.exp(-error.sum(-1) / std**2)
    raise ValueError(f"Unknown exp_kernel mode {mode!r}. Use 'mean_of_exp', 'exp_of_mean', or 'exp_of_sum'.")

validity_weighted_frame_mean

validity_weighted_frame_mean(per_frame: Tensor, valid: Tensor | None) -> Tensor

Average a per-frame reward (N, K) over the K future frames.

Each frame is weighted by its valid mask (N, K) — 1.0 inside the clip, 0.0 for frames past the clip end (the buffer holds the last valid frame there, so an unmasked mean would score against a fake target). The denominator is clamped at 1 so an all-invalid row returns 0 rather than NaN. valid=None (no validity available) falls back to a plain mean over K. Returns (N,).

Source code in eden/managers/terms/rewards/tracking.py
def validity_weighted_frame_mean(
    per_frame: torch.Tensor,
    valid: torch.Tensor | None,
) -> torch.Tensor:
    """Average a per-frame reward ``(N, K)`` over the K future frames.

    Each frame is weighted by its ``valid`` mask ``(N, K)`` — 1.0 inside the
    clip, 0.0 for frames past the clip end (the buffer holds the last valid
    frame there, so an unmasked mean would score against a fake target). The
    denominator is clamped at 1 so an all-invalid row returns 0 rather than
    NaN. ``valid=None`` (no validity available) falls back to a plain mean over
    K. Returns ``(N,)``.
    """
    if valid is None:
        return per_frame.mean(-1)
    return (per_frame * valid).sum(-1) / valid.sum(-1).clamp(min=1.0)

motion_base_position_tracking_exp

motion_base_position_tracking_exp(env: MimicRLEnvBase, *, entity_name: str, std: float) -> Tensor

Exp-kernel reward on world-frame base position vs motion ref.

DexMachina equivalent: obj_pos_rew.

Source code in eden/managers/terms/rewards/tracking.py
@REWARD_TERM_REGISTRY.register()
def motion_base_position_tracking_exp(
    env: MimicRLEnvBase,
    *,
    entity_name: str,
    std: float,
) -> torch.Tensor:
    """Exp-kernel reward on world-frame base position vs motion ref.

    DexMachina equivalent: ``obj_pos_rew``.
    """
    assert_motion_field_present(env, entity_name, "base_pos_w")
    entity = env.get_entity(entity_name)
    ref = env.motion_manager.data[entity_name]
    error = torch.sum(torch.square(entity.get_pos() - current_motion_frame(ref.base_pos_w)), dim=-1)
    return torch.exp(-error / std**2)

motion_base_orientation_tracking_exp

motion_base_orientation_tracking_exp(env: MimicRLEnvBase, *, entity_name: str, std: float) -> Tensor

Exp-kernel reward on quat-error magnitude of the base vs motion ref (wxyz).

DexMachina equivalent: obj_rot_rew.

Source code in eden/managers/terms/rewards/tracking.py
@REWARD_TERM_REGISTRY.register()
def motion_base_orientation_tracking_exp(
    env: MimicRLEnvBase,
    *,
    entity_name: str,
    std: float,
) -> torch.Tensor:
    """Exp-kernel reward on quat-error magnitude of the base vs motion ref (wxyz).

    DexMachina equivalent: ``obj_rot_rew``.
    """
    assert_motion_field_present(env, entity_name, "base_quat_w")
    entity = env.get_entity(entity_name)
    ref = env.motion_manager.data[entity_name]
    error = quat_error_magnitude(entity.get_quat(), current_motion_frame(ref.base_quat_w)) ** 2
    return torch.exp(-error / std**2)
motion_link_position_tracking_exp(env: MimicRLEnvBase, *, entity_name: str, links_name: list[str], std: float, exp_first: bool = True, frame_offsets: Sequence[int] = (0,)) -> Tensor

Exp-kernel reward on world-frame link positions vs motion ref.

Single-link (links_name=["wrist"]) gives DexMachina wrist-pos / object-pos tracking; multi-link gives DexMachina fingertip imitation.

exp_first=True (default, matches DexMachina exp_kpt_first=True): mean over links of exp(-d² / std²). exp_first=False: exp(-mean(d²) / std²) — one soft pass on the average error.

frame_offsets=(0,) (default) scores only the current motion frame. Multi-frame (e.g. (0, 4, 8)) averages the per-frame reward over the listed buffer indices, masked by MotionReferenceData.validity so frames past the clip end contribute zero. Pair with the obs-side :func:~eden.managers.terms.observations.tracking.motion_future_links_pos_b lookahead set to the same offsets so the reward scores what the policy sees.

Source code in eden/managers/terms/rewards/tracking.py
@REWARD_TERM_REGISTRY.register()
def motion_link_position_tracking_exp(
    env: MimicRLEnvBase,
    *,
    entity_name: str,
    links_name: list[str],
    std: float,
    exp_first: bool = True,
    frame_offsets: Sequence[int] = (0,),
) -> torch.Tensor:
    """Exp-kernel reward on world-frame link positions vs motion ref.

    Single-link (``links_name=["wrist"]``) gives DexMachina wrist-pos /
    object-pos tracking; multi-link gives DexMachina fingertip imitation.

    ``exp_first=True`` (default, matches DexMachina ``exp_kpt_first=True``):
    mean over links of ``exp(-d² / std²)``.
    ``exp_first=False``: ``exp(-mean(d²) / std²)`` — one soft pass on the
    average error.

    ``frame_offsets=(0,)`` (default) scores only the current motion frame.
    Multi-frame (e.g. ``(0, 4, 8)``) averages the per-frame reward over the
    listed buffer indices, masked by ``MotionReferenceData.validity`` so
    frames past the clip end contribute zero. Pair with the obs-side
    :func:`~eden.managers.terms.observations.tracking.motion_future_links_pos_b`
    lookahead set to the same offsets so the reward scores what the policy sees.
    """
    assert_motion_field_present(env, entity_name, "links_pos_w")
    motion_idx = get_motion_link_idx(env, entity_name, links_name)
    robot_idx = get_robot_link_idx(env, entity_name, links_name)
    ref = env.motion_manager.data[entity_name]
    offsets = resolve_frame_offsets(env, "links_pos_w", frame_offsets, ref.links_pos_w.shape[1])
    # (N, K, L_all, 3) → (N, K, L, 3) and broadcast robot_pos to match.
    motion_pos = select_motion_frames(ref.links_pos_w, offsets, motion_idx)
    robot_pos = env.get_entity(entity_name).get_links_pos(ls_idx_local=robot_idx)  # (N, L, 3)
    sq = torch.sum(torch.square(motion_pos - robot_pos[:, None]), dim=-1)  # (N, K, L)
    valid = _validity_mask_for(ref, offsets, dtype=sq.dtype)  # (N, K)
    per_frame = exp_kernel(sq, std, mode="mean_of_exp" if exp_first else "exp_of_mean")  # (N, K)
    return validity_weighted_frame_mean(per_frame, valid)
motion_link_orientation_tracking_exp(env: MimicRLEnvBase, *, entity_name: str, links_name: list[str], std: float, exp_first: bool = True, frame_offsets: Sequence[int] = (0,)) -> Tensor

Exp-kernel reward on world-frame link orientations vs motion ref (wxyz).

Single-link gives DexMachina wrist-rot / object-rot tracking; multi-link reduces the same way as the position variant.

frame_offsets matches :func:motion_link_position_tracking_exp — default (0,) is the current frame; multi-frame masks past-clip frames via validity.

Source code in eden/managers/terms/rewards/tracking.py
@REWARD_TERM_REGISTRY.register()
def motion_link_orientation_tracking_exp(
    env: MimicRLEnvBase,
    *,
    entity_name: str,
    links_name: list[str],
    std: float,
    exp_first: bool = True,
    frame_offsets: Sequence[int] = (0,),
) -> torch.Tensor:
    """Exp-kernel reward on world-frame link orientations vs motion ref (wxyz).

    Single-link gives DexMachina wrist-rot / object-rot tracking; multi-link
    reduces the same way as the position variant.

    ``frame_offsets`` matches :func:`motion_link_position_tracking_exp` —
    default ``(0,)`` is the current frame; multi-frame masks past-clip
    frames via ``validity``.
    """
    assert_motion_field_present(env, entity_name, "links_quat_w")
    motion_idx = get_motion_link_idx(env, entity_name, links_name)
    robot_idx = get_robot_link_idx(env, entity_name, links_name)
    ref = env.motion_manager.data[entity_name]
    offsets = resolve_frame_offsets(env, "links_quat_w", frame_offsets, ref.links_quat_w.shape[1])
    # (N, K, L_all, 4) → (N, K, L, 4); robot_quat is broadcast.
    motion_quat = select_motion_frames(ref.links_quat_w, offsets, motion_idx)
    robot_quat = env.get_entity(entity_name).get_links_quat(ls_idx_local=robot_idx)  # (N, L, 4)
    K = offsets.shape[0]
    err = quat_error_magnitude(motion_quat, robot_quat[:, None].expand(-1, K, -1, -1)) ** 2  # (N, K, L)
    valid = _validity_mask_for(ref, offsets, dtype=err.dtype)  # (N, K)
    per_frame = exp_kernel(err, std, mode="mean_of_exp" if exp_first else "exp_of_mean")  # (N, K)
    return validity_weighted_frame_mean(per_frame, valid)

motion_dofs_pos_tracking_exp

motion_dofs_pos_tracking_exp(env: MimicRLEnvBase, *, entity_name: str, std: float, dofs_name: list[str] | None = None, reduction: Literal['sum', 'per_dof_mean', 'mean'] = 'sum', frame_offsets: Sequence[int] = (0,)) -> Tensor

Exp-kernel reward on per-DOF qpos vs motion ref.

reduction="sum" (default): exp(-Σ(q − q_ref)² / std²). Single hard pass/fail; matches DexMachina obj_arti_rew. reduction="per_dof_mean": mean over dofs of exp(-(q − q_ref)² / std²). Each DOF gets independent credit; matches DexMachina bc_rew for hand qpos behavior cloning. reduction="mean": exp(-mean((q − q_ref)²) / std²) — one soft pass on the MSE; matches ConTrack arm/finger joint tracking.

dofs_name=None tracks every DOF the motion ref exposes for the entity.

frame_offsets defaults to (0,) (current frame only). Multi-frame averages the per-frame exp(...) weighted by validity.

Source code in eden/managers/terms/rewards/tracking.py
@REWARD_TERM_REGISTRY.register()
def motion_dofs_pos_tracking_exp(
    env: MimicRLEnvBase,
    *,
    entity_name: str,
    std: float,
    dofs_name: list[str] | None = None,
    reduction: Literal["sum", "per_dof_mean", "mean"] = "sum",
    frame_offsets: Sequence[int] = (0,),
) -> torch.Tensor:
    """Exp-kernel reward on per-DOF qpos vs motion ref.

    ``reduction="sum"`` (default): ``exp(-Σ(q − q_ref)² / std²)``. Single
    hard pass/fail; matches DexMachina ``obj_arti_rew``.
    ``reduction="per_dof_mean"``: mean over dofs of
    ``exp(-(q − q_ref)² / std²)``. Each DOF gets independent credit;
    matches DexMachina ``bc_rew`` for hand qpos behavior cloning.
    ``reduction="mean"``: ``exp(-mean((q − q_ref)²) / std²)`` — one soft
    pass on the MSE; matches ConTrack arm/finger joint tracking.

    ``dofs_name=None`` tracks every DOF the motion ref exposes for the
    entity.

    ``frame_offsets`` defaults to ``(0,)`` (current frame only). Multi-frame
    averages the per-frame ``exp(...)`` weighted by validity.
    """
    assert_motion_field_present(env, entity_name, "dofs_pos")
    entity = env.get_entity(entity_name)
    ref = env.motion_manager.data[entity_name]
    if dofs_name is None:
        dofs_name = env.motion_manager.get_target_dofs_name(entity_name)
        if dofs_name is None:
            raise ValueError(f"Entity '{entity_name}' is not tracked by any motion term.")
        if not dofs_name:
            raise ValueError(
                f"motion_dofs_pos_tracking_exp: entity '{entity_name}' has no DOFs in the "
                f"motion reference. Use motion_link_position_tracking_exp / "
                f"motion_base_position_tracking_exp for rigid or link-only entities, or "
                f"pass an explicit dofs_name list."
            )
    motion_idx = get_motion_dofs_idx(env, entity_name, dofs_name)
    robot_idx = get_robot_dofs_idx(env, entity_name, dofs_name)
    offsets = resolve_frame_offsets(env, "dofs_pos", frame_offsets, ref.dofs_pos.shape[1])
    # (N, K, D_all) → (N, K, D); robot_q is the current state, broadcast over K.
    motion_q = select_motion_frames(ref.dofs_pos, offsets, motion_idx)
    # Handed to the getter as `dofs_idx_local`, not used to gather from a full-width read:
    # `get_dofs_pos()` returns the entity's DOFs in *dofs_name* order, so `index_select`-ing it
    # with `robot_idx` (Genesis-local, tree-numbered, offset by a floating base's six) is out of
    # bounds on a floating base and a silent permutation on a fixed one whose declaration order
    # differs from the tree's. The getter takes local indices and returns them in the order given.
    robot_q = entity.get_dofs_pos(dofs_idx_local=robot_idx)  # (N, D)
    sq = torch.square(robot_q[:, None] - motion_q)  # (N, K, D)
    valid = _validity_mask_for(ref, offsets, dtype=sq.dtype)  # (N, K)
    kernel_mode = {"per_dof_mean": "mean_of_exp", "mean": "exp_of_mean", "sum": "exp_of_sum"}[reduction]
    per_frame = exp_kernel(sq, std, mode=kernel_mode)  # (N, K)
    return validity_weighted_frame_mean(per_frame, valid)

Contact matching

Contact-cloud Chamfer reward.

Scores policy-vs-demo per-link-pair contact configurations via masked Chamfer distance, in either world / object / wrist frame, with exp or sigmoid kernels. Builds on :meth:RigidEntity.get_grouped_contacts (the policy contact-cloud aggregator) and :func:eden.utils.pc.chamfer_distance (the distance kernel).

Reads the demo-side contact target from the motion manager (env.motion_manager.data[hand_entity_name].contact_points_w / contact_points_valid). A dataset packs them through add_motion_sequence; when the loaded motion data does not carry them, :meth:MotionContactChamferReward.build raises via :func:assert_motion_field_present.

Mirrors DexMachina RewardModule.compute_hand_contact_reward (dexmachina/envs/rewards.py:293) at single-frame, single-part granularity. Per-part / multi-frame composition is left to the config layer (one term per part / frame), keeping the reward kernel composable.

Attributes

EMPTY_DISTANCE_SENTINEL module-attribute

EMPTY_DISTANCE_SENTINEL: Final[float] = 100.0

Per-direction Chamfer distance returned when one cloud is empty and the other is not. Mirrors DexMachina reward_utils.chamfer_distance (dexmachina/envs/reward_utils.py:44) — large enough that exp(-100/std**2) ≈ 0 turns "policy contacts but demo does not" (or vice-versa) into a hard penalty. chamfer_distance itself defaults to 0.0 (no signal); this term overrides to the DexMachina value.

Classes

MotionContactChamferReward

MotionContactChamferReward(env: EnvBase, options: _TermOptionsT)

Bases: RewardTerm

Exp/sigmoid reward on the policy-vs-demo contact Chamfer distance.

Per-step semantics:

  1. Policy contacts: hand.get_grouped_contacts(object, links_a, links_b) — (B, n_a, n_b, 3) weighted-mean positions plus (B, n_a, n_b) validity mask.
  2. Demo contacts: env.motion_manager.data[hand_entity_name].contact_points_w[:, lookahead_index] — same shape. Validity from contact_points_valid[:, lookahead_index].
  3. Each cloud is transformed by its own anchor before the Chamfer comparison (matches DexMachina rewards.py:321-328):

  4. frame="object": policy uses obj.get_pos() / get_quat(); demo uses motion_manager.data[object_entity_name].base_pos_w / base_quat_w at lookahead_index.

  5. frame="wrist": policy uses the named wrist link's current pose on the hand; demo uses links_pos_w / links_quat_w on the hand's motion data at lookahead_index and the motion-side wrist index.
  6. frame="world": no transform.

Two instances composed at the config layer (frame="object" and frame="wrist", summed via term weights) reproduce DexMachina's multiply_frame_contact=True once weights are set up — the dual-anchor frame transform is what makes the composition meaningful. 4. chamfer_distance over the flattened (n_a, n_b) → N clouds with empty_distance=EMPTY_DISTANCE_SENTINEL (DexMachina sentinel, 100.0). 5. Distance → reward via the configured kernel. 6. Both-empty mask: per zero_contact_mode.

PARAMETER DESCRIPTION
hand_entity_name

Active entity (usually the hand). Indexes axis 1 of the grouped-contact tensors.

object_entity_name

Contact partner (usually the manipulated object). Indexes axis 2.

hand_links_name

:func:resolve_matching_names-compatible patterns selecting which links on hand_entity_name contribute. Empty → all links on the hand.

object_links_name

Same, on the object side.

frame

Frame the Chamfer distance is measured in. "world" (default), "object", or "wrist". Each cloud is anchored independently to its own object/wrist pose.

wrist_link_name

Required iff frame="wrist". Names the link on hand_entity_name whose pose defines the local frame. Resolved on both the robot side (current sim pose) and the motion side (demo recorded pose) at build().

lookahead_index

Frame index into the per-env motion-reference window. 0 (default) reads the current target, the convention of the motion-tracking terms. >0 reads future targets in the look-ahead window. Validated at build() against the per-env window size.

kernel

"exp" (default) or "sigmoid".

std

Exp-kernel scale; exp(-d / std**2). Default 1/sqrt(30) ≈ 0.1826 for exact parity with DexMachina contact_beta=30. Mapping: std = 1/sqrt(beta).

sigmoid_a

Sigmoid-kernel constants; ignored unless kernel="sigmoid". Defaults match DexMachina (a=100, b=5).

sigmoid_b

Sigmoid-kernel constants; ignored unless kernel="sigmoid". Defaults match DexMachina (a=100, b=5).

zero_contact_mode

"zero" (default, DexMachina mask_zero_contact=True) or "one".

empty_distance

Passed to :func:chamfer_distance. Default :data:EMPTY_DISTANCE_SENTINEL (100.0) mirrors DexMachina; chamfer_distance itself defaults to 0.0.

Notes

The demo-side fields contact_points_w and contact_points_valid of :class:MotionReferenceData are optional. When the loaded motion data does not carry them, :meth:build raises via :func:assert_motion_field_present, so the reward cannot silently score against zeros.

The term assumes a free-floating hand and free-floating object — typical for dexterous manipulation. Arm-mounted hand setups are supported because the hand sub-tree remains free relative to the (possibly fixed) arm; the wrist anchor still resolves correctly.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

hand_entity_name class-attribute instance-attribute
hand_entity_name: str = 'robot'
object_entity_name class-attribute instance-attribute
object_entity_name: str = 'object'
hand_links_name: list[str] = []
object_links_name: list[str] = []
frame class-attribute instance-attribute
frame: Literal['world', 'object', 'wrist'] = 'world'
wrist_link_name: str | None = None
lookahead_index class-attribute instance-attribute
lookahead_index: int = 0
kernel class-attribute instance-attribute
kernel: Literal['exp', 'sigmoid'] = 'exp'
std class-attribute instance-attribute
std: float = 1.0 / math.sqrt(30.0)
sigmoid_a class-attribute instance-attribute
sigmoid_a: float = 100.0
sigmoid_b class-attribute instance-attribute
sigmoid_b: float = 5.0
zero_contact_mode class-attribute instance-attribute
zero_contact_mode: Literal['zero', 'one'] = 'zero'
empty_distance class-attribute instance-attribute
empty_distance: float = EMPTY_DISTANCE_SENTINEL

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/contact.py
def build(self) -> None:
    env = self._env
    hand: RigidEntity = env.get_entity(self.hand_entity_name)
    obj: RigidEntity = env.get_entity(self.object_entity_name)

    if self.lookahead_index < 0:
        raise ValueError(f"MotionContactChamferReward.lookahead_index must be >= 0, got {self.lookahead_index}.")

    self._hand_links_idx = self._resolve_links(hand, self.hand_links_name, "hand_links_name")
    self._object_links_idx = self._resolve_links(obj, self.object_links_name, "object_links_name")

    if self.frame == "wrist":
        if self.wrist_link_name is None:
            raise ValueError(
                f"MotionContactChamferReward(frame='wrist') requires wrist_link_name on entity "
                f"'{self.hand_entity_name}'."
            )
        _, wrist_idx = hand.find_named_links_idx_local([self.wrist_link_name], preserve_order=True)
        self._wrist_link_idx = torch.tensor([wrist_idx[0]], dtype=torch.long, device=self.device)

    # Fail-fast: require the demo contact cloud plus the demo-side anchor fields the chosen frame requires.
    assert_motion_field_present(env, self.hand_entity_name, "contact_points_w")
    assert_motion_field_present(env, self.hand_entity_name, "contact_points_valid")
    if self.frame == "object":
        assert_motion_field_present(env, self.object_entity_name, "base_pos_w")
        assert_motion_field_present(env, self.object_entity_name, "base_quat_w")
    elif self.frame == "wrist":
        assert_motion_field_present(env, self.hand_entity_name, "links_pos_w")
        assert_motion_field_present(env, self.hand_entity_name, "links_quat_w")
        # Resolve the wrist's motion-side index — may differ from the
        # robot-side index if the motion sequence tracks a permuted /
        # subset of the hand's links.
        self._motion_wrist_idx = get_motion_link_idx(env, self.hand_entity_name, [self.wrist_link_name])
        # A Python int, read once here: indexing with the 0-dim CUDA `idx[0]` syncs on every use.
        self._motion_wrist_col = int(self._motion_wrist_idx[0])

    # Per-env motion-reference window size. ``contact_points_w`` may not
    # be allocated on the buffer yet; use ``dofs_pos`` — always populated
    # by ``MotionReferenceData.make_empty`` — as the window-size oracle
    # (all per-frame fields share num_frames).
    ref = env.motion_manager.data[self.hand_entity_name]
    window = ref.dofs_pos.shape[1]
    if self.lookahead_index >= window:
        raise ValueError(
            f"MotionContactChamferReward.lookahead_index={self.lookahead_index} "
            f"is out of bounds for motion-reference window size {window} on "
            f"entity '{self.hand_entity_name}'."
        )

    # Sigmoid-offset pre-computation matches DexMachina sigmoid_offset:
    #   offset = 1 - sigmoid(b)  → sigmoid(-a*0+b) + offset = 1 at d=0.
    if self.kernel == "sigmoid":
        # The kernel is sigmoid(-a*d + b): with sigmoid_a > 0 it *decreases* as the Chamfer
        # distance d grows (worse tracking → lower reward). sigmoid_a <= 0 would flip that and
        # reward worse tracking, so require it strictly positive.
        if self.sigmoid_a <= 0:
            raise ValueError(f"MotionContactChamferReward.sigmoid_a must be > 0, got {self.sigmoid_a}.")
        self._sigmoid_offset = 1.0 - 1.0 / (1.0 + math.exp(-self.sigmoid_b))
    else:
        self._sigmoid_offset = 0.0
compute
compute(envs_idx: Tensor | None = None) -> Tensor
Source code in eden/managers/terms/rewards/contact.py
def compute(self, envs_idx: torch.Tensor | None = None) -> torch.Tensor:
    env = self._env
    hand: RigidEntity = env.get_entity(self.hand_entity_name)
    obj: RigidEntity = env.get_entity(self.object_entity_name)
    i = self.lookahead_index

    policy = hand.get_grouped_contacts(
        obj,
        links_a_idx_local=self._hand_links_idx,
        links_b_idx_local=self._object_links_idx,
    )
    hand_ref = env.motion_manager.data[self.hand_entity_name]

    policy_anchor_pos: torch.Tensor | None = None
    policy_anchor_quat: torch.Tensor | None = None
    demo_anchor_pos: torch.Tensor | None = None
    demo_anchor_quat: torch.Tensor | None = None

    if self.frame == "object":
        obj_ref = env.motion_manager.data[self.object_entity_name]
        policy_anchor_pos = obj.get_pos()
        policy_anchor_quat = obj.get_quat()
        demo_anchor_pos = obj_ref.base_pos_w[:, i]
        demo_anchor_quat = obj_ref.base_quat_w[:, i]
    elif self.frame == "wrist":
        policy_anchor_pos = hand.get_links_pos(ls_idx_local=self._wrist_link_idx)[:, 0]
        policy_anchor_quat = hand.get_links_quat(ls_idx_local=self._wrist_link_idx)[:, 0]
        demo_anchor_pos = hand_ref.links_pos_w[:, i, self._motion_wrist_col]
        demo_anchor_quat = hand_ref.links_quat_w[:, i, self._motion_wrist_col]

    return _compute_contact_chamfer_reward(
        policy_pos=policy["position"],
        policy_valid=policy["valid"],
        policy_anchor_pos=policy_anchor_pos,
        policy_anchor_quat=policy_anchor_quat,
        demo_pos=hand_ref.contact_points_w[:, i],
        demo_valid=hand_ref.contact_points_valid[:, i],
        demo_anchor_pos=demo_anchor_pos,
        demo_anchor_quat=demo_anchor_quat,
        kernel=self.kernel,
        std=self.std,
        sigmoid_a=self.sigmoid_a,
        sigmoid_b=self.sigmoid_b,
        sigmoid_offset=self._sigmoid_offset,
        zero_contact_mode=self.zero_contact_mode,
        empty_distance=self.empty_distance,
    )

MotionContactMatchReward

MotionContactMatchReward(env: EnvBase, options: _TermOptionsT)

Bases: _MotionContactTermBase

Fraction of links whose contact state matches the demo's contact-ON links.

ConTrack's contact-event overlap reward: per tracked link, credit 1 iff some partner column has BOTH the demo annotation and simulated contact (same-partner matching — ConTrack's per-object (ref & sim).any); mean over the tracked links. Links the demo marks contact-free contribute 0 either way — the term rewards making annotated contacts, not avoiding spurious ones (ConTrack contact_reward).

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Methods:

compute
compute(envs_idx: Tensor | None = None) -> Tensor
Source code in eden/managers/terms/rewards/contact.py
def compute(self, envs_idx: torch.Tensor | None = None) -> torch.Tensor:
    # Per-partner AND before the any-reduction: demo contact with partner A
    # plus sim contact with partner B earns nothing. The partner axes are
    # aligned by build() (name-anchored via object_links_name); for a single
    # partner this is identical to AND-ing the two any-flags.
    match = (self._policy_contacts()["valid"] & self._demo_valid()).any(dim=-1)  # (N, L)
    return match.to(torch.float32).mean(dim=-1) * self._frame_valid()

MotionContactDistanceReward

MotionContactDistanceReward(env: EnvBase, options: _TermOptionsT)

Bases: _MotionContactTermBase

Exp-kernel reward on per-link contact-point distance in the object frame.

ConTrack's contact-point distance reward (contact_distance_reward): for each link in joint contact (demo AND sim), the simulated contact point is expressed in the CURRENT object frame and the demo point in the DEMO object frame (dual anchor — the comparison measures where on the object the link touches, decoupled from object tracking error), then scored with exp(-d² / std²) and averaged over jointly-contacting links. Returns 0 when no link is jointly in contact.

Source code in eden/managers/base.py
def __init__(self, env: EnvBase, options: _TermOptionsT):
    ManagerTermBase.__init__(self, env=env)
    ConfigurableMixin.__init__(self, options=options)

Attributes

std class-attribute instance-attribute
std: float = 0.03

Methods:

build
build() -> None
Source code in eden/managers/terms/rewards/contact.py
def build(self) -> None:
    super().build()
    env = self._env
    assert_motion_field_present(env, self.hand_entity_name, "contact_points_w")
    assert_motion_field_present(env, self.object_entity_name, "base_pos_w")
    assert_motion_field_present(env, self.object_entity_name, "base_quat_w")
compute
compute(envs_idx: Tensor | None = None) -> Tensor
Source code in eden/managers/terms/rewards/contact.py
def compute(self, envs_idx: torch.Tensor | None = None) -> torch.Tensor:
    env = self._env
    obj: RigidEntity = env.get_entity(self.object_entity_name)
    policy = self._policy_contacts()  # position (N, L, n_b, 3), valid (N, L, n_b)
    hand_ref = env.motion_manager.data[self.hand_entity_name]
    obj_ref = env.motion_manager.data[self.object_entity_name]

    demo_pos_w = hand_ref.contact_points_w[:, 0].index_select(1, self._motion_links_idx)  # (N, L, P, 3)
    demo_valid = self._demo_valid()  # (N, L, P)

    sim_local = inv_transform_by_trans_quat(
        policy["position"],
        obj.get_pos().unsqueeze(1).unsqueeze(1),
        obj.get_quat().unsqueeze(1).unsqueeze(1),
    )
    demo_local = inv_transform_by_trans_quat(
        demo_pos_w,
        obj_ref.base_pos_w[:, 0].unsqueeze(1).unsqueeze(1),
        obj_ref.base_quat_w[:, 0].unsqueeze(1).unsqueeze(1),
    )

    # Partner axes are aligned in build(): the policy contact axis is ordered
    # to object_links_name (or all object links when single-partner), matching
    # the demo column order, so P == n_b and column i pairs with column i.
    joint = (demo_valid & policy["valid"]).to(torch.float32)  # (N, L, P)
    d_sq = torch.sum(torch.square(sim_local - demo_local), dim=-1)  # (N, L, P)
    # Per link: mean d² over jointly-contacting partners, exp kernel, then
    # average over links with at least one joint contact (0 if none).
    cnt = joint.sum(dim=-1)  # (N, L)
    link_has = (cnt > 0).to(torch.float32)
    mean_d_sq = (d_sq * joint).sum(dim=-1) / cnt.clamp(min=1.0)
    per_link = torch.exp(-mean_d_sq / (self.std**2)) * link_has
    reward = per_link.sum(dim=-1) / link_has.sum(dim=-1).clamp(min=1.0)
    return reward * self._frame_valid()

Functions:

See also