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
654 lines
22 KiB
Python
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)
|