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
+83
View File
@@ -300,6 +300,89 @@ Copy concatenated state components specified by ``sig`` from ``state`` into ``d
Copy state from src to dst.
.. _mj_readCtrl:
`mj_readCtrl <#mj_readCtrl>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mj_readCtrl
Read the control value for an actuator at a given time, taking delays into account. If no history buffer exists, return
``mjData.ctrl[id]``. If a history buffer exists (:ref:`nsample<actuator-general-nsample>` > 0), read from the delay
buffer at ``time - actuator_delay[id]`` using the requested interpolation order:
- ``interp = 0``: Zero-order hold (piecewise constant)
- ``interp = 1``: Piecewise Linear
- ``interp = 2``: Cubic Spline (Catmull-Rom)
- ``interp = -1``: Use the actuator's :ref:`interp<actuator-general-interp>` value.
In all three cases, constant extrapolation outside of buffer bounds.
See :ref:`Delays<CDelay>` for details.
.. _mj_readSensor:
`mj_readSensor <#mj_readSensor>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mj_readSensor
Read a sensor value at a given time, taking delays into account. If no history buffer exists, return a pointer to the
sensor's slice of ``mjData.sensordata``. If a history buffer exists (:ref:`nsample<sensor-nsample>` > 0), read from the
history buffer at ``time - sensor_delay[id]``. See :ref:`Delays<CDelay>` for details.
**Return value semantics:**
- If no history buffer exists (:ref:`nsample<sensor-nsample>` = 0), returns a pointer to the sensor's slice of
``mjData.sensordata``.
- If a history buffer exists (:ref:`nsample<sensor-nsample>` > 0) and the requested time matches a stored sample
(always true for ``interp = 0``), returns a pointer to the data in the history buffer.
- If interpolation is required (``interp = 1 or 2``), returns ``NULL`` and writes the interpolated result to
``result`` (must be of size ``dim``).
**Interpolation:**
- ``interp = 0``: Zero-order hold (piecewise constant)
- ``interp = 1``: Piecewise Linear
- ``interp = 2``: Cubic Spline (Catmull-Rom)
- ``interp = -1``: Use the value in :ref:`interp<sensor-interp>`
In all three cases, constant extrapolation outside of buffer bounds.
**Usage:**
.. code-block:: C
// read sensor 0 of data size `dim` at time t
mjtNum result[dim];
const mjtNum* ptr = mj_readSensor(m, d, 0, t, result, /* interp = */ 1);
const mjtNum* data = ptr ? ptr : result;
.. _mj_initCtrlHistory:
`mj_initCtrlHistory <#mj_initCtrlHistory>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mj_initCtrlHistory
Initialize the history buffer for an actuator with custom values. The ``times`` array specifies the timestamps for each
sample (must be length :ref:`nsample<actuator-general-nsample>`), and ``values`` specifies the control values. If
``times`` is ``NULL``, the existing timestamps in the buffer are used, and only the values are updated.
See :ref:`Delays<CDelay>` for details.
.. _mj_initSensorHistory:
`mj_initSensorHistory <#mj_initSensorHistory>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mj_initSensorHistory
Initialize the history buffer for a sensor with custom values. The ``times`` array specifies the timestamps for each
sample (must be length :ref:`nsample<sensor-nsample>`), and ``values`` specifies the sensor values (must be of size
``nsample * dim``). If ``times`` is ``NULL``, the existing timestamps in the buffer are used.
The ``phase`` argument sets the user slot, which stores the last computation time for interval sensors.
See :ref:`Delays<CDelay>` for details.
.. _mj_setKeyframe:
`mj_setKeyframe <#mj_setKeyframe>`__
+63
View File
@@ -205,6 +205,69 @@ is not a subset of the bits set in ``srcsig``.
Copy concatenated state components specified by ``sig`` from ``state`` into ``d``. The bits of the integer
``sig`` correspond to element fields of :ref:`mjtState`. Fails with :ref:`mju_error` if ``sig`` is invalid.
.. _mj_readCtrl:
Read the control value for an actuator at a given time, taking delays into account. If no history buffer exists, return
``mjData.ctrl[id]``. If a history buffer exists (:ref:`nsample<actuator-general-nsample>` > 0), read from the delay
buffer at ``time - actuator_delay[id]`` using the requested interpolation order:
- ``interp = 0``: Zero-order hold (piecewise constant)
- ``interp = 1``: Piecewise Linear
- ``interp = 2``: Cubic Spline (Catmull-Rom)
- ``interp = -1``: Use the actuator's :ref:`interp<actuator-general-interp>` value.
In all three cases, constant extrapolation outside of buffer bounds.
See :ref:`Delays<CDelay>` for details.
.. _mj_readSensor:
Read a sensor value at a given time, taking delays into account. If no history buffer exists, return a pointer to the
sensor's slice of ``mjData.sensordata``. If a history buffer exists (:ref:`nsample<sensor-nsample>` > 0), read from the
history buffer at ``time - sensor_delay[id]``. See :ref:`Delays<CDelay>` for details.
**Return value semantics:**
- If no history buffer exists (:ref:`nsample<sensor-nsample>` = 0), returns a pointer to the sensor's slice of
``mjData.sensordata``.
- If a history buffer exists (:ref:`nsample<sensor-nsample>` > 0) and the requested time matches a stored sample
(always true for ``interp = 0``), returns a pointer to the data in the history buffer.
- If interpolation is required (``interp = 1 or 2``), returns ``NULL`` and writes the interpolated result to
``result`` (must be of size ``dim``).
**Interpolation:**
- ``interp = 0``: Zero-order hold (piecewise constant)
- ``interp = 1``: Piecewise Linear
- ``interp = 2``: Cubic Spline (Catmull-Rom)
- ``interp = -1``: Use the value in :ref:`interp<sensor-interp>`
In all three cases, constant extrapolation outside of buffer bounds.
**Usage:**
.. code-block:: C
// read sensor 0 of data size `dim` at time t
mjtNum result[dim];
const mjtNum* ptr = mj_readSensor(m, d, 0, t, result, /* interp = */ 1);
const mjtNum* data = ptr ? ptr : result;
.. _mj_initCtrlHistory:
Initialize the history buffer for an actuator with custom values. The ``times`` array specifies the timestamps for each
sample (must be length :ref:`nsample<actuator-general-nsample>`), and ``values`` specifies the control values. If
``times`` is ``NULL``, the existing timestamps in the buffer are used, and only the values are updated.
See :ref:`Delays<CDelay>` for details.
.. _mj_initSensorHistory:
Initialize the history buffer for a sensor with custom values. The ``times`` array specifies the timestamps for each
sample (must be length :ref:`nsample<sensor-nsample>`), and ``values`` specifies the sensor values (must be of size
``nsample * dim``). If ``times`` is ``NULL``, the existing timestamps in the buffer are used.
The ``phase`` argument sets the user slot, which stores the last computation time for interval sensors.
See :ref:`Delays<CDelay>` for details.
.. _mj_mulJacVec:
This function multiplies the constraint Jacobian mjData.efc_J by a vector. Note that the Jacobian can be either dense or
+592 -75
View File
File diff suppressed because it is too large Load Diff
+753 -18
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -32,6 +32,9 @@ Upcoming version (not yet released)
General
^^^^^^^
- Actuators and sensors now support arbitrary delays, see :ref:`Delays<CDelay>` for details. Adding
delays introduces a new ``mjData.history`` variable to the :ref:`Physics state<siPhysicsState>`.
.. image:: images/changelog/poncho.png
:width: 45%
:align: right
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 30 KiB

+37 -11
View File
@@ -21,20 +21,21 @@ typedef enum mjtState_ { // state elements
mjSTATE_QPOS = 1<<1, // position
mjSTATE_QVEL = 1<<2, // velocity
mjSTATE_ACT = 1<<3, // actuator activation
mjSTATE_WARMSTART = 1<<4, // acceleration used for warmstart
mjSTATE_CTRL = 1<<5, // control
mjSTATE_QFRC_APPLIED = 1<<6, // applied generalized force
mjSTATE_XFRC_APPLIED = 1<<7, // applied Cartesian force/torque
mjSTATE_EQ_ACTIVE = 1<<8, // enable/disable constraints
mjSTATE_MOCAP_POS = 1<<9, // positions of mocap bodies
mjSTATE_MOCAP_QUAT = 1<<10, // orientations of mocap bodies
mjSTATE_USERDATA = 1<<11, // user data
mjSTATE_PLUGIN = 1<<12, // plugin state
mjSTATE_HISTORY = 1<<4, // history buffers (control, sensor)
mjSTATE_WARMSTART = 1<<5, // acceleration used for warmstart
mjSTATE_CTRL = 1<<6, // control
mjSTATE_QFRC_APPLIED = 1<<7, // applied generalized force
mjSTATE_XFRC_APPLIED = 1<<8, // applied Cartesian force/torque
mjSTATE_EQ_ACTIVE = 1<<9, // enable/disable constraints
mjSTATE_MOCAP_POS = 1<<10, // positions of mocap bodies
mjSTATE_MOCAP_QUAT = 1<<11, // orientations of mocap bodies
mjSTATE_USERDATA = 1<<12, // user data
mjSTATE_PLUGIN = 1<<13, // plugin state
mjNSTATE = 13, // number of state elements
mjNSTATE = 14, // number of state elements
// convenience values for commonly used state specifications
mjSTATE_PHYSICS = mjSTATE_QPOS | mjSTATE_QVEL | mjSTATE_ACT,
mjSTATE_PHYSICS = mjSTATE_QPOS | mjSTATE_QVEL | mjSTATE_ACT | mjSTATE_HISTORY,
mjSTATE_FULLPHYSICS = mjSTATE_TIME | mjSTATE_PHYSICS | mjSTATE_PLUGIN,
mjSTATE_USER = mjSTATE_CTRL | mjSTATE_QFRC_APPLIED | mjSTATE_XFRC_APPLIED |
mjSTATE_EQ_ACTIVE | mjSTATE_MOCAP_POS | mjSTATE_MOCAP_QUAT |
@@ -221,6 +222,7 @@ struct mjData_ {
mjtNum* qpos; // position (nq x 1)
mjtNum* qvel; // velocity (nv x 1)
mjtNum* act; // actuator activation (na x 1)
mjtNum* history; // history buffer (nhistory x 1)
mjtNum* qacc_warmstart; // acceleration used for warmstart (nv x 1)
mjtNum* plugin_state; // plugin state (npluginstate x 1)
@@ -1101,6 +1103,7 @@ struct mjModel_ {
mjtSize nuserdata; // number of mjtNums reserved for the user
mjtSize nsensordata; // number of mjtNums in sensor data vector
mjtSize npluginstate; // number of mjtNums in plugin state vector
mjtSize nhistory; // number of mjtNums in history buffer
// buffer sizes
mjtSize narena; // number of bytes in the mjData arena (inclusive of stack)
@@ -1520,6 +1523,9 @@ struct mjModel_ {
int* actuator_actadr; // first activation address; -1: stateless (nu x 1)
int* actuator_actnum; // number of activation variables (nu x 1)
int* actuator_group; // group for visibility (nu x 1)
int* actuator_history; // history buffer: [nsample, interp] (nu x 2)
int* actuator_historyadr; // address in history buffer; -1: none (nu x 1)
mjtNum* actuator_delay; // delay time in seconds; 0: no delay (nu x 1)
mjtByte* actuator_ctrllimited; // is control limited (nu x 1)
mjtByte* actuator_forcelimited;// is force limited (nu x 1)
mjtByte* actuator_actlimited; // is activation limited (nu x 1)
@@ -1551,6 +1557,10 @@ struct mjModel_ {
int* sensor_adr; // address in sensor array (nsensor x 1)
mjtNum* sensor_cutoff; // cutoff for real and positive; 0: ignore (nsensor x 1)
mjtNum* sensor_noise; // noise standard deviation (nsensor x 1)
int* sensor_history; // history buffer: [nsample, interp] (nsensor x 2)
int* sensor_historyadr; // address in history buffer; -1: none (nsensor x 1)
mjtNum* sensor_delay; // delay time in seconds; 0: no delay (nsensor x 1)
mjtNum* sensor_interval; // interval: [period, phase] in seconds (nsensor x 2)
mjtNum* sensor_user; // user data (nsensor x nuser_sensor)
int* sensor_plugin; // plugin instance id; -1: not a plugin (nsensor x 1)
@@ -2445,6 +2455,9 @@ typedef struct mjsActuator_ { // actuator specification
// other
int group; // group
int nsample; // number of samples in history buffer
int interp; // interpolation order (0=ZOH, 1=linear, 2=cubic)
double delay; // delay time in seconds; 0: no delay
mjDoubleVec* userdata; // user data
mjsPlugin plugin; // actuator plugin
mjString* info; // message appended to compiler errors
@@ -2469,6 +2482,12 @@ typedef struct mjsSensor_ { // sensor specification
double cutoff; // cutoff for real and positive datatypes
double noise; // noise stdev
// history buffer
int nsample; // number of samples in history buffer
int interp; // interpolation order (0=ZOH, 1=linear, 2=cubic)
double delay; // delay time in seconds
double interval[2]; // [period, time_prev] in seconds
// other
mjDoubleVec* userdata; // user data
mjsPlugin plugin; // sensor plugin
@@ -3234,6 +3253,13 @@ void mj_extractState(const mjModel* m, const mjtNum* src, int srcsig,
mjtNum* dst, int dstsig);
void mj_setState(const mjModel* m, mjData* d, const mjtNum* state, int sig);
void mj_copyState(const mjModel* m, const mjData* src, mjData* dst, int sig);
mjtNum mj_readCtrl(const mjModel* m, const mjData* d, int id, mjtNum time, int interp);
const mjtNum* mj_readSensor(const mjModel* m, const mjData* d, int id, mjtNum time,
mjtNum* result, int interp);
void mj_initCtrlHistory(const mjModel* m, mjData* d, int id,
const mjtNum* times, const mjtNum* values);
void mj_initSensorHistory(const mjModel* m, mjData* d, int id,
const mjtNum* times, const mjtNum* values, mjtNum phase);
void mj_setKeyframe(mjModel* m, const mjData* d, int k);
int mj_addContact(const mjModel* m, mjData* d, const mjContact* con);
int mj_isPyramidal(const mjModel* m);
+166
View File
@@ -1081,20 +1081,186 @@ copied in the array mjData.sensordata and are available for user processing.
Here we describe the XML attributes common to all sensor types, so as to avoid repetition later.
.. _sensor-name:
:at:`name`: :at-val:`string, optional`
Name of the sensor.
.. _sensor-noise:
:at:`noise`: :at-val:`real, "0"`
The standard deviation of the noise model of this sensor. In versions prior to 3.1.4, this would lead to noise being
added to the sensors. In release 3.1.4 this feature was removed, see :doc:`3.1.4 changelog <changelog>` for a
detailed justification. As of subsequent versions, this attrbute serves as a convenient location for saving standard
deviation information for later use.
.. _sensor-cutoff:
:at:`cutoff`: :at-val:`real, "0"`
When this value is positive, it limits the absolute value of the sensor output. It is also used to normalize the
sensor data plots in :ref:`simulate.cc <saSimulate>`. Note that :at:`cutoff` has a different meaning for
:ref:`collision sensors<collision-sensors>`.
.. _sensor-nsample:
:at:`nsample`: :at-val:`int, "0"`
If :at-val:`nsample` is greater than 0, creates a time-indexed ring buffer with :at:`nsample` slots of sensor data.
During state advancement, the current sensor data is appended to the buffer with timestamp ``time``, and the oldest
sample is removed. Values in the history buffer can be read via :ref:`mj_readSensor`. A positive :at-val:`nsample`
is required for both :ref:`delay<sensor-delay>` and :ref:`interval<sensor-interval>` features.
See :ref:`Delays<CDelay>` for details.
.. _sensor-interp:
:at:`interp`: :at-val:`[zoh, linear, cubic], "zoh"`
The interpolation method used when reading from the history buffer. Corresponds to the ``interp`` argument in
:ref:`mj_readSensor`.
- ``zoh``: Zero-order hold (piecewise constant).
- ``linear``: Piecewise linear interpolation.
- ``cubic``: Cubic spline interpolation (Catmull-Rom).
The :at:`interp` value is for advanced use-cases, see :ref:`Delays<CDelay>` for details.
.. _sensor-delay:
:at:`delay`: :at-val:`real, "0"`
If greater than 0, sensor values in ``mjData.sensordata`` are read from the history buffer at ``time - delay`` rather
than computed directly. Requires positive :ref:`nsample<sensor-nsample>`, cannot be negative.
In the most common case, ``delay = nsample * timestep``, see :ref:`Delays<CDelay>` for details.
.. _sensor-interval:
:at:`interval`: :at-val:`real, "0 0"`
This attribute controls how often sensor values are recomputed. It is useful for modeling sensors that have a larger
sampling period than the simulation timestep. Requires a history buffer (:ref:`nsample <sensor-nsample>` > 0).
This attribute is defined by two real-valued numbers, both in units of time, called :at:`interval` =
":at-val:`period` :at-val:`phase`". It is possible to only specify the :at-val:`period`, in which case the
:at-val:`phase` is assumed to be 0.
The :at-val:`period` specifies the interval period between recomputations. The default value of 0 has the special
meaning "every simulation timestep". Note that the period is not required to be an integer multiple of the timestep.
For example, if the simulation timestep is 1.0, and :at-val:`period` is 2.5, the sensor will be computed at times
0.0, 3.0, 5.0, 8.0, 10.0, 13.0, ... with the actual interval alternating between 2 and 3 timesteps. :at-val:`period`
cannot be negative. Note that only ``period > timestep`` values make sense; values smaller than or equal to the
timestep will not lead to an error but merely cause the sensor to be recomputed at every timestep.
The :at-val:`phase` only takes effect during history buffer initialization in :ref:`mj_resetData`. It specifies the
last time that the sensor was computed "before the simulation started" in continuous time (i.e., disregarding the
quantization of timesteps). It is useful for precisely controlling the *relative phase* of sensor computation and
simulation time, when interval is used. The default value of 0 has the special meaning ":at-val:`-period`", i.e.
specifying that the sensor should be computed at the first timestep of the simulation. Continuing our example from
earlier, if the timestep is 1.0 and interval is ":at-val:`2.5 -1.5`", the sensor will be computed at times 1.0, 4.0,
6.0, 9.0, 11.0, 14.0, etc. :at-val:`phase` must be in the range :math:`(-\text{period}, 0]`.
:at:`user`: :at-val:`real(nuser_sensor), "0 0 ..."`
See :ref:`User parameters <CUser>`.
.. _CDelay:
Delays
~~~~~~
Both actuators and sensors support time delays via a ring buffer that stores timestamped samples. When the integer
attribute :at:`nsample` (:ref:`actuators<actuator-general-nsample>`, :ref:`sensors<sensor-nsample>`) is positive, a
buffer with :at:`nsample` slots is included in the :ref:`physics state<siPhysicsState>` component ``mjData.history``
and the samples and current timestamps are written into the buffer upon state advancement.
If additionally the real-valued :at:`delay` attribute (:ref:`actuators<actuator-general-delay>`,
:ref:`sensors<sensor-delay>`) is positive, then during the forward dynamics the control or sensor values are read from
the history buffer (instead of being read from ``ctrl`` or recomputed, respectively). Positive :at:`delay` requires
positive :at:`nsample`.
Note that since reading happens before writing, the minimum positive delay is effectively one timestep, despite
:at:`delay` being real-valued.
Delayed reading in the engine is triggered by positive :at:`delay`, and performed by the API functions
:ref:`mj_readCtrl` and :ref:`mj_readSensor`, which read from the buffer at ``time - delay``, effectively implementing
the requested delay. These functions take ``time`` as an argument and can be used whenever :at:`nsample` is positive,
allowing the user to inspect the contents of the history buffer at any time, including in a "history-only" mode
(:at:`nsample` > 0, :at:`delay` = 0), where past values are accessible via the API but the simulation is unaffected.
**Sensor Modes**
Sensors support both :ref:`delay<sensor-delay>` and :ref:`interval<sensor-interval>` attributes.
The combination determines behavior:
.. list-table::
:header-rows: 1
:widths: 10 10 80
* - delay
- interval
- Write / Read behavior
* - = 0
- = 0
- History-only: computed every step, written to ``sensordata``, pushed into history buffer
* - > 0
- = 0
- Delayed: computed every step, ``sensordata`` contains delayed reading (read from buffer)
* - = 0
- > 0
- Periodic: computed on interval, ``sensordata`` contains last computed value (no delay)
* - > 0
- > 0
- Periodic + Delayed: computed on interval, ``sensordata`` contains delayed reading (read from buffer)
**Initialization**
History buffers are initialized by :ref:`mj_resetData` as follows:
- **Values**: Always initialized to zero. For custom initialization after reset, call :ref:`mj_initCtrlHistory`
and :ref:`mj_initSensorHistory`.
- **Actuator timestamps**: ``[..., -2*dt, -dt]``.
- **Sensor timestamps** without :ref:`interval<sensor-interval>`: ``[..., -2*dt, -dt]``.
- **Sensor timestamps** with :ref:`interval<sensor-interval>`: Samples are spaced at ``period`` intervals rather than
``dt``. The continuous-time timestamps ``[..., phase-2*period, phase-period, phase]`` are rounded up to the nearest
multiple of ``dt``, since that is when samples could have been computed. If ``phase = 0`` (the default), it is
interpreted as ``-period``, meaning the first sample will be computed at ``t = 0``.
**Causality and interpolation**
The most common positive delay value is ``delay = timestep * nsample``, which implements a simple
history buffer, with no causality issues.
.. warning::
If ``delay > timestep * nsample``, then data will be read before the earliest buffer bound, resulting in non-causal
extrapolation: using a value from before it was actually recorded. This scenario will not lead to a runtime error,
so it is up to the user to avoid it.
Setting ``delay < timestep * nsample`` is not problematic and can be useful for system identification and stochastic
delays. In these use cases, one should choose a maximum possible ``delay_max`` and set ``nsample = ceil(delay_max /
timestep)``. Then at run-time or sysID-time, the :ref:`mjModel` fields ``actuator_delay`` or ``sensor_delay`` can be
safely modified, so long as ``delay_max`` is not exceeded.
.. image:: images/modeling/delay_buffer_light.svg
:width: 50%
:align: right
:class: only-light
.. image:: images/modeling/delay_buffer_dark.svg
:width: 50%
:align: right
:class: only-dark
These two use cases are the reason for including the :at:`interp` attribute (:ref:`actuators<actuator-general-interp>`,
:ref:`sensors<sensor-interp>`). While real-world exogenous delays are generally a zero-order-hold phenomenon, this
implies discontinuity: a small change in the delay has no effect, until the timestep threshold is crossed. For example
with ``dt = 0.1`` and ``nsample = 2``, there is no functional difference between ``delay = 0.2`` and ``delay = 0.101``
(both read from the oldest sample), but stepping from ``delay = 0.101`` to ``delay = 0.1`` crosses a threshold and
changes behavior. By allowing higher order interpolation, the effect of delays becomes continuous (``interp = linear``)
and differentiable (``interp = cubic``).
Note that interpolation does not makes sense for some types of sensors, for example sensors that report integer values
(e.g. :ref:`insidesite<sensor-insidesite>`).
.. _CCamera:
Cameras
+6 -1
View File
@@ -264,7 +264,7 @@ individual components and combinations of components. These are:
Physics state
"""""""""""""
The *physics state* (:ref:`mjSTATE_PHYSICS<mjtState>`) contains the main quantities which are time-integrated during
stepping. These are ``mjData.{qpos, qvel, act}``:
stepping. These are ``mjData.{qpos, qvel, act, history}``:
Position: ``qpos``
The configuration in generalized coodinates, denoted in the :ref:`Numerical Integration<geIntegration>` section as
@@ -281,6 +281,11 @@ Actuator activation: ``act``
actuators (such as biological muscles) that have their own activation states assembled in ``mjData.act``, denoted
as :math:`w` in the :ref:`Numerical Integration<geIntegration>` section.
History buffer: ``history``
When actuators or sensors have a positive :at:`nsample` attribute (:ref:`actuators<actuator-general-nsample>`,
:ref:`sensors<sensor-nsample>`), this buffer stores timestamped samples of previous
control or sensor values. See :ref:`Delays<CDelay>` for details.
.. _siFullPhysics:
Full physics state