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
183 lines
5.4 KiB
Python
183 lines
5.4 KiB
Python
"""Tests for BuiltinActuatorGroup (unified delayed + non-delayed)."""
|
|
|
|
import mujoco
|
|
import pytest
|
|
import torch
|
|
from conftest import get_test_device, load_fixture_xml
|
|
|
|
from mjlab.actuator import (
|
|
BuiltinPositionActuatorCfg,
|
|
IdealPdActuatorCfg,
|
|
)
|
|
from mjlab.entity import Entity, EntityArticulationInfoCfg, EntityCfg
|
|
from mjlab.sim.sim import Simulation, SimulationCfg
|
|
|
|
ROBOT_XML = load_fixture_xml("floating_base_articulated")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def device():
|
|
return get_test_device()
|
|
|
|
|
|
def make_entity(actuator_cfgs, num_envs, device):
|
|
cfg = EntityCfg(
|
|
spec_fn=lambda: mujoco.MjSpec.from_string(ROBOT_XML),
|
|
articulation=EntityArticulationInfoCfg(actuators=actuator_cfgs),
|
|
)
|
|
entity = Entity(cfg)
|
|
model = entity.compile()
|
|
sim = Simulation(num_envs=num_envs, cfg=SimulationCfg(), model=model, device=device)
|
|
entity.initialize(model, sim.model, sim.data, device)
|
|
return entity, sim
|
|
|
|
|
|
def test_fused_group_created(device):
|
|
"""Delayed builtins with same config are fused into one group."""
|
|
cfg1 = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint1",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=1,
|
|
delay_max_lag=3,
|
|
)
|
|
cfg2 = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint2",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=1,
|
|
delay_max_lag=3,
|
|
)
|
|
entity, _ = make_entity((cfg1, cfg2), num_envs=2, device=device)
|
|
|
|
assert len(entity._builtin_group._delayed_groups) == 1
|
|
assert len(entity._custom_actuators) == 0
|
|
|
|
|
|
def test_different_delay_configs_separate_groups(device):
|
|
"""Delayed builtins with different delay configs get separate groups."""
|
|
cfg1 = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint1",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=1,
|
|
delay_max_lag=3,
|
|
)
|
|
cfg2 = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint2",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=5,
|
|
delay_max_lag=10,
|
|
)
|
|
entity, _ = make_entity((cfg1, cfg2), num_envs=2, device=device)
|
|
|
|
assert len(entity._builtin_group._delayed_groups) == 2
|
|
|
|
|
|
def test_non_builtin_delayed_not_fused(device):
|
|
"""Builtin delay fusion does not absorb ideal PD actuators.
|
|
|
|
The ideal PD actuator is fused separately into the PD group, not into the
|
|
builtin group and not left on the per-actuator custom path.
|
|
"""
|
|
delayed_builtin = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint1",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=1,
|
|
delay_max_lag=3,
|
|
)
|
|
custom = IdealPdActuatorCfg(
|
|
target_names_expr=("joint2",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
effort_limit=100.0,
|
|
)
|
|
entity, _ = make_entity((delayed_builtin, custom), num_envs=2, device=device)
|
|
|
|
assert len(entity._builtin_group._delayed_groups) == 1
|
|
assert len(entity._fused_actuator_group._groups) == 1
|
|
assert len(entity._custom_actuators) == 0
|
|
|
|
|
|
def test_delayed_controls_written(device):
|
|
"""Fused delayed builtins write controls to sim correctly."""
|
|
cfg = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint.*",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=0,
|
|
delay_max_lag=0,
|
|
)
|
|
entity, sim = make_entity((cfg,), num_envs=1, device=device)
|
|
|
|
target = torch.tensor([[0.5, -0.3]], device=device)
|
|
entity.set_joint_position_target(target)
|
|
entity.write_data_to_sim()
|
|
|
|
# With lag=0, output equals input.
|
|
assert torch.allclose(sim.data.ctrl[0], target[0])
|
|
|
|
|
|
def test_delay_actually_delays(device):
|
|
"""With nonzero fixed lag, a new target takes lag steps to appear."""
|
|
cfg = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint.*",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=2,
|
|
delay_max_lag=2,
|
|
)
|
|
entity, sim = make_entity((cfg,), num_envs=1, device=device)
|
|
|
|
target_a = torch.tensor([[1.0, 2.0]], device=device)
|
|
target_b = torch.tensor([[5.0, 6.0]], device=device)
|
|
|
|
# Fill the buffer with target_a so lag clamp doesn't mask the test.
|
|
for _ in range(3):
|
|
entity.set_joint_position_target(target_a)
|
|
entity.write_data_to_sim()
|
|
assert torch.allclose(sim.data.ctrl[0], target_a[0])
|
|
|
|
# Now send target_b. With lag=2, it takes 2 more steps to arrive.
|
|
entity.set_joint_position_target(target_b)
|
|
entity.write_data_to_sim()
|
|
assert torch.allclose(sim.data.ctrl[0], target_a[0]) # still old
|
|
|
|
entity.set_joint_position_target(target_b)
|
|
entity.write_data_to_sim()
|
|
assert torch.allclose(sim.data.ctrl[0], target_a[0]) # still old
|
|
|
|
entity.set_joint_position_target(target_b)
|
|
entity.write_data_to_sim()
|
|
assert torch.allclose(sim.data.ctrl[0], target_b[0]) # now arrives
|
|
|
|
|
|
def test_reset_clears_buffer(device):
|
|
"""Reset clears delay buffers so old values don't leak through."""
|
|
cfg = BuiltinPositionActuatorCfg(
|
|
target_names_expr=("joint.*",),
|
|
stiffness=50.0,
|
|
damping=5.0,
|
|
delay_min_lag=2,
|
|
delay_max_lag=2,
|
|
)
|
|
entity, sim = make_entity((cfg,), num_envs=1, device=device)
|
|
|
|
target_a = torch.tensor([[1.0, 2.0]], device=device)
|
|
target_b = torch.tensor([[5.0, 6.0]], device=device)
|
|
|
|
# Fill buffer with target_a.
|
|
for _ in range(3):
|
|
entity.set_joint_position_target(target_a)
|
|
entity.write_data_to_sim()
|
|
assert torch.allclose(sim.data.ctrl[0], target_a[0])
|
|
|
|
# Reset, then fill with target_b. Old target_a must not appear.
|
|
entity.reset(torch.tensor([0], device=device))
|
|
for _ in range(3):
|
|
entity.set_joint_position_target(target_b)
|
|
entity.write_data_to_sim()
|
|
assert torch.allclose(sim.data.ctrl[0], target_b[0])
|