Add actuator and sensor delays. Fixes #1004

PiperOrigin-RevId: 866478839
Change-Id: Id21a6da0f98454c8fa39ea5af8a5e213d6eae497
This commit is contained in:
Yuval Tassa
2026-02-06 08:46:45 -08:00
committed by Copybara-Service
parent 84fa527723
commit 6419534bad
48 changed files with 6282 additions and 219 deletions
+165 -1
View File
@@ -1369,7 +1369,9 @@ Euler integrator, semi-implicit in velocity.
for i in range(0, 3):
self.assertEqual(
dist[i],
mujoco.mj_ray(self.model, self.data, pnt, vec[i], None, 1, -1, geom1, None),
mujoco.mj_ray(
self.model, self.data, pnt, vec[i], None, 1, -1, geom1, None
),
)
self.assertEqual(geomid[i], geom1)
self.assertEqual(geomid[i], geom_ex[i])
@@ -1717,6 +1719,168 @@ Euler integrator, semi-implicit in velocity.
self.assertIn(model_path, dependencies)
self.assertIn(msh_path, dependencies)
def test_mj_read_ctrl_and_init_ctrl_delay(self):
xml = r"""
<mujoco>
<worldbody>
<body>
<geom type="sphere" size="0.1"/>
<joint name="hinge" type="hinge"/>
</body>
</worldbody>
<actuator>
<position name="actuator" joint="hinge" delay="0.01" nsample="4"/>
</actuator>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
# Initialize the delay buffer with known values
# actuator_history[i, 0] = nsample, actuator_history[i, 1] = interp
nhistory = model.actuator_history[0, 0]
self.assertEqual(nhistory, 4)
times = np.array([0.0, 0.01, 0.02, 0.03])
values = np.array([1.0, 2.0, 3.0, 4.0])
mujoco.mj_initCtrlHistory(model, data, 0, times, values)
# Read back a value using zero-order hold
# mj_readCtrl auto-subtracts delay: lookup_time = read_time - delay
# delay = 0.01, so:
# read_time=0.02 -> lookup at 0.01 -> value 2.0
# read_time=0.03 -> lookup at 0.02 -> value 3.0
result = mujoco.mj_readCtrl(model, data, 0, 0.02, interp=0)
self.assertEqual(result, 2.0) # ZOH returns value at t=0.01
# Test with times=None (uses existing timestamps)
new_values = np.array([5.0, 6.0, 7.0, 8.0])
mujoco.mj_initCtrlHistory(model, data, 0, None, new_values)
# read_time=0.02 -> lookup at 0.01 -> value 6.0
result = mujoco.mj_readCtrl(model, data, 0, 0.02, interp=0)
self.assertEqual(result, 6.0)
# Test dimension validation errors
with self.assertRaises(TypeError):
# wrong times
mujoco.mj_initCtrlHistory(model, data, 0, np.zeros(3), values)
with self.assertRaises(TypeError):
# wrong values
mujoco.mj_initCtrlHistory(model, data, 0, times, np.zeros(5))
def test_mj_read_sensor_and_init_sensor_delay(self):
xml = r"""
<mujoco>
<worldbody>
<body>
<geom type="sphere" size="0.1"/>
<joint name="hinge" type="hinge"/>
<site name="site"/>
</body>
</worldbody>
<sensor>
<accelerometer name="accel" site="site" delay="0.01" nsample="3"/>
</sensor>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
# Initialize the delay buffer with known values
# sensor_history[i, 0] = nsample, sensor_history[i, 1] = interp
nhistory = model.sensor_history[0, 0]
dim = model.sensor_dim[0]
self.assertEqual(nhistory, 3)
self.assertEqual(dim, 3) # accelerometer has dim=3
times = np.array([0.0, 0.01, 0.02])
values = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float64)
mujoco.mj_initSensorHistory(model, data, 0, times, values, phase=0.0)
# Read back a value using zero-order hold
# mj_readSensor auto-subtracts delay: lookup_time = read_time - delay
# delay = 0.01, so:
# read_time=0.02 -> lookup at 0.01 -> value [4, 5, 6]
result = np.zeros(dim)
mujoco.mj_readSensor(model, data, 0, 0.02, result, interp=0)
# ZOH returns value at t=0.01
np.testing.assert_array_equal(result, [4, 5, 6])
# Test with times=None (uses existing timestamps)
new_values = np.array([
[10, 11, 12], [13, 14, 15], [16, 17, 18]], dtype=np.float64)
mujoco.mj_initSensorHistory(model, data, 0, None, new_values, phase=0.0)
# read_time=0.02 -> lookup at 0.01 -> value [13, 14, 15]
mujoco.mj_readSensor(model, data, 0, 0.02, result, interp=0)
np.testing.assert_array_equal(result, [13, 14, 15])
# Test dimension validation errors
with self.assertRaises(TypeError):
# wrong result size
mujoco.mj_readSensor(model, data, 0, 0.02, np.zeros(2), interp=0)
with self.assertRaises(TypeError):
# wrong times size
mujoco.mj_initSensorHistory(model, data, 0, np.zeros(2), values, 0.0)
with self.assertRaises(TypeError):
# wrong values rows
mujoco.mj_initSensorHistory(model, data, 0, times, np.zeros((4, 3)), 0.0)
with self.assertRaises(TypeError):
# wrong values cols
mujoco.mj_initSensorHistory(model, data, 0, times, np.zeros((3, 2)), 0.0)
def test_init_sensor_history_pedagogical(self):
# A framequat sensor reports body orientation as a unit quaternion.
# Quaternions are never zero: the identity quaternion is [1, 0, 0, 0].
# This test demonstrates why mj_initSensorHistory is needed: after
# mj_makeData, the history buffer is filled with zeros, which is invalid.
xml = r"""
<mujoco>
<worldbody>
<body name="body">
<freejoint/>
<geom type="sphere" size="0.1"/>
</body>
</worldbody>
<sensor>
<framequat name="quat" objtype="body" objname="body" delay="0.01" nsample="5"/>
</sensor>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
dim = model.sensor_dim[0]
nsample = model.sensor_history[0, 0]
delay = model.sensor_delay[0]
self.assertEqual(dim, 4)
self.assertEqual(nsample, 5)
self.assertEqual(delay, 0.01)
# After mj_makeData, reading from the delay buffer gives all zeros.
# For a quaternion sensor, this is invalid data.
result = np.zeros(dim)
mujoco.mj_readSensor(model, data, 0, delay, result, interp=0)
np.testing.assert_array_equal(result, [0, 0, 0, 0])
# To get valid sensor values, we temporarily set delay to 0 so that
# mj_forward populates sensordata directly (without using the delay
# buffer), then restore the original delay value.
saved_delay = model.sensor_delay.copy()
model.sensor_delay[:] = 0
mujoco.mj_forward(model, data)
model.sensor_delay[:] = saved_delay
# Now sensordata contains the valid identity quaternion.
np.testing.assert_array_equal(data.sensordata, [1, 0, 0, 0])
# Use mj_initSensorHistory to fill the buffer with valid quaternion values.
# Passing None for times keeps the existing timestamps in the buffer.
values = np.tile(data.sensordata, (nsample, 1))
mujoco.mj_initSensorHistory(model, data, 0, None, values, phase=0.0)
# Now reading from the delay buffer gives the valid identity quaternion.
mujoco.mj_readSensor(model, data, 0, delay, result, interp=0)
np.testing.assert_array_equal(result, [1, 0, 0, 0])
def _assert_attributes_equal(self, actual_obj, expected_obj, attr_to_compare):
for name in attr_to_compare:
actual_value = getattr(actual_obj, name)