From 0d1c92b20a1766e9d8a17e6837a50a12d951cf07 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 30 May 2024 05:37:03 -0700 Subject: [PATCH] Add documentation on pipeline stages and mjData self-consistency. Fixes #1667 PiperOrigin-RevId: 638619787 Change-Id: Ic8dd0fbd178c4946757a07d74fca3b2c905f9080 --- doc/APIreference/functions.rst | 16 ++--- doc/APIreference/functions_override.rst | 16 ++--- doc/computation/index.rst | 88 ++++++++++++++++++------- doc/programming/simulation.rst | 18 ++--- 4 files changed, 91 insertions(+), 47 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index f171b635..7bc60256 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -57,18 +57,18 @@ Main simulation These are the main entry points to the simulator. Most users will only need to call :ref:`mj_step`, which computes everything and advanced the simulation state by one time step. Controls and applied forces must either be set in advance -(in mjData.{ctrl, qfrc_applied, xfrc_applied}), or a control callback :ref:`mjcb_control` must be installed which will -be called just before the controls and applied forces are needed. Alternatively, one can use :ref:`mj_step1` and +(in ``mjData.{ctrl, qfrc_applied, xfrc_applied}``), or a control callback :ref:`mjcb_control` must be installed which +will be called just before the controls and applied forces are needed. Alternatively, one can use :ref:`mj_step1` and :ref:`mj_step2` which break down the simulation pipeline into computations that are executed before and after the controls are needed; in this way one can set controls that depend on the results from :ref:`mj_step1`. Keep in mind -though that the RK4 solver does not work with mj_step1/2. +though that the RK4 solver does not work with mj_step1/2. See :ref:`Pipeline` for a more detailed description. mj_forward performs the same computations as :ref:`mj_step` but without the integration. It is useful after loading or resetting a model (to put the entire mjData in a valid state), and also for out-of-order computations that involve sampling or finite-difference approximations. -mj_inverse runs the inverse dynamics, and writes its output in ``mjData.qfrc_inverse``. Note that ``mjData.qacc`` must -be set before calling this function. Given the state (qpos, qvel, act), mj_forward maps from force to acceleration, +:ref:`mj_inverse` runs the inverse dynamics, and writes its output in ``mjData.qfrc_inverse``. Note that ``mjData.qacc`` +must be set before calling this function. Given the state (qpos, qvel, act), mj_forward maps from force to acceleration, while mj_inverse maps from acceleration to force. Mathematically these functions are inverse of each other, but numerically this may not always be the case because the forward dynamics rely on a constraint optimization algorithm which is usually terminated early. The difference between the results of forward and inverse dynamics can be computed @@ -77,7 +77,7 @@ general sanity check). The skip version of :ref:`mj_forward` and :ref:`mj_inverse` are useful for example when qpos was unchanged but qvel was changed (usually in the context of finite differencing). Then there is no point repeating the computations that only -depend on qpos. Calling the dynamics with skipstage = mjSTAGE_POS will achieve these savings. +depend on qpos. Calling the dynamics with skipstage = :ref:`mjSTAGE_POS` will achieve these savings. .. _mj_step: @@ -249,8 +249,8 @@ degrees-of-freedom and a given point. Given a body specified by its integer id ( frame (``point``) treated as attached to the body, the Jacobian has both translational (``jacp``) and rotational (``jacr``) components. Passing ``NULL`` for either pointer will skip that part of the computation. Each component is a 3-by-nv matrix. Each row of this matrix is the gradient of the corresponding coordinate of the specified point with -respect to the degrees-of-freedom. The ability to compute end-effector Jacobians efficiently and analytically is one of -the advantages of working in minimal coordinates. +respect to the degrees-of-freedom. The :ref:`pipeline stages` required for Jacobian computations to be +consistenst with the current generalized positions ``mjData.qpos`` are :ref:`mj_kinematics` and :ref:`mj_comPos`. .. _mj_jacBody: diff --git a/doc/APIreference/functions_override.rst b/doc/APIreference/functions_override.rst index 0724fd4f..9f39042d 100644 --- a/doc/APIreference/functions_override.rst +++ b/doc/APIreference/functions_override.rst @@ -33,18 +33,18 @@ The model and all files referenced in it can be loaded from disk or from a VFS w These are the main entry points to the simulator. Most users will only need to call :ref:`mj_step`, which computes everything and advanced the simulation state by one time step. Controls and applied forces must either be set in advance -(in mjData.{ctrl, qfrc_applied, xfrc_applied}), or a control callback :ref:`mjcb_control` must be installed which will -be called just before the controls and applied forces are needed. Alternatively, one can use :ref:`mj_step1` and +(in ``mjData.{ctrl, qfrc_applied, xfrc_applied}``), or a control callback :ref:`mjcb_control` must be installed which +will be called just before the controls and applied forces are needed. Alternatively, one can use :ref:`mj_step1` and :ref:`mj_step2` which break down the simulation pipeline into computations that are executed before and after the controls are needed; in this way one can set controls that depend on the results from :ref:`mj_step1`. Keep in mind -though that the RK4 solver does not work with mj_step1/2. +though that the RK4 solver does not work with mj_step1/2. See :ref:`Pipeline` for a more detailed description. mj_forward performs the same computations as :ref:`mj_step` but without the integration. It is useful after loading or resetting a model (to put the entire mjData in a valid state), and also for out-of-order computations that involve sampling or finite-difference approximations. -mj_inverse runs the inverse dynamics, and writes its output in ``mjData.qfrc_inverse``. Note that ``mjData.qacc`` must -be set before calling this function. Given the state (qpos, qvel, act), mj_forward maps from force to acceleration, +:ref:`mj_inverse` runs the inverse dynamics, and writes its output in ``mjData.qfrc_inverse``. Note that ``mjData.qacc`` +must be set before calling this function. Given the state (qpos, qvel, act), mj_forward maps from force to acceleration, while mj_inverse maps from acceleration to force. Mathematically these functions are inverse of each other, but numerically this may not always be the case because the forward dynamics rely on a constraint optimization algorithm which is usually terminated early. The difference between the results of forward and inverse dynamics can be computed @@ -53,7 +53,7 @@ general sanity check). The skip version of :ref:`mj_forward` and :ref:`mj_inverse` are useful for example when qpos was unchanged but qvel was changed (usually in the context of finite differencing). Then there is no point repeating the computations that only -depend on qpos. Calling the dynamics with skipstage = mjSTAGE_POS will achieve these savings. +depend on qpos. Calling the dynamics with skipstage = :ref:`mjSTAGE_POS` will achieve these savings. .. _Initialization: @@ -165,8 +165,8 @@ degrees-of-freedom and a given point. Given a body specified by its integer id ( frame (``point``) treated as attached to the body, the Jacobian has both translational (``jacp``) and rotational (``jacr``) components. Passing ``NULL`` for either pointer will skip that part of the computation. Each component is a 3-by-nv matrix. Each row of this matrix is the gradient of the corresponding coordinate of the specified point with -respect to the degrees-of-freedom. The ability to compute end-effector Jacobians efficiently and analytically is one of -the advantages of working in minimal coordinates. +respect to the degrees-of-freedom. The :ref:`pipeline stages` required for Jacobian computations to be +consistenst with the current generalized positions ``mjData.qpos`` are :ref:`mj_kinematics` and :ref:`mj_comPos`. .. _mj_jacBody: diff --git a/doc/computation/index.rst b/doc/computation/index.rst index 9512537d..eb1bf285 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -1575,11 +1575,23 @@ Top level pipeline does not support the Runge Kutta integrator. - :ref:`mj_fwdPosition` invokes stages **2-11**, the position-dependent part of the pipeline. +.. _piStages: + Stages ^^^^^^ +Below we describe the pipeline stages and API functions corresponding to each stage. All functions write their outputs +into attributes of :ref:`mjData`. It is informative to compare the list below with the :ref:`mjData` struct definition, +wherein comments above blocks of attributes specify the function that computes them. Note that each stage depends on +the values computed in some or all of the previous stages. + 1. Check the positions and velocities for invalid or unacceptably large real values indicating divergence. If divergence is detected, the state is automatically reset and the corresponding warning is raised: :ref:`mj_checkPos`, :ref:`mj_checkVel` + +Position +'''''''' +The stages below compute quantities that depend on the generalized positions ``mjData.qpos``. + 2. Compute the forward kinematics. This yields the global positions and orientations of all bodies, geoms, sites, cameras and lights. It also normalizes all quaternions: :ref:`mj_kinematics`, :ref:`mj_camLight` 3. Compute the body inertias and joint axes, in global frames centered at the centers of mass of the corresponding @@ -1596,6 +1608,12 @@ Stages 11. Compute the matrices and vectors needed by the constraint solvers: :ref:`mj_projectConstraint` 12. Compute sensor data that only depends on position, and the potential energy if enabled: :ref:`mj_sensorPos`, :ref:`mj_energyPos` + +Velocity +'''''''' +The stages below compute quantities that depend on the generalized velocity ``mjData.qvel``. Due to the sequential +dependence structure of the pipeline, the actual dependence is on both ``qpos`` and ``qvel``. + 13. Compute the tendon, flex edge and actuator velocities: :ref:`mj_fwdVelocity` 14. Compute the body velocities and rates of change of the joint axes, again in the global coordinate frames centered at the subtree centers of mass: :ref:`mj_comVel` @@ -1604,6 +1622,12 @@ Stages (if required by sensors, call :ref:`mj_subtreeVel`): :ref:`mj_sensorVel` 17. Compute the reference constraint acceleration: :ref:`mj_referenceConstraint` 18. Compute the vector of Coriolis, centrifugal and gravitational forces: :ref:`mj_rne` + +Force/acceleration +'''''''''''''''''' +The stages below compute quantities that depend on :ref:`user inputs`. Due to the sequential nature +of the pipeline, the actual dependence is on the entire :ref:`integration state`. + 19. Compute the actuator forces and activation dynamics if defined: :ref:`mj_fwdActuation` 20. Compute the joint acceleration resulting from all forces except for the (still unknown) constraint forces: :ref:`mj_fwdAcceleration` @@ -1620,12 +1644,53 @@ Stages repeats the above sequence three more times, except for the optional computations which are performed only once: one of :ref:`mj_Euler`, :ref:`mj_RungeKutta`, :ref:`mj_implicit` +.. _piConsistency: + +Consistency in ``mjData`` +~~~~~~~~~~~~~~~~~~~~~~~~~ +The MuJoCo computation pipeline is entirely imperative, nothing happens automatically. This leads to behavior which +can seem unexpected to users more familiar with other paradigms. Here are two examples of intended behaviors which can +sometimes be surprising: + +- After setting :ref:`the state`, state-derived quantities do not automatically correspond to the new state. + The required stage or stages must be manually invoked. For example after setting the generalized positions + ``mjData.qpos``, Cartesian positions and orientations will not be consistent with ``qpos`` without first calling + :ref:`mj_kinematics`. +- After an :ref:`mj_step`, which terminates immediately after updating the state, quantities in ``mjData`` correspond + to the *previous* state (or more precisely, the *transition* between the previous and current state). + In particular, all position-dependent sensor values and position-dependent computations like kinematic + :ref:`Jacobians`, will be with respect to the *previous positions*. + + + +.. _piReproducibility: + +Reproducibility +~~~~~~~~~~~~~~~ + +MuJoCo's simulation pipeline is entirely deterministic and reproducible -- if a :ref:`state` in a trajectory is +saved and reloaded and :ref:`mj_step` called again, the resulting next state will be identical. However, there are some +important caveats: + +- Save all the required :ref:`integration state` components. In particular :ref:`warmstart + accelerations` have only a very small effect on the next state, but should be saved if bit-wise equality + is required. +- Any numerical difference between states, no matter how small, will become significant upon integration, especially for + systems with contact. Contact events have high `Lyapunov exponents + `__; this is a property of any rigid-body simulator (and indeed of + `real-world physics `__) and is not MuJoCo-specific. +- Exact reproducibility is only guaranteed within a **single version**, on the **same architecture**. Small numerical + differences are quite common between versioned releases, for example due to code optimizations. This means that when + saving an initial state and an open-loop control sequence, the resulting rolled-out trajectory will be identical + within the same version, but will likely be different between MuJoCo versions or different operating systems. + .. _piInverse: Inverse dynamics ~~~~~~~~~~~~~~~~ -The top-level function :ref:`mj_inverse` invokes the following sequence of computations. +The top-level function :ref:`mj_inverse` invokes the following sequence of computations. The notes above regarding +:ref:`consistency` and :ref:`reproducibility` apply here as well. #. Compute the forward kinematics. #. Compute the body inertias and joint axes. @@ -1652,27 +1717,6 @@ The top-level function :ref:`mj_inverse` invokes the following sequence of compu equals the sum of external and actuation forces. -.. _piReproducibility: - -Reproducibility -~~~~~~~~~~~~~~~ - -MuJoCo's simulation pipeline is entirely deterministic and reproducible -- if a :ref:`state` in a trajectory is -saved and reloaded and :ref:`mj_step` called again, the resulting next state will be identical. However, there are some -important caveats: - -- Save all the required :ref:`integration state` components. In particular :ref:`warmstart - accelerations` have only a very small effect on the next state, but should be saved if bit-wise equality - is required. -- Any numerical difference between states, no matter how small, will become significant upon integration, especially for - systems with contact. Contact events have high `Lyapunov exponents - `__; this is a property of any rigid-body simulator (and indeed of - `real-world physics `__) and is not MuJoCo-specific. -- Exact reproducibility is only guaranteed within a **single version**, on the **same architecture**. Small numerical - differences are quite common between versioned releases, for example due to code optimizations. This means that when - saving an initial state and an open-loop control sequence, the resulting rolled-out trajectory will be identical - within the same version, but will likely be different between MuJoCo versions or different operating systems. - .. _derivatives: Derivatives diff --git a/doc/programming/simulation.rst b/doc/programming/simulation.rst index cc5aa5e5..5b3d50f8 100644 --- a/doc/programming/simulation.rst +++ b/doc/programming/simulation.rst @@ -150,10 +150,10 @@ continuous time, and then integrate over a time period specified by ``mjModel.op the acceleration ``mjData.qacc`` at time ``mjData.time``, given the :ref:`state and control ` at time ``mjData.time``. The numerical integrator then advances the state and time to ``mjData.time + mjModel.opt.timestep``. Now, the control is required to be a function of the state at time ``mjData.time``. However a general feedback -controller can be a very complex function, depending on various features of the state - in particular all the features +controller can be a very complex function, depending on various features of the state -- in particular all the features computed by MuJoCo as intermediate results of the simulation. These may include contacts, Jacobians, passive forces. -None of these quantities are available before ``mj_step`` is called (or rather, they are available but outdated by one -time step). In contrast, when ``mj_step`` calls our control callback, it does so as late in the computation as possible +None of these quantities are available before ``mj_step`` is called (or rather, they are available but *outdated by one +time step*). In contrast, when ``mj_step`` calls our control callback, it does so as late in the computation as possible - namely after all the intermediate results dependent on the state but not on the control have been computed. The same effect can be achieved without using a control callback. This is done by breaking ``mj_step`` in two parts: @@ -253,7 +253,7 @@ notion of state of a dynamical system. Dynamical systems are usually described i where ``t`` is the time, ``x`` is the state vector, ``u`` is the control vector, and ``f`` is the function that computes the time-derivative of the state. This is a continuous-time formulation, and indeed the physics model simulated by MuJoCo is defined in continuous time. Even though the numerical integrator operates in discrete time, the -main part of the computation - namely the function :ref:`mj_forward` - corresponds to the +main part of the computation---namely the function :ref:`mj_forward`---corresponds to the continuous-time dynamics function ``f(t,x,u)`` above. Here we explain this correspondence. The state vector in MuJoCo is: @@ -426,7 +426,7 @@ the first and second sections (position and velocity dependent) have already bee When can we use the above machinery and skip some of the computations? In a regular simulation this is not possible. However, MuJoCo is designed not only for simulation but also for more advanced applications such as model-based optimization, machine learning etc. In such settings one often needs to sample the dynamics at a cloud of nearby -states, or approximate derivatives via finite differences - which is another form of sampling. If the samples are +states, or approximate derivatives via finite differences -- which is another form of sampling. If the samples are arranged on a grid, where only the position or only the velocity or only the control is different from the center point, then the above mechanism can improve performance by about a factor of 2. @@ -591,7 +591,7 @@ The only way to guarantee that all changes are saved is to save the model as a b changes need to be made programmatically, as in system identification for example, and this can only be done with the compiled model. So in summary, we have reasonable but not perfect mechanisms for saving model changes. The reason for this lack of perfection is that we are working with a compiled model, so this is like changing a binary executable and -asking a "decompiler" to make corresponding changes to the C code - it is just not possible in general. +asking a "decompiler" to make corresponding changes to the C code -- it is just not possible in general. .. _siLayout: @@ -642,7 +642,7 @@ spread out the data so that we do not have to perform many memory moves when ins layout "uncompressed". It is still a valid layout, but instead of A_rowadr[r] = A_rowadr[r-1] + A_rownnz[r] which is the standard convention, we set A_rowadr[r] = r*n. MuJoCo uses sparse matrices internally -To represent 3D orientations and rotations, MuJoCo uses unit quaternions - namely 4D unit vectors arranged as q = (w, +To represent 3D orientations and rotations, MuJoCo uses unit quaternions -- namely 4D unit vectors arranged as q = (w, x, y, z). Here (x, y, z) is the rotation axis unit vector scaled by sin(a/2), where a is the rotation angle in radians, and w = cos(a/2). Thus the quaternion corresponding to a null rotation is (1, 0, 0, 0). This is the default setting of all quaternions in MJCF. @@ -841,7 +841,7 @@ Jacobians The derivative of any vector function with respect to its vector argument is called Jacobian. When this term is used in multi-joint kinematics and dynamics, it refers to the derivative of some spatial quantity as a function of the system configuration. In that case the Jacobian is also a linear map that operates on vectors in the (co)tangent space -to the configuration manifold - such as velocities, momenta, accelerations, forces. One caveat here is that the system +to the configuration manifold -- such as velocities, momenta, accelerations, forces. One caveat here is that the system configuration encoded in ``mjData.qpos`` has dimensionality ``mjModel.nq``, while the tangent space has dimensionality ``mjModel.nv``, and the latter is smaller when quaternion joints are present. So the size of the Jacobian matrix is N-by-``mjModel.nv`` where N is the dimensionality of the spatial quantity being differentiated. @@ -861,7 +861,7 @@ Jacobians, which tell us how a spatial frame anchored at the given point will tr change to the kinematic configuration. More precisely, the Jacobian maps joint velocities to end-effector velocities, while the transpose of the Jacobian maps end-effector forces to joint forces. There are also several other ``mj_jacXXX`` functions; these are convenience functions that call the main ``mj_jac`` function with different points -of interest - such as a body center of mass, geom center etc. +of interest -- such as a body center of mass, geom center etc. The ability to compute end-effector Jacobians exactly and efficiently is a key advantage of working in joint coordinates. Such Jacobians are the foundation of many control schemes that map end-effector errors to actuator