Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions docs/source/en/user_guide/robots.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ motrix_envs/src/motrix_envs/robot/
```

An MJCF file should contain only the reusable robot model, without a floor, lights, or task-specific objects. For a
URDF robot, `UrdfFileCfg` can add simulation properties through its `geoms`, `sites`, `joints`, and `actuators` fields.
URDF robot, `UrdfFileCfg` can add simulation properties through its `geoms`, `sites`, and `joints` fields.
Declare MJCF and URDF actuators on the robot instance through its inherited `BodyCfg.actuators` dictionary.
Import `ActuatorCfg`, `PositionActuatorCfg`, and `MotorActuatorCfg` from `motrix_env_core.config.scene.actuator`.

### 2. Declare the `RobotCfg`

Expand Down Expand Up @@ -77,6 +79,7 @@ The public `RobotCfg` fields are:
| `base_link_name` | Root link used to attach the model to a scene |
| `translation` / `rotation` | Optional default instance pose; task configs can override it |
| `prefix` / `suffix` | Optional element-name affixes for name isolation within a scene |
| `actuators` | `None` preserves imported actuators; `{}` removes them; a non-empty dictionary replaces them |
| `key_pose` | Named poses that share one explicit joint order, such as `default` or `crouch` |

`KeyPoseCfg.joint_names` must be unique and non-empty. Every pose must contain one finite value per joint. Keep only
Expand All @@ -99,24 +102,40 @@ class MyHumanoid(HumanoidRobotCfg):
Both foot-link names must be non-empty and distinct. `resolved_foot_link_names` automatically applies the robot
instance's `prefix` and `suffix`.

A URDF robot uses the same config hierarchy and replaces `model` with `UrdfFileCfg`. Add position actuators or sole sites
explicitly when the source URDF does not provide them:
A URDF robot uses the same config hierarchy and replaces `model` with `UrdfFileCfg`. Sites remain on the model config;
actuators belong to `RobotCfg`. This minimal example assumes `base` and `left_foot` links and a limited `left_hip` joint:

```python
model: UrdfFileCfg = UrdfFileCfg(
file=MY_ROBOT_ASSET_DIR / "my_robot.urdf",
sites=[SiteCfg(name="left_sole", parent_link_name="left_foot")],
actuators=[
PositionActuatorCfg(
from motrix_env_core.config.scene import RobotCfg
from motrix_env_core.config.scene.actuator import PositionActuatorCfg
from motrix_env_core.config.scene.urdf import SiteCfg, UrdfFileCfg

robot = RobotCfg(
model=UrdfFileCfg(
file="my_robot.urdf",
sites=[SiteCfg(name="left_sole", parent_link_name="left_foot")],
),
base_link_name="base",
actuators={
"left_hip_position": PositionActuatorCfg(
joint_name="left_hip",
kp=100.0,
kv=2.0,
inherit_joint_range=True,
),
],
},
)
```

Dictionary keys name actuators; `joint_name` uses imported names before instance `prefix` / `suffix` decoration.
Declare all controlled joints in the dictionary: it replaces rather than appends to the imported actuator set.
Omit `actuators` or set it to `None` to preserve imported actuators; use `{}` to remove them.

`PositionActuatorCfg` uses position gain `kp` and absolute damping coefficient `kv` (default `0.0`).
It inherits `joint_name`, `ctrl_range` (control input limits), and `force_range` (actuator output limits) from `ActuatorCfg`.
Set `inherit_joint_range=True` to use imported joint limits as the control range; this cannot be combined with an explicit
`ctrl_range`. For direct joint effort control with unit transmission and gain, use `MotorActuatorCfg`.

### 3. Register the robot

Import and register a built-in robot in `motrix_envs/robot/__init__.py`:
Expand Down
37 changes: 28 additions & 9 deletions docs/source/zh_CN/user_guide/robots.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ motrix_envs/src/motrix_envs/robot/
```

MJCF 文件应只包含可跨场景复用的机器人模型,不要把地面、灯光或任务专属物体放进机器人文件。若使用 URDF,可以通过
`UrdfFileCfg` 的 `geoms`、`sites`、`joints` 和 `actuators` 补充 URDF 本身不包含的仿真属性。
`UrdfFileCfg` 的 `geoms`、`sites` 和 `joints` 补充 URDF 本身不包含的仿真属性。
MJCF 和 URDF 的执行器均通过机器人实例继承的 `BodyCfg.actuators` 字典声明。
从 `motrix_env_core.config.scene.actuator` 导入 `ActuatorCfg`、`PositionActuatorCfg` 和 `MotorActuatorCfg`。

### 2. 声明 `RobotCfg`

Expand Down Expand Up @@ -76,6 +78,7 @@ class MyRobot(RobotCfg):
| `base_link_name` | 模型连接到场景时使用的根链接名称 |
| `translation` / `rotation` | 可选的默认实例位姿;任务配置可以在实例化时覆盖 |
| `prefix` / `suffix` | 可选的模型元素名称前后缀,用于同一场景中的名称隔离 |
| `actuators` | `None` 保留导入执行器;`{}` 移除全部执行器;非空字典替换全部执行器 |
| `key_pose` | 共享一套明确关节顺序的有名姿态,例如 `default`、`crouch` |

`KeyPoseCfg.joint_names` 必须唯一且非空;每个姿态的数值数量必须与关节数量一致,所有值必须为有限数。这里只保存机器人
Expand All @@ -97,24 +100,40 @@ class MyHumanoid(HumanoidRobotCfg):

两个脚部名称必须非空且互不相同。`resolved_foot_link_names` 会自动应用机器人实例的 `prefix` 和 `suffix`。

URDF 机器人沿用相同的配置层次,并将 `model` 换成 `UrdfFileCfg`。需要 position actuator 或脚底 site 时,可以
在模型配置中显式补充:
URDF 机器人沿用相同的配置层次,并将 `model` 换成 `UrdfFileCfg`。site 仍属于模型配置,执行器属于 `RobotCfg`。
下面的最小示例假定模型包含 `base` 和 `left_foot` link,以及具有限位的 `left_hip` joint:

```python
model: UrdfFileCfg = UrdfFileCfg(
file=MY_ROBOT_ASSET_DIR / "my_robot.urdf",
sites=[SiteCfg(name="left_sole", parent_link_name="left_foot")],
actuators=[
PositionActuatorCfg(
from motrix_env_core.config.scene import RobotCfg
from motrix_env_core.config.scene.actuator import PositionActuatorCfg
from motrix_env_core.config.scene.urdf import SiteCfg, UrdfFileCfg

robot = RobotCfg(
model=UrdfFileCfg(
file="my_robot.urdf",
sites=[SiteCfg(name="left_sole", parent_link_name="left_foot")],
),
base_link_name="base",
actuators={
"left_hip_position": PositionActuatorCfg(
joint_name="left_hip",
kp=100.0,
kv=2.0,
inherit_joint_range=True,
),
],
},
)
```

字典键定义执行器名称;`joint_name` 使用应用实例 `prefix` / `suffix` 之前的导入名称。
字典替换而非追加导入执行器集合,因此需声明全部受控关节。省略 `actuators` 或设为 `None` 可保留导入执行器,
`{}` 则移除全部执行器。

`PositionActuatorCfg` 使用位置增益 `kp` 和绝对阻尼系数 `kv`(默认 `0.0`),并从 `ActuatorCfg` 继承
`joint_name`、`ctrl_range`(控制输入限幅)和 `force_range`(执行器输出限幅)。
设置 `inherit_joint_range=True` 可将导入关节限位用作控制范围,不能与显式 `ctrl_range` 同时设置。
直接控制关节力或力矩时,使用传动比与增益均为 1 的 `MotorActuatorCfg`。

### 3. 注册机器人

内置机器人在 `motrix_envs/robot/__init__.py` 中导入并注册:
Expand Down
4 changes: 2 additions & 2 deletions motrix_env_core/src/motrix_env_core/config/scene/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

from motrix_env_core.config.scene._utils import Vec2, Vec3, Vec4
from motrix_env_core.config.scene.actuator import ActuatorCfg, MotorActuatorCfg, PositionActuatorCfg
from motrix_env_core.config.scene.asset import (
HFieldAssetCfg,
MaterialCfg,
Expand Down Expand Up @@ -50,9 +51,7 @@
grid_terrain,
)
from motrix_env_core.config.scene.urdf import (
ActuatorCfg,
JointCfg,
PositionActuatorCfg,
SiteCfg,
UrdfFileCfg,
UrdfGeomCfg,
Expand Down Expand Up @@ -82,6 +81,7 @@
"MaterialCfg",
"MjcfFileCfg",
"ModelFileCfg",
"MotorActuatorCfg",
"NoiseTerrainGeneratorCfg",
"PositionActuatorCfg",
"ProceduralHFieldAssetCfg",
Expand Down
47 changes: 47 additions & 0 deletions motrix_env_core/src/motrix_env_core/config/scene/actuator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright Motphys Technology Co., Ltd. 2025, 2026
# SPDX-License-Identifier: Apache-2.0

"""Format-neutral joint actuator declarations for model instances."""

import math

from motrix_env_core.config import configclass
from motrix_env_core.config.scene._utils import Vec2, validate_range


@configclass(kw_only=True)
class ActuatorCfg:
"""Base declaration using undecorated names in the imported model."""

joint_name: str
ctrl_range: Vec2 | None = None
force_range: Vec2 | None = None

def validate(self) -> None:
if not self.joint_name:
raise ValueError("ActuatorCfg.joint_name must not be empty")
validate_range("ActuatorCfg.ctrl_range", self.ctrl_range)
validate_range("ActuatorCfg.force_range", self.force_range)


@configclass(kw_only=True)
class MotorActuatorCfg(ActuatorCfg):
"""Direct joint effort motor with unit transmission and gain."""


@configclass(kw_only=True)
class PositionActuatorCfg(ActuatorCfg):
"""Joint position servo with absolute damping coefficient kv."""

kp: float
kv: float = 0.0
inherit_joint_range: bool = False

def validate(self) -> None:
super().validate()
if self.inherit_joint_range and self.ctrl_range is not None:
raise ValueError("PositionActuatorCfg.inherit_joint_range is mutually exclusive with ctrl_range")
if not math.isfinite(self.kp) or self.kp <= 0.0:
raise ValueError(f"PositionActuatorCfg.kp must be positive and finite, got {self.kp}")
if not math.isfinite(self.kv) or self.kv < 0.0:
raise ValueError(f"PositionActuatorCfg.kv must be non-negative and finite, got {self.kv}")
19 changes: 16 additions & 3 deletions motrix_env_core/src/motrix_env_core/config/scene/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from motrix_env_core.config import configclass
from motrix_env_core.config.scene._utils import Vec3, Vec4, optional_vec, resolve_path
from motrix_env_core.config.scene.actuator import ActuatorCfg


@configclass(kw_only=True)
Expand Down Expand Up @@ -269,9 +270,12 @@ class BodyCfg(SceneObjCfg):

Holds the backend-neutral contract for attaching an external MJCF/URDF
model (a free-floating body tree): which file, which link is the attach
root, and optional placement/name decoration. Robots extend this with
key-pose and actuation semantics; prop objects (balls, boxes, ...) use
it directly.
root, placement/name decoration, and optional actuator declarations.
``actuators=None`` preserves the imported model; an explicit dict replaces
its actuators, and an empty dict removes them. Dict keys own actuator names. Names refer to the imported
model before prefix/suffix decoration. Replacement resets actuator control
and activation keyframe state, while preserving joint pose/velocity state.
Robots extend this with key-pose semantics; props use it directly.
"""

model: ModelFileCfg
Expand All @@ -280,6 +284,8 @@ class BodyCfg(SceneObjCfg):
rotation: Vec4 | None = None
prefix: str | None = None
suffix: str | None = None
# None preserves imported actuators; a dict replaces them (empty removes all).
actuators: dict[str, ActuatorCfg] | None = None

def validate(self, name: str) -> None:
super().validate(name)
Expand All @@ -290,6 +296,13 @@ def validate(self, name: str) -> None:
raise ValueError("BodyCfg.base_link_name must not be empty")
optional_vec("BodyCfg.translation", self.translation, 3)
optional_vec("BodyCfg.rotation", self.rotation, 4)
if self.actuators is not None:
for actuator_name, actuator in self.actuators.items():
if not isinstance(actuator_name, str) or not actuator_name:
raise ValueError("BodyCfg.actuators keys must be non-empty names")
if not isinstance(actuator, ActuatorCfg):
raise TypeError(f"BodyCfg.actuators must contain ActuatorCfg, got {type(actuator).__name__}")
actuator.validate()

def resolve_name(self, name: str) -> str:
return f"{self.prefix or ''}{name}{self.suffix or ''}"
Expand Down
47 changes: 2 additions & 45 deletions motrix_env_core/src/motrix_env_core/config/scene/urdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

from motrix_env_core.config import configclass
from motrix_env_core.config.scene._utils import Vec2, Vec3, Vec4, optional_vec, resolve_path, validate_range
from motrix_env_core.config.scene._utils import Vec3, Vec4, optional_vec, resolve_path
from motrix_env_core.config.scene.base import ModelFileCfg


Expand Down Expand Up @@ -62,54 +62,13 @@ def validate(self) -> None:
optional_vec("SiteCfg.rotation", self.rotation, 4)


@configclass(kw_only=True)
class ActuatorCfg:
"""An actuator added to an imported robot before scene composition."""

joint_name: str
name: str | None = None
ctrl_range: Vec2 | None = None
force_range: Vec2 | None = None

@property
def actuator_name(self) -> str:
return self.joint_name if self.name is None else self.name

def validate(self) -> None:
if not self.joint_name:
raise ValueError("ActuatorCfg.joint_name must not be empty")
if not self.actuator_name:
raise ValueError("ActuatorCfg.name must not be empty")
validate_range("ActuatorCfg.ctrl_range", self.ctrl_range)
validate_range("ActuatorCfg.force_range", self.force_range)


@configclass(kw_only=True)
class PositionActuatorCfg(ActuatorCfg):
"""A joint position servo using absolute damping coefficient ``kv``."""

kp: float
kv: float = 0.0
inherit_joint_range: bool = False

def validate(self) -> None:
super().validate()
if self.inherit_joint_range and self.ctrl_range is not None:
raise ValueError("PositionActuatorCfg.inherit_joint_range is mutually exclusive with ctrl_range")
if self.kp <= 0.0:
raise ValueError(f"PositionActuatorCfg.kp must be positive, got {self.kp}")
if self.kv < 0.0:
raise ValueError(f"PositionActuatorCfg.kv must be non-negative, got {self.kv}")


@configclass(kw_only=True)
class UrdfFileCfg(ModelFileCfg):
"""A URDF model augmented with simulation-only sites, joints, and actuators."""
"""A URDF model augmented with simulation-only sites and joint properties."""

geoms: list[UrdfGeomCfg] = []
sites: list[SiteCfg] = []
joints: list[JointCfg] = []
actuators: list[ActuatorCfg] = []

def validate(self) -> None:
super().validate()
Expand All @@ -123,5 +82,3 @@ def validate(self) -> None:
site.validate()
for joint in self.joints:
joint.validate()
for actuator in self.actuators:
actuator.validate()
48 changes: 48 additions & 0 deletions motrix_env_core/tests/test_actuator_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright Motphys Technology Co., Ltd. 2025, 2026
# SPDX-License-Identifier: Apache-2.0

"""Format-neutral actuator declarations belong to model instances."""

import pytest
from omegaconf import OmegaConf

from motrix_env_core.config.scene.actuator import MotorActuatorCfg, PositionActuatorCfg
from motrix_env_core.config.scene.base import BodyCfg, RobotCfg
from motrix_env_core.config.scene.mjcf import MjcfFileCfg


def test_body_actuator_defaults_and_instance_isolation():
model = MjcfFileCfg(file="unused.xml")
preserved = BodyCfg(model=model, base_link_name="base")
removed = BodyCfg(model=model, base_link_name="base", actuators={})
assert preserved.actuators is None
assert removed.actuators == {}
removed.actuators["motor"] = MotorActuatorCfg(joint_name="hinge")
assert BodyCfg(model=model, base_link_name="base", actuators={}).actuators == {}
robot = RobotCfg(model=model, base_link_name="base", actuators={"motor": MotorActuatorCfg(joint_name="hinge")})
assert robot.actuators["motor"].joint_name == "hinge"


@pytest.mark.parametrize("kp,kv", [(0.0, 0.0), (-1.0, 0.0), (float("nan"), 0.0), (1.0, -1.0), (1.0, float("inf"))])
def test_position_actuator_validates_finite_gains(kp, kv):
with pytest.raises(ValueError):
PositionActuatorCfg(joint_name="hinge", kp=kp, kv=kv).validate()


def test_named_actuator_override_preserves_other_fields():
body = BodyCfg(
model=MjcfFileCfg(file="unused.xml"),
base_link_name="base",
actuators={"servo": PositionActuatorCfg(joint_name="hinge", kp=10.0, kv=1.0)},
)
cfg = OmegaConf.merge(OmegaConf.structured(body), {"actuators": {"servo": {"kp": 20.0}}})
overridden = OmegaConf.to_object(cfg)
assert overridden.actuators["servo"].kp == 20.0
assert overridden.actuators["servo"].kv == 1.0
assert overridden.actuators["servo"].joint_name == "hinge"
assert body.actuators["servo"].kp == 10.0


def test_position_actuator_range_sources_are_exclusive():
with pytest.raises(ValueError, match="mutually exclusive"):
PositionActuatorCfg(joint_name="hinge", kp=1.0, inherit_joint_range=True, ctrl_range=(-1.0, 1.0)).validate()
Loading
Loading