mjlab/tests/test_builtin_dcmotor_actuator.py
Upstream Snapshot 32a241c28f
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
Import upstream snapshot c19f713c415a699a79d71cd96aa13c3104a05047
Upstream: https://github.com/michaelgillett/mjlab
Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047
Upstream-Branch: main
2026-08-28 15:42:17 +08:00

654 lines
22 KiB
Python

"""Tests for BuiltinDcMotorActuator.
Covers wiring of MuJoCo's native ``<dcmotor>`` element through mjlab: the
three input modes (voltage / position / velocity), torque saturation,
config validation, and DR integration.
"""
import math
from unittest.mock import Mock
import mujoco
import pytest
import torch
from conftest import (
create_entity_with_actuator,
get_test_device,
initialize_entity,
load_fixture_xml,
)
from mjlab.actuator import (
BuiltinDcMotorActuator,
BuiltinDcMotorActuatorCfg,
DcMotorDatasheetParams,
DcMotorInputMode,
DcMotorPhysicalParams,
)
from mjlab.actuator.actuator import TransmissionType
from mjlab.entity import Entity, EntityArticulationInfoCfg, EntityCfg
from mjlab.envs.mdp import dr
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.scene import Scene, SceneCfg
from mjlab.sim.sim import Simulation, SimulationCfg
ROBOT_XML = load_fixture_xml("floating_base_articulated")
# Motor characterization used throughout (resolves to K=0.24, R=2.88).
V_NOM, TAU_STALL, OMEGA_NL = 24.0, 2.0, 100.0
K = V_NOM / OMEGA_NL
R = K * V_NOM / TAU_STALL
@pytest.fixture(scope="module")
def device():
return get_test_device()
DATASHEET = DcMotorDatasheetParams(
nominal_voltage=V_NOM, stall_torque=TAU_STALL, no_load_speed=OMEGA_NL
)
def _make_cfg(
*,
mode: DcMotorInputMode = DcMotorInputMode.POSITION,
stiffness=5.0,
damping=0.5,
voltage_limit=24.0,
**extra,
) -> BuiltinDcMotorActuatorCfg:
"""Build a cfg with sensible PID defaults. ``extra`` forwards any other
BuiltinDcMotorActuatorCfg kwarg (effort_limit, integral_gain, thermal,
delay_*, etc.)."""
return BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=mode,
motor_params=DATASHEET,
stiffness=stiffness,
damping=damping,
voltage_limit=voltage_limit,
**extra,
)
def _make_entity(**kwargs) -> Entity:
return create_entity_with_actuator(ROBOT_XML, _make_cfg(**kwargs))
def _make_initialized(device, **kwargs):
"""Build entity from cfg kwargs and initialize it through the sim."""
return initialize_entity(_make_entity(**kwargs), device)
def _drive(
entity: Entity,
sim,
device: str,
*,
pos_target=None,
vel_target=None,
effort_target=None,
q0=None,
qd0=None,
) -> None:
zero = torch.zeros(1, 2, device=device)
entity.write_joint_state_to_sim(
position=q0 if q0 is not None else zero,
velocity=qd0 if qd0 is not None else zero,
)
entity.set_joint_position_target(pos_target if pos_target is not None else zero)
entity.set_joint_velocity_target(vel_target if vel_target is not None else zero)
entity.set_joint_effort_target(effort_target if effort_target is not None else zero)
entity.write_data_to_sim()
sim.forward()
# Wiring sanity.
def test_kr_packed_into_gainprm(device):
"""The XML compiler derives K and R from the nominal triplet."""
_, sim = initialize_entity(_make_entity(effort_limit=1.5), device)
m = sim.mj_model
for i in range(2):
assert m.actuator_gainprm[i, 0] == pytest.approx(R, abs=1e-6)
assert m.actuator_gainprm[i, 1] == pytest.approx(K, abs=1e-6)
assert m.actuator_gainprm[i, 4] == pytest.approx(5.0) # kp
assert m.actuator_gainprm[i, 6] == pytest.approx(0.5) # kd
assert m.actuator_gainprm[i, 7] == pytest.approx(24.0) # Vmax
assert m.actuator_gainprm[i, 8] == pytest.approx(1.0) # input_mode=position
assert m.actuator_gaintype[i] == mujoco.mjtGain.mjGAIN_DCMOTOR
assert m.actuator_biastype[i] == mujoco.mjtBias.mjBIAS_DCMOTOR
# No activation state: ki=0, no inductance, no thermal/lugre/slew.
assert m.actuator_actnum[i] == 0
def test_motor_const_path(device):
"""Physical params pack K = sqrt(Kt*Ke) and R verbatim."""
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DcMotorPhysicalParams(kt=0.1, ke=0.05, resistance=2.0),
)
_, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
m = sim.mj_model
for i in range(2):
assert m.actuator_gainprm[i, 0] == pytest.approx(2.0, abs=1e-6)
assert m.actuator_gainprm[i, 1] == pytest.approx((0.1 * 0.05) ** 0.5, abs=1e-6)
# Stateless motor physics.
def test_voltage_mode_steady_state(device):
"""At rest, ctrl = V -> tau = K * V / R."""
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
)
entity, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
V = torch.tensor([[10.0, -5.0]], device=device)
_drive(entity, sim, device, effort_target=V)
v_adr = entity.indexing.joint_v_adr
expected = K * V[0] / R
assert torch.allclose(sim.data.qfrc_actuator[0, v_adr], expected, atol=1e-4)
def test_voltage_mode_voltage_limit_zero_is_noop(device):
"""Docstring promises ``voltage_limit=0`` disables clamping. Verify against
MuJoCo's ``dcmotor_voltage`` (which only clamps when ``Vmax > 0``)."""
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
voltage_limit=0.0,
)
entity, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
V = torch.tensor([[1000.0, 0.0]], device=device) # absurdly high voltage.
_drive(entity, sim, device, effort_target=V)
v_adr = entity.indexing.joint_v_adr
expected = K * V[0] / R
assert torch.allclose(sim.data.qfrc_actuator[0, v_adr], expected, atol=1e-2)
def test_back_emf_reduces_torque_at_velocity(device):
"""Same V, joint moving at omega: tau = K * (V - K * omega) / R."""
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
)
entity, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
V = torch.tensor([[10.0, 0.0]], device=device)
omega0 = torch.tensor([[2.0, 0.0]], device=device)
_drive(entity, sim, device, effort_target=V, qd0=omega0)
v_adr = entity.indexing.joint_v_adr
expected = K * (V[0] - K * omega0[0]) / R
assert torch.allclose(sim.data.qfrc_actuator[0, v_adr], expected, atol=1e-4)
def test_position_mode_pid_at_rest(device):
"""kd=0, no Vmax clamp: tau = K * kp * (target - q) / R."""
# voltage_limit must be >0 (cfg invariant), pick it big enough not to clamp.
entity, sim = initialize_entity(
_make_entity(damping=0.0, voltage_limit=1000.0), device
)
pos = torch.tensor([[0.1, -0.05]], device=device)
_drive(entity, sim, device, pos_target=pos)
v_adr = entity.indexing.joint_v_adr
expected = K * 5.0 * pos[0] / R
assert torch.allclose(sim.data.qfrc_actuator[0, v_adr], expected, atol=1e-4)
def test_position_mode_voltage_clamp(device):
"""Huge position error -> PID voltage saturates at Vmax."""
entity, sim = initialize_entity(
_make_entity(stiffness=100.0, damping=0.0, voltage_limit=2.0),
device,
)
# kp * err = 100 * 0.5 = 50 V, well above Vmax=2.
pos = torch.tensor([[0.5, 0.0]], device=device)
_drive(entity, sim, device, pos_target=pos)
v_adr = entity.indexing.joint_v_adr
qfrc = sim.data.qfrc_actuator[0, v_adr]
expected_first = K * 2.0 / R # tau at clamped V.
assert qfrc[0].item() == pytest.approx(expected_first, abs=1e-4)
assert qfrc[1].item() == pytest.approx(0.0, abs=1e-4)
def test_velocity_mode_pid(device):
"""P-only velocity tracking: tau = K * kp * (target - qdot) / R."""
entity, sim = initialize_entity(
_make_entity(mode=DcMotorInputMode.VELOCITY, damping=0.0, voltage_limit=1000.0),
device,
)
qd0 = torch.tensor([[1.0, 0.0]], device=device)
vel_target = torch.tensor([[3.0, 0.0]], device=device)
_drive(entity, sim, device, vel_target=vel_target, qd0=qd0)
v_adr = entity.indexing.joint_v_adr
# back-EMF subtracts K*omega; this is folded into the dcmotor bias.
# voltage = kp*(target - qdot); tau = K*(voltage - K*omega)/R.
voltage = 5.0 * (vel_target[0] - qd0[0])
expected = K * (voltage - K * qd0[0]) / R
assert torch.allclose(sim.data.qfrc_actuator[0, v_adr], expected, atol=1e-4)
def test_effort_limit_clamps_torque(device):
"""forcerange clamps the algebraic torque output."""
entity, sim = initialize_entity(
_make_entity(stiffness=100.0, damping=0.0, voltage_limit=1000.0, effort_limit=0.1),
device,
)
m = sim.mj_model
for i in range(2):
assert m.actuator_forcelimited[i] == 1
assert m.actuator_forcerange[i, 0] == pytest.approx(-0.1)
assert m.actuator_forcerange[i, 1] == pytest.approx(0.1)
# Unclamped tau would be K * 100 * 0.5 / R ~= K*50/R, well above 0.1.
pos = torch.tensor([[0.5, 0.0]], device=device)
_drive(entity, sim, device, pos_target=pos)
v_adr = entity.indexing.joint_v_adr
qfrc = sim.data.qfrc_actuator[0, v_adr]
assert qfrc[0].item() == pytest.approx(0.1, abs=1e-4)
assert qfrc[1].item() == pytest.approx(0.0, abs=1e-4)
# Cogging.
def test_cogging_packed_into_biasprm(device):
"""``cogging=(A, Np, phi)`` packs into ``biasprm[0:3]``."""
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
cogging=(0.5, 4.0, 0.1),
)
_, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
m = sim.mj_model
for i in range(2):
assert m.actuator_biasprm[i, 0] == pytest.approx(0.5)
assert m.actuator_biasprm[i, 1] == pytest.approx(4.0)
assert m.actuator_biasprm[i, 2] == pytest.approx(0.1)
def test_cogging_contributes_torque(device):
"""At ctrl=0 (no electromagnetic torque), qfrc_actuator equals the cogging
term ``A * sin(Np * q + phi)`` evaluated at the joint angle."""
A, Np, phi = 0.5, 4.0, 0.1
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
cogging=(A, Np, phi),
)
entity, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
q0, q1 = 0.3, -0.2
_drive(
entity,
sim,
device,
q0=torch.tensor([[q0, q1]], device=device),
effort_target=torch.zeros(1, 2, device=device),
)
v_adr = entity.indexing.joint_v_adr
qfrc = sim.data.qfrc_actuator[0, v_adr]
assert qfrc[0].item() == pytest.approx(A * math.sin(Np * q0 + phi), abs=1e-5)
assert qfrc[1].item() == pytest.approx(A * math.sin(Np * q1 + phi), abs=1e-5)
def test_cogging_bypasses_effort_limit(device):
"""Cogging is added *after* the forcerange clamp (MuJoCo's intentional
model: ``effort_limit`` bounds electromagnetic torque, cogging is
mechanical). Total torque can exceed ``effort_limit`` by up to the
cogging amplitude."""
A, Np, phi = 0.5, 0.0, math.pi / 2 # sin(pi/2)=1, so cogging = A at any q.
cfg = BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
cogging=(A, Np, phi),
effort_limit=0.05, # An order of magnitude below A.
)
entity, sim = initialize_entity(create_entity_with_actuator(ROBOT_XML, cfg), device)
# Pick a voltage large enough that the electromagnetic torque alone
# would saturate forcerange at +/- 0.05.
V = torch.tensor([[100.0, 0.0]], device=device)
_drive(entity, sim, device, effort_target=V)
v_adr = entity.indexing.joint_v_adr
qfrc = sim.data.qfrc_actuator[0, v_adr]
# joint1: electromagnetic clamped to +0.05, plus cogging A=0.5.
assert qfrc[0].item() == pytest.approx(0.05 + A, abs=1e-5)
# joint2: zero voltage, electromagnetic=0, only cogging.
assert qfrc[1].item() == pytest.approx(A, abs=1e-5)
# Optional stateful extensions (integral, slew, inductance, thermal, LuGre).
# Each behavior check compares against a baseline with the feature disabled
# so that removing the wiring in edit_spec causes the comparison to fail.
def _step_n(entity, sim, device, n: int, *, pos_target=None, eff_target=None):
zero = torch.zeros(1, 2, device=device)
entity.write_joint_state_to_sim(position=zero, velocity=zero)
for _ in range(n):
entity.set_joint_position_target(pos_target if pos_target is not None else zero)
entity.set_joint_velocity_target(zero)
entity.set_joint_effort_target(eff_target if eff_target is not None else zero)
entity.write_data_to_sim()
sim.step()
def _qfrc(entity, sim) -> torch.Tensor:
return sim.data.qfrc_actuator[0, entity.indexing.joint_v_adr].clone()
def test_integral_gain_ramps_torque(device):
"""Integrator in position mode ramps torque over time even with ``kp``
and ``kd`` near zero."""
# stiffness must be > 0 (validation); choose tiny so ki dominates.
base = dict(
mode=DcMotorInputMode.POSITION, stiffness=1e-4, damping=0.0, voltage_limit=24.0
)
ent_off, sim_off = _make_initialized(device, **base, integral_gain=0.0)
ent_on, sim_on = _make_initialized(device, **base, integral_gain=10.0)
target = torch.tensor([[0.5, 0.0]], device=device)
for sim, ent in ((sim_off, ent_off), (sim_on, ent_on)):
_step_n(ent, sim, device, n=20, pos_target=target)
assert _qfrc(ent_on, sim_on)[0].abs() > 100.0 * _qfrc(ent_off, sim_off)[0].abs()
def test_slew_rate_limits_voltage(device):
"""``slew_rate`` rate-limits ``ctrl``: after one step, effective voltage
is far below the requested input."""
base = dict(
mode=DcMotorInputMode.VOLTAGE, stiffness=0.0, damping=0.0, voltage_limit=0.0
)
ent_off, sim_off = _make_initialized(device, **base, slew_rate=0.0)
ent_on, sim_on = _make_initialized(device, **base, slew_rate=10.0)
V = torch.tensor([[100.0, 0.0]], device=device)
for sim, ent in ((sim_off, ent_off), (sim_on, ent_on)):
_step_n(ent, sim, device, n=1, eff_target=V)
assert _qfrc(ent_off, sim_off)[0] > 100.0 * _qfrc(ent_on, sim_on)[0]
def test_inductance_lags_current(device):
"""Large ``inductance`` (te >> dt) suppresses early-step torque."""
base = dict(
mode=DcMotorInputMode.VOLTAGE, stiffness=0.0, damping=0.0, voltage_limit=0.0
)
ent_off, sim_off = _make_initialized(device, **base, inductance=0.0)
ent_on, sim_on = _make_initialized(device, **base, inductance=1.0)
V = torch.tensor([[10.0, 0.0]], device=device)
for sim, ent in ((sim_off, ent_off), (sim_on, ent_on)):
_step_n(ent, sim, device, n=2, eff_target=V)
assert _qfrc(ent_off, sim_off)[0].abs() > 10.0 * _qfrc(ent_on, sim_on)[0].abs()
def test_thermal_decays_torque(device):
"""I^2R heating raises T, which raises effective resistance and decays
torque over time."""
# Params chosen for visible effect in a handful of steps without going
# numerically unstable: small C (fast heating) and modest alpha.
base = dict(
mode=DcMotorInputMode.VOLTAGE, stiffness=0.0, damping=0.0, voltage_limit=0.0
)
ent_off, sim_off = _make_initialized(device, **base)
ent_on, sim_on = _make_initialized(
device, **base, thermal=(1.0, 0.1, 0.0, 0.01, 0.0, 0.0)
)
V = torch.tensor([[100.0, 0.0]], device=device)
for sim, ent in ((sim_off, ent_off), (sim_on, ent_on)):
_step_n(ent, sim, device, n=5, eff_target=V)
assert _qfrc(ent_on, sim_on)[0].abs() < 0.5 * _qfrc(ent_off, sim_off)[0].abs()
def test_lugre_subtracts_friction(device):
"""LuGre friction subtracts a velocity-dependent force after the
``effort_limit`` clamp (mechanical, like cogging)."""
# Static comparison at v>0, ctrl=0; avoids feedback between LuGre slowing
# the joint and back-EMF easing off under sim.step().
# no LuGre: qfrc = -K^2 * v / R (back-EMF only)
# w/ LuGre: qfrc = -K^2 * v / R - sigma1*v - ...
base = dict(
mode=DcMotorInputMode.VOLTAGE, stiffness=0.0, damping=0.0, voltage_limit=0.0
)
ent_off, sim_off = _make_initialized(device, **base)
ent_on, sim_on = _make_initialized(
device, **base, lugre=(1e4, 100.0, 0.1, 0.15, 0.01)
)
zero = torch.zeros(1, 2, device=device)
v0 = torch.tensor([[1.0, 0.0]], device=device)
for sim, ent in ((sim_off, ent_off), (sim_on, ent_on)):
ent.write_joint_state_to_sim(position=zero, velocity=v0)
ent.set_joint_position_target(zero)
ent.set_joint_velocity_target(zero)
ent.set_joint_effort_target(zero)
ent.write_data_to_sim()
sim.forward()
assert abs(_qfrc(ent_on, sim_on)[0]) > 100.0 * abs(_qfrc(ent_off, sim_off)[0])
# Config validation.
def test_pid_mode_requires_gains():
with pytest.raises(ValueError, match="stiffness"):
BuiltinDcMotorActuatorCfg(
target_names_expr=("j",),
mode=DcMotorInputMode.POSITION,
motor_params=DATASHEET,
voltage_limit=1.0,
)
with pytest.raises(ValueError, match="voltage_limit"):
BuiltinDcMotorActuatorCfg(
target_names_expr=("j",),
mode=DcMotorInputMode.POSITION,
motor_params=DATASHEET,
stiffness=1.0,
)
def test_voltage_mode_rejects_pid_gains():
with pytest.raises(ValueError, match="VOLTAGE"):
BuiltinDcMotorActuatorCfg(
target_names_expr=("j",),
mode=DcMotorInputMode.VOLTAGE,
motor_params=DATASHEET,
stiffness=1.0,
)
def test_site_rejected():
with pytest.raises(ValueError, match="SITE"):
BuiltinDcMotorActuatorCfg(
target_names_expr=("j",),
motor_params=DATASHEET,
stiffness=1.0,
voltage_limit=1.0,
transmission_type=TransmissionType.SITE,
)
# Joint-level passthrough.
def test_armature_applied(device):
_, sim = initialize_entity(_make_entity(armature=0.7), device)
m = sim.mj_model
for jname in ("joint1", "joint2"):
dof_id = m.jnt_dofadr[m.joint(jname).id]
assert m.dof_armature[dof_id] == pytest.approx(0.7)
# Domain randomization.
def _scene_env(
device,
num_envs=2,
mode: DcMotorInputMode = DcMotorInputMode.POSITION,
):
def spec_fn():
spec = mujoco.MjSpec.from_string(ROBOT_XML)
for a in list(spec.actuators):
spec.delete(a)
return spec
entity_cfg = EntityCfg(
spec_fn=spec_fn,
articulation=EntityArticulationInfoCfg(
actuators=(
BuiltinDcMotorActuatorCfg(
target_names_expr=("joint.*",),
mode=mode,
motor_params=DATASHEET,
stiffness=5.0 if mode != DcMotorInputMode.VOLTAGE else 0.0,
damping=0.5 if mode != DcMotorInputMode.VOLTAGE else 0.0,
voltage_limit=24.0 if mode != DcMotorInputMode.VOLTAGE else 0.0,
effort_limit=50.0,
),
)
),
)
scene_cfg = SceneCfg(num_envs=num_envs, entities={"robot": entity_cfg})
scene = Scene(scene_cfg, device)
model = scene.compile()
sim = Simulation(num_envs=num_envs, cfg=SimulationCfg(), model=model, device=device)
scene.initialize(model, sim.model, sim.data)
env = Mock()
env.num_envs = num_envs
env.device = device
env.scene = {"robot": scene["robot"]}
env.sim = sim
return env
@pytest.mark.parametrize(
"operation, kp_in, kd_in, kp_expected, kd_expected",
[
# scale: multiplies the configured defaults (kp=5.0, kd=0.5).
("scale", 2.0, 3.0, 2.0 * 5.0, 3.0 * 0.5),
# abs: writes the value directly.
("abs", 10.0, 2.0, 10.0, 2.0),
],
)
def test_dr_pd_gains_position_mode(
device, operation, kp_in, kd_in, kp_expected, kd_expected
):
env = _scene_env(device)
robot = env.scene["robot"]
act = robot.actuators[0]
assert isinstance(act, BuiltinDcMotorActuator)
ctrl_ids = act.global_ctrl_ids
env.sim.expand_model_fields(("actuator_gainprm", "actuator_biasprm"))
dr.pd_gains(
env,
env_ids=torch.tensor([0], device=device),
kp_range=(kp_in, kp_in),
kd_range=(kd_in, kd_in),
asset_cfg=SceneEntityCfg("robot"),
operation=operation,
)
m = env.sim.model
n = len(ctrl_ids)
assert torch.allclose(
m.actuator_gainprm[0, ctrl_ids, 4], torch.full((n,), kp_expected, device=device)
)
assert torch.allclose(
m.actuator_gainprm[0, ctrl_ids, 6], torch.full((n,), kd_expected, device=device)
)
# Other env untouched (cfg defaults).
assert torch.allclose(m.actuator_gainprm[1, ctrl_ids, 4], torch.tensor(5.0))
assert torch.allclose(m.actuator_gainprm[1, ctrl_ids, 6], torch.tensor(0.5))
def test_dr_pd_gains_voltage_mode_rejected(device):
env = _scene_env(device, mode=DcMotorInputMode.VOLTAGE)
env.sim.expand_model_fields(("actuator_gainprm", "actuator_biasprm"))
with pytest.raises(ValueError, match="VOLTAGE"):
dr.pd_gains(
env,
env_ids=torch.tensor([0], device=device),
kp_range=(1.0, 1.0),
kd_range=(1.0, 1.0),
asset_cfg=SceneEntityCfg("robot"),
)
def test_dr_effort_limits_writes_forcerange(device):
env = _scene_env(device)
robot = env.scene["robot"]
act = robot.actuators[0]
ctrl_ids = act.global_ctrl_ids
env.sim.expand_model_fields(
("actuator_forcerange", "jnt_actfrcrange", "tendon_actfrcrange")
)
dr.effort_limits(
env,
env_ids=torch.tensor([0], device=device),
effort_limit_range=(123.0, 123.0),
asset_cfg=SceneEntityCfg("robot"),
operation="abs",
)
m = env.sim.model
n = len(ctrl_ids)
assert torch.allclose(
m.actuator_forcerange[0, ctrl_ids, 0],
torch.full((n,), -123.0, device=device),
)
assert torch.allclose(
m.actuator_forcerange[0, ctrl_ids, 1],
torch.full((n,), 123.0, device=device),
)
# Env 1 keeps the configured default of 50.
assert torch.allclose(m.actuator_forcerange[1, ctrl_ids, 1], torch.tensor(50.0))
# Delay.
def test_delay_position_mode(device):
"""A 2-step lag should make position-mode torque reference step-0 target."""
entity, sim = initialize_entity(
_make_entity(
stiffness=10.0,
damping=0.0,
voltage_limit=1000.0,
delay_min_lag=2,
delay_max_lag=2,
),
device,
)
zero = torch.zeros(1, 2, device=device)
entity.write_joint_state_to_sim(position=zero, velocity=zero)
targets = [
torch.tensor([[0.1, 0.0]], device=device),
torch.tensor([[0.3, 0.0]], device=device),
torch.tensor([[0.5, 0.0]], device=device),
]
for p in targets:
entity.set_joint_position_target(p)
entity.set_joint_velocity_target(zero)
entity.set_joint_effort_target(zero)
entity.write_data_to_sim()
sim.forward()
v_adr = entity.indexing.joint_v_adr
# With lag=2 and three writes, the effective target is targets[0].
expected = K * 10.0 * targets[0][0] / R
assert torch.allclose(sim.data.qfrc_actuator[0, v_adr], expected, atol=1e-4)