Some checks failed
nightly / Test against latest dependencies (py3.10) (push) Has been cancelled
nightly / Test against latest dependencies (py3.13) (push) Has been cancelled
tests / tests (3.13, locked) (push) Has been cancelled
tests / tests (3.13, unlocked) (push) Has been cancelled
tests / pyright (3.10) (push) Has been cancelled
tests / lint-format (push) Has been cancelled
tests / tests (3.10, locked) (push) Has been cancelled
tests / tests (3.11, locked) (push) Has been cancelled
tests / tests (3.12, locked) (push) Has been cancelled
tests / pyright (3.11) (push) Has been cancelled
tests / pyright (3.12) (push) Has been cancelled
tests / pyright (3.13) (push) Has been cancelled
tests / ty-check (3.10) (push) Has been cancelled
tests / ty-check (3.11) (push) Has been cancelled
tests / ty-check (3.12) (push) Has been cancelled
tests / ty-check (3.13) (push) Has been cancelled
tests / stubs (push) Has been cancelled
tests / smoke-test (push) Has been cancelled
Docker / check_paths (push) Has been cancelled
docs / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Upstream: https://github.com/michaelgillett/mjlab Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047 Upstream-Branch: main
319 lines
8.6 KiB
Python
319 lines
8.6 KiB
Python
"""Tests for actions."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
import mujoco
|
|
import pytest
|
|
import torch
|
|
from conftest import get_test_device, load_fixture_xml
|
|
|
|
from mjlab.actuator.actuator import TransmissionType
|
|
from mjlab.actuator.builtin_actuator import BuiltinMotorActuatorCfg
|
|
from mjlab.entity import Entity, EntityArticulationInfoCfg, EntityCfg
|
|
from mjlab.envs import ManagerBasedRlEnv
|
|
from mjlab.envs.mdp.actions import (
|
|
JointPositionActionCfg,
|
|
RelativeJointPositionActionCfg,
|
|
SiteEffortActionCfg,
|
|
TendonEffortActionCfg,
|
|
TendonLengthActionCfg,
|
|
TendonVelocityActionCfg,
|
|
)
|
|
from mjlab.sim.sim import Simulation, SimulationCfg
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def device():
|
|
return get_test_device()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def fixtures_dir():
|
|
return Path(__file__).parent / "fixtures"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def tendon_finger_entity(fixtures_dir, device):
|
|
return make_entity(
|
|
fixtures_dir / "tendon_finger.xml",
|
|
("finger_tendon",),
|
|
TransmissionType.TENDON,
|
|
device,
|
|
from_file=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def fixed_base_entity(fixtures_dir, device):
|
|
return make_entity(
|
|
fixtures_dir / "fixed_base_articulated.xml",
|
|
("joint.*",),
|
|
TransmissionType.JOINT,
|
|
device,
|
|
from_file=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def floating_base_entity(device):
|
|
return make_entity(
|
|
load_fixture_xml("floating_base_articulated"),
|
|
("joint.*",),
|
|
TransmissionType.JOINT,
|
|
device,
|
|
from_file=False,
|
|
)
|
|
|
|
|
|
def make_entity(xml_or_path, target_expr, transmission_type, device, from_file=False):
|
|
"""Create and initialize entity."""
|
|
|
|
def spec_fn():
|
|
if from_file:
|
|
return mujoco.MjSpec.from_file(str(xml_or_path))
|
|
return mujoco.MjSpec.from_string(xml_or_path)
|
|
|
|
cfg = EntityCfg(
|
|
spec_fn=spec_fn,
|
|
articulation=EntityArticulationInfoCfg(
|
|
actuators=(
|
|
BuiltinMotorActuatorCfg(
|
|
target_names_expr=target_expr,
|
|
transmission_type=transmission_type,
|
|
effort_limit=10.0,
|
|
),
|
|
)
|
|
),
|
|
)
|
|
entity = Entity(cfg)
|
|
model = entity.compile()
|
|
sim = Simulation(num_envs=4, cfg=SimulationCfg(), model=model, device=device)
|
|
entity.initialize(model, sim.model, sim.data, device)
|
|
return entity
|
|
|
|
|
|
def make_env(entity, name, device):
|
|
"""Create mock environment."""
|
|
env = Mock(spec=ManagerBasedRlEnv)
|
|
env.num_envs = 4
|
|
env.device = device
|
|
env.scene = {name: entity}
|
|
return env
|
|
|
|
|
|
def test_base_action_applies_scale_and_offset(tendon_finger_entity, device):
|
|
"""BaseAction: processed = raw * scale + offset."""
|
|
entity = tendon_finger_entity
|
|
env = make_env(entity, "finger", device)
|
|
|
|
cfg = TendonLengthActionCfg(
|
|
entity_name="finger",
|
|
actuator_names=("finger_tendon",),
|
|
scale=2.0,
|
|
offset=0.5,
|
|
)
|
|
action = cfg.build(env)
|
|
|
|
raw = torch.tensor([[1.0], [2.0], [3.0], [4.0]], device=device)
|
|
action.process_actions(raw)
|
|
|
|
assert torch.allclose(action._processed_actions, raw * 2.0 + 0.5)
|
|
|
|
|
|
def test_base_action_reset_zeros_specific_envs(tendon_finger_entity, device):
|
|
"""BaseAction.reset() zeros raw_action for specified env_ids only."""
|
|
entity = tendon_finger_entity
|
|
env = make_env(entity, "finger", device)
|
|
|
|
cfg = TendonLengthActionCfg(entity_name="finger", actuator_names=("finger_tendon",))
|
|
action = cfg.build(env)
|
|
|
|
action.process_actions(torch.ones(4, 1, device=device))
|
|
action.reset(env_ids=torch.tensor([0, 2], device=device))
|
|
|
|
assert torch.all(action.raw_action[[0, 2]] == 0.0)
|
|
assert torch.all(action.raw_action[[1, 3]] == 1.0)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"cfg_cls,target_attr,fixture,target_expr,transmission,entity_name",
|
|
[
|
|
# Joints.
|
|
(
|
|
JointPositionActionCfg,
|
|
"joint_pos_target",
|
|
"floating_base_articulated",
|
|
("joint.*",),
|
|
TransmissionType.JOINT,
|
|
"robot",
|
|
),
|
|
# Tendons.
|
|
(
|
|
TendonLengthActionCfg,
|
|
"tendon_len_target",
|
|
"tendon_finger.xml",
|
|
("finger_tendon",),
|
|
TransmissionType.TENDON,
|
|
"finger",
|
|
),
|
|
(
|
|
TendonVelocityActionCfg,
|
|
"tendon_vel_target",
|
|
"tendon_finger.xml",
|
|
("finger_tendon",),
|
|
TransmissionType.TENDON,
|
|
"finger",
|
|
),
|
|
(
|
|
TendonEffortActionCfg,
|
|
"tendon_effort_target",
|
|
"tendon_finger.xml",
|
|
("finger_tendon",),
|
|
TransmissionType.TENDON,
|
|
"finger",
|
|
),
|
|
# Sites.
|
|
(
|
|
SiteEffortActionCfg,
|
|
"site_effort_target",
|
|
"quadcopter.xml",
|
|
("rotor_.*",),
|
|
TransmissionType.SITE,
|
|
"drone",
|
|
),
|
|
],
|
|
)
|
|
def test_action_sets_entity_target(
|
|
fixtures_dir,
|
|
device,
|
|
cfg_cls,
|
|
target_attr,
|
|
fixture,
|
|
target_expr,
|
|
transmission,
|
|
entity_name,
|
|
):
|
|
"""Each action type writes to correct entity.data field."""
|
|
if fixture.endswith(".xml"):
|
|
entity = make_entity(
|
|
fixtures_dir / fixture, target_expr, transmission, device, from_file=True
|
|
)
|
|
else:
|
|
entity = make_entity(
|
|
load_fixture_xml(fixture), target_expr, transmission, device, from_file=False
|
|
)
|
|
|
|
env = make_env(entity, entity_name, device)
|
|
cfg = cfg_cls(entity_name=entity_name, actuator_names=target_expr)
|
|
action = cfg.build(env)
|
|
|
|
target = torch.arange(4 * action.action_dim, device=device, dtype=torch.float32)
|
|
target = target.reshape(4, action.action_dim) * 0.1
|
|
|
|
action.process_actions(target)
|
|
action.apply_actions()
|
|
|
|
entity_target = getattr(entity.data, target_attr)
|
|
assert torch.allclose(entity_target, target)
|
|
|
|
|
|
def test_base_action_clip(fixed_base_entity, device):
|
|
"""BaseAction: clip clamps only matched actuators; others stay unclipped."""
|
|
entity = fixed_base_entity
|
|
env = make_env(entity, "robot", device)
|
|
|
|
# Clip only joint1; joint2 should remain unclipped.
|
|
cfg = JointPositionActionCfg(
|
|
entity_name="robot",
|
|
actuator_names=("joint.*",),
|
|
scale=1.0,
|
|
use_default_offset=False,
|
|
clip={"joint1": (-0.5, 0.5)},
|
|
)
|
|
action = cfg.build(env)
|
|
|
|
# joint1=2.0 should be clipped to 0.5, joint2=2.0 should pass through.
|
|
raw = torch.tensor([[2.0, 2.0]], device=device).expand(4, -1)
|
|
action.process_actions(raw)
|
|
|
|
assert torch.allclose(
|
|
action._processed_actions[:, 0], torch.tensor(0.5, device=device)
|
|
)
|
|
assert torch.allclose(
|
|
action._processed_actions[:, 1], torch.tensor(2.0, device=device)
|
|
)
|
|
|
|
|
|
def test_relative_joint_position_zero_action(floating_base_entity, device):
|
|
"""With zero action, targets equal current_pos."""
|
|
entity = floating_base_entity
|
|
env = make_env(entity, "robot", device)
|
|
|
|
cfg = RelativeJointPositionActionCfg(
|
|
entity_name="robot", actuator_names=("joint.*",), scale=1.0
|
|
)
|
|
action = cfg.build(env)
|
|
|
|
current_pos = entity.data.joint_pos[:, action.target_ids].clone()
|
|
|
|
action.process_actions(torch.zeros(4, action.action_dim, device=device))
|
|
action.apply_actions()
|
|
|
|
assert torch.allclose(entity.data.joint_pos_target[:, action.target_ids], current_pos)
|
|
|
|
|
|
def test_relative_joint_position_nonzero_action(floating_base_entity, device):
|
|
"""With nonzero action, targets shift by action * scale from current."""
|
|
entity = floating_base_entity
|
|
env = make_env(entity, "robot", device)
|
|
|
|
scale = 0.1
|
|
cfg = RelativeJointPositionActionCfg(
|
|
entity_name="robot", actuator_names=("joint.*",), scale=scale
|
|
)
|
|
action = cfg.build(env)
|
|
|
|
current_pos = entity.data.joint_pos[:, action.target_ids].clone()
|
|
|
|
raw = torch.ones(4, action.action_dim, device=device)
|
|
action.process_actions(raw)
|
|
action.apply_actions()
|
|
|
|
expected = current_pos + raw * scale
|
|
assert torch.allclose(entity.data.joint_pos_target[:, action.target_ids], expected)
|
|
|
|
|
|
def test_relative_joint_position_ignores_encoder_bias(floating_base_entity, device):
|
|
"""Encoder bias must not affect the target: target = current_pos + delta."""
|
|
entity = floating_base_entity
|
|
env = make_env(entity, "robot", device)
|
|
|
|
cfg = RelativeJointPositionActionCfg(
|
|
entity_name="robot", actuator_names=("joint.*",), scale=1.0
|
|
)
|
|
action = cfg.build(env)
|
|
|
|
entity.data.encoder_bias[:, action.target_ids] = 0.05
|
|
|
|
current_pos = entity.data.joint_pos[:, action.target_ids].clone()
|
|
delta = 0.1
|
|
raw = torch.full((4, action.action_dim), delta, device=device)
|
|
action.process_actions(raw)
|
|
action.apply_actions()
|
|
|
|
assert torch.allclose(
|
|
entity.data.joint_pos_target[:, action.target_ids], current_pos + delta
|
|
)
|
|
|
|
# Reset bias so this shared fixture doesn't affect other tests.
|
|
entity.data.encoder_bias[:, action.target_ids] = 0.0
|
|
|
|
|
|
def test_relative_joint_position_offset_raises(floating_base_entity, device):
|
|
"""Setting offset on RelativeJointPositionActionCfg raises ValueError."""
|
|
with pytest.raises(ValueError, match="offset"):
|
|
RelativeJointPositionActionCfg(
|
|
entity_name="robot", actuator_names=("joint.*",), offset=0.5
|
|
)
|