diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 9cc05a09..09622ed0 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -3275,6 +3275,9 @@ These matrices and their dimensions are: - All outputs are optional (can be NULL). - ``eps`` is the finite-differencing epsilon. - ``flg_centered`` denotes whether to use forward (0) or centered (1) differences. +- Accuracy can be somewhat improved if solver :ref:`iterations` are set to a + fixed (small) value and solver :ref:`tolerance` is set to 0. This insures that + all calls to the solver will perform exactly the same number of iterations. .. _mjd_inverseFD: diff --git a/doc/APIreference/functions_override.rst b/doc/APIreference/functions_override.rst index 8fbb2865..98241ae5 100644 --- a/doc/APIreference/functions_override.rst +++ b/doc/APIreference/functions_override.rst @@ -463,6 +463,9 @@ These matrices and their dimensions are: - All outputs are optional (can be NULL). - ``eps`` is the finite-differencing epsilon. - ``flg_centered`` denotes whether to use forward (0) or centered (1) differences. +- Accuracy can be somewhat improved if solver :ref:`iterations` are set to a + fixed (small) value and solver :ref:`tolerance` is set to 0. This insures that + all calls to the solver will perform exactly the same number of iterations. .. _mjd_inverseFD: diff --git a/doc/changelog.rst b/doc/changelog.rst index 86e4504f..1c5cd20f 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -142,15 +142,16 @@ General `Levi Burner `__. 24. Deleted the code sample ``testxml``. The functionality provided by this utility is implemented in the `WriteReadCompare `__ test. +25. Deleted the code sample ``derivative``. Functionality provided by :ref:`mjd_transitionFD`. Python bindings ^^^^^^^^^^^^^^^ -25. Fixed `#870 `__ where calling ``update_scene`` with an invalid +26. Fixed `#870 `__ where calling ``update_scene`` with an invalid camera name used the default camera. -26. Added ``user_scn`` to the :ref:`passive viewer` handle, which allows users to add custom +27. Added ``user_scn`` to the :ref:`passive viewer` handle, which allows users to add custom visualization geoms (`#1023 `__). -27. Added optional boolean keyword arguments ``show_left_ui`` and ``show_right_ui`` to the functions ``viewer.launch`` +28. Added optional boolean keyword arguments ``show_left_ui`` and ``show_right_ui`` to the functions ``viewer.launch`` and ``viewer.launch_passive``, which allow users to launch a viewer with UI panels hidden. Simulate @@ -160,21 +161,21 @@ Simulate :align: right :width: 240px -28. Added **state history** mechanism to :ref:`simulate` and the managed +29. Added **state history** mechanism to :ref:`simulate` and the managed :ref:`Python viewer`. State history can be viewed by scrubbing the History slider and (more precisely) with the left and right arrow keys. See screen capture: -29. The ``LOADING...`` label is now shown correctly. +30. The ``LOADING...`` label is now shown correctly. `Contribution `__ by `Levi Burner `__. Bug fixes ^^^^^^^^^ -30. Fixed a bug that was causing :ref:`geom margin` to be ignored during the construction of +31. Fixed a bug that was causing :ref:`geom margin` to be ignored during the construction of midphase collision trees. -31. Fixed a bug that was generating incorrect values in ``efc_diagApprox`` for weld equality constraints. +32. Fixed a bug that was generating incorrect values in ``efc_diagApprox`` for weld equality constraints. Version 2.3.7 (July 20, 2023) diff --git a/doc/overview.rst b/doc/overview.rst index 68bb46a9..bb89671e 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -98,7 +98,7 @@ Separation of model and data :ref:`mjModel` is constructed by the compiler. :ref:`mjData` is constructed at runtime, given :ref:`mjModel`. This separation makes it easy to simulate multiple models as well as multiple states and controls for each model, in turn facilitating :ref:`multi-threading ` for sampling and :ref:`finite - differences `. The top-level API functions reflect this basic separation, and have + differences `. The top-level API functions reflect this basic separation, and have the format: .. code:: C diff --git a/doc/programming/samples.rst b/doc/programming/samples.rst index b482ba56..fb24c61b 100644 --- a/doc/programming/samples.rst +++ b/doc/programming/samples.rst @@ -177,69 +177,3 @@ proliferation of overlapping technologies, which differ not only between platfor case of Linux. The addition of a couple of extra functions (such as those provided by OSMesa for example) could have avoided a lot of confusion. EGL is a newer standard from Khronos which aims to do this, and it is gaining popularity. But we cannot yet assume that all users have it installed. - -.. _saDerivative: - -`derivative `_ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This code sample illustrates the numerical approximation of forward and inverse dynamics derivatives via finite -differences. The process involves a number of epochs. In each epoch the simulation is advanced for a specified number -of steps, derivatives are computed at the last state, and timing and accuracy statistics are collected. The averages -over epochs are printed at the end. - -The code can be incorporated in user projects where derivatives are needed, and can also be used as a stand-alone tool -for estimating CPU time and numerical accuracy. Accuracy is estimated in the function ``checkderiv()`` using several -mathematical identities about the derivatives of inverse functions; the residuals being computed would be zero if the -derivatives were exact. Note that these identities involve matrix multiplications which may affect the accuracy -estimates. Timing tests are applied only to the parallel section, where the function ``worker()`` is executed in -multiple threads using OpenMP. There are fewer threads than forward/inverse dynamics evaluations, thus each thread -executes multiple evaluations. For a more general discussion of parallel processing in MuJoCo see -:ref:`multi-threading ` below. - -Recall than for a differentiable function ``f(x)`` the derivative can be approximated as - -.. code-block:: Text - - df/dx = (f(x+eps)-f(x))/eps - -where ``eps`` is a small number. One can also use the centered finite difference method, which is two times slower but -more accurate. Here ``f`` is one of the functions - -.. code-block:: Text - - forward dynamics: qacc(qfrc_applied, qvel, qpos) - inverse dynamics: qfrc_inverse(qacc, qvel, qpos) - -The code sample computes six Jacobian matrices, containing the derivative of each function with respect to its three -arguments. The results are stored in the array ``deriv``. All six Jacobian matrices are square, with dimensionality -equal to the number of degrees of freedom ``mjModel.nv``. When the model configuration includes quaternion joints, -mjData.qpos has larger dimensionality than the other vectors, however the derivative is only defined in the tangent -space to the configuration manifold. This is why, when differentiating with respect to the elements of ``mjData.qpos``, -we do not directly add ``eps`` but instead use the function :ref:`mju_quatIntegrate` to perturb the quaternion in the -tangent space, keeping it normalized. This technique should also be used in any other situation where quaternions need -to be perturbed. - -There are some important subtleties in this code that improve speed as well as accuracy. To speed up the computation, -we re-use intermediate results whenever possible. This relies on the skip mechanism described under :ref:`forward -dynamics ` and :ref:`inverse dynamics ` below. We first perturb force dimensions, keeping -position and velocity fixed. In this way we avoid recomputing results that depend on position and velocity but not on -force. Then we perturb velocity dimensions, and avoid recomputing results that depend on position but not on velocity -or force. Finally we perturb position dimensions - which requires full computation because everything depends on -position. - -Accuracy depends on the value of ``eps`` which is user-adjustable, as well as the shape of the function. In the case -of forward dynamics however, the function evaluation involves an iterative constraint solver, and this must be handled -with care. In general, the difference between ``f(x+eps)`` and ``f(x)`` is very small, thus any noise affecting the -two function evaluations differently can make the resulting derivatives meaningless. Different warm-starts or -different number of solver iterations can act as such noise here. Therefore we fix the warm-start ``mjData.qacc`` to a -value pre-computed at the center point, using ``nwarmup`` extra major iterations to obtain a more accurate warm-start. -We also fix the number of solver iterations to ``niter`` and set ``mjModel.opt.tolerance = 0``; this disables the early -termination mechanism. Note that the original simulation options are restored in the serial code which advances the -state. - -We emphasize that the above subtleties are not high-order corrections that can be incorporated later. In the presence -of unilateral constraints, numerical derivatives are hard to compute and there is no shortcut around it; indeed they -would not even be defined if it wasn't for our soft-constraint model. Making the constraints softer results in more -accurate results. This effect is so strong that in some situations it makes sense to intentionally work with the wrong -model, i.e., a model that is softer than desired, so as to obtain more accurate derivatives. diff --git a/doc/programming/simulation.rst b/doc/programming/simulation.rst index 82743536..cc5aa5e5 100644 --- a/doc/programming/simulation.rst +++ b/doc/programming/simulation.rst @@ -428,9 +428,7 @@ However, MuJoCo is designed not only for simulation but also for more advanced a 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 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. The code sample :ref:`derivative.cc -` illustrates this approach, and also shows how :ref:`multi-threading ` can be used for -additional speedup. +point, then the above mechanism can improve performance by about a factor of 2. .. _siInverse: @@ -494,10 +492,7 @@ bodies, we may eventually implement within-step multi-threading, but for now thi Rather than speed up a single simulation, we prefer to use multi-threading to speed up sampling operations that are common in more advanced applications. Simulation is inherently serial over time (the output of one mj_step is the input to the next), while in sampling many calls to either forward or inverse dynamics can be executed in parallel -since there are no dependencies among them, except perhaps for a common initial state. The code sample -:ref:`derivative.cc ` illustrates one important example of sampling, namely the approximation of -dynamics derivatives via finite differences. Here we will not repeat the material from that section, but will instead -explain MuJoCo's general approach to parallel processing. +since there are no dependencies among them, except perhaps for a common initial state. MuJoCo was designed for multi-threading from its beginning. Unlike most existing simulators where the notion of dynamical system state is difficult to map to the software state and is often distributed among multiple objects, in @@ -515,7 +510,7 @@ management. // allocate per-thread mjData mjData* d[64]; - for( int n=0; n` for an example). This -approach is more efficient because the thread-specific mjData is large compared to the processor cache. +work explicitly among threads. This approach is more efficient because the thread-specific mjData is large compared to +the processor cache. We also use a shared mjModel for cache-efficiency. In some situations it may not be possible to use the same mjModel for all threads. One obvious reason is that mjModel may need to be modified within the thread function. Another reason diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 8015ed81..67962e94 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -76,18 +76,12 @@ target_compile_options(compile PUBLIC ${MUJOCO_SAMPLE_COMPILE_OPTIONS}) target_link_libraries(compile Threads::Threads) target_link_options(compile PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) -add_executable(derivative derivative.cc) -target_compile_options(derivative PUBLIC ${MUJOCO_SAMPLE_COMPILE_OPTIONS}) -target_link_libraries(derivative Threads::Threads) -target_link_options(derivative PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) - add_executable(testspeed testspeed.cc) target_compile_options(testspeed PUBLIC ${MUJOCO_SAMPLE_COMPILE_OPTIONS}) target_link_libraries(testspeed Threads::Threads) target_link_options(testspeed PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) target_link_libraries(compile mujoco::mujoco) -target_link_libraries(derivative mujoco::mujoco) target_link_libraries(testspeed mujoco::mujoco) # Build samples that require GLFW. @@ -115,7 +109,6 @@ target_link_options(record PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) embed_in_bundle(basic simulate) embed_in_bundle(compile simulate) - embed_in_bundle(derivative simulate) embed_in_bundle(record simulate) embed_in_bundle(testspeed simulate) endif() @@ -134,7 +127,6 @@ if(_INSTALL_SAMPLES) TARGETS basic compile - derivative record testspeed INSTALL_DIRECTORY @@ -148,7 +140,6 @@ if(_INSTALL_SAMPLES) install( TARGETS basic compile - derivative record testspeed EXPORT ${PROJECT_NAME} diff --git a/sample/Makefile b/sample/Makefile index 33aab005..3f2949d2 100644 --- a/sample/Makefile +++ b/sample/Makefile @@ -8,6 +8,5 @@ COMMON=-O2 -I../include -L../lib -std=c++17 -pthread -Wl,-no-as-needed -Wl,-rpat all: $(CXX) $(COMMON) testspeed.cc -lmujoco -o ../bin/testspeed $(CXX) $(COMMON) compile.cc -lmujoco -o ../bin/compile - $(CXX) $(COMMON) derivative.cc -lmujoco -fopenmp -o ../bin/derivative $(CXX) $(COMMON) basic.cc -lmujoco -lglfw -o ../bin/basic $(CXX) $(COMMON) record.cc -lmujoco -lglfw -o ../bin/record diff --git a/sample/Makefile.macos b/sample/Makefile.macos index 263e688b..07cd830e 100644 --- a/sample/Makefile.macos +++ b/sample/Makefile.macos @@ -14,6 +14,5 @@ ALLFLAGS=$(CXXFLAGS) -L$(GLFWROOT)/lib -Wl,-rpath,$(MUJOCOPATH) all: clang++ $(ALLFLAGS) testspeed.cc -framework mujoco -o testspeed clang++ $(ALLFLAGS) compile.cc -framework mujoco -o compile - clang++ $(ALLFLAGS) derivative.cc -framework mujoco -o derivative clang++ $(ALLFLAGS) basic.cc -framework mujoco -lglfw -o basic clang++ $(ALLFLAGS) record.cc -framework mujoco -lglfw -o record diff --git a/sample/Makefile.windows b/sample/Makefile.windows index db7719c9..9aa858c3 100644 --- a/sample/Makefile.windows +++ b/sample/Makefile.windows @@ -12,7 +12,6 @@ COMMON=/O2 /MT /EHsc /arch:AVX /I../include /Fe../bin/ all: cl $(COMMON) testspeed.cc ../lib/mujoco.lib cl $(COMMON) compile.cc ../lib/mujoco.lib - cl $(COMMON) derivative.cc /openmp ../lib/mujoco.lib cl $(COMMON) basic.cc ../lib/glfw3dll.lib ../lib/mujoco.lib cl $(COMMON) record.cc ../lib/glfw3dll.lib ../lib/mujoco.lib del *.obj diff --git a/sample/derivative.cc b/sample/derivative.cc deleted file mode 100644 index e3fcd94f..00000000 --- a/sample/derivative.cc +++ /dev/null @@ -1,439 +0,0 @@ -// Copyright 2021 DeepMind Technologies Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -#include -#include - -#include - -// enable compilation with and without OpenMP support -#if defined(_OPENMP) - #include -#else - // omp timer replacement - #include - double omp_get_wtime(void) { - static std::chrono::system_clock::time_point _start = std::chrono::system_clock::now(); - std::chrono::duration elapsed = std::chrono::system_clock::now() - _start; - return elapsed.count(); - } - - // omp functions used below - void omp_set_dynamic(int) {} - void omp_set_num_threads(int) {} - int omp_get_num_procs(void) { - return 1; - } -#endif - - -// gloval variables: internal -const int MAXTHREAD = 64; // maximum number of threads allowed -const int MAXEPOCH = 100; // maximum number of epochs -int isforward = 0; // dynamics mode: forward or inverse -mjtNum* deriv = 0; // dynamics derivatives (6*nv*nv): - // dinv/dpos, dinv/dvel, dinv/dacc, dacc/dpos, dacc/dvel, dacc/dfrc - - -// global variables: user-defined, with defaults -int nthread = 0; // number of parallel threads (default set later) -int niter = 30; // fixed number of solver iterations for finite-differencing -int nwarmup = 3; // center point repetitions to improve warmstart -int nepoch = 20; // number of timing epochs -int nstep = 500; // number of simulation steps per epoch -double eps = 1e-6; // finite-difference epsilon - - -// worker function for parallel finite-difference computation of derivatives -void worker(const mjModel* m, const mjData* dmain, mjData* d, int id) { - int nv = m->nv; - - // allocate stack space for result at center - mj_markStack(d); - mjtNum* center = mj_stackAllocNum(d, nv); - mjtNum* warmstart = mj_stackAllocNum(d, nv); - - // prepare static schedule: range of derivative columns to be computed by this thread - int chunk = (m->nv + nthread-1) / nthread; - int istart = id * chunk; - int iend = mjMIN(istart + chunk, m->nv); - - // copy state and control from dmain to thread-specific d - d->time = dmain->time; - mju_copy(d->qpos, dmain->qpos, m->nq); - mju_copy(d->qvel, dmain->qvel, m->nv); - mju_copy(d->qacc, dmain->qacc, m->nv); - mju_copy(d->qacc_warmstart, dmain->qacc_warmstart, m->nv); - mju_copy(d->qfrc_applied, dmain->qfrc_applied, m->nv); - mju_copy(d->xfrc_applied, dmain->xfrc_applied, 6*m->nbody); - mju_copy(d->ctrl, dmain->ctrl, m->nu); - - // run full computation at center point (usually faster than copying dmain) - if (isforward) { - mj_forward(m, d); - - // extra solver iterations to improve warmstart (qacc) at center point - for (int rep=1; repqacc : d->qfrc_inverse); - - // save output for center point and warmstart (needed in forward only) - mju_copy(center, output, nv); - mju_copy(warmstart, d->qacc_warmstart, nv); - - // select target vector and original vector for force or acceleration derivative - mjtNum* target = (isforward ? d->qfrc_applied : d->qacc); - const mjtNum* original = (isforward ? dmain->qfrc_applied : dmain->qacc); - - // finite-difference over force or acceleration: skip = mjSTAGE_VEL - for (int i=istart; iqacc_warmstart, warmstart, m->nv); - mj_forwardSkip(m, d, mjSTAGE_VEL, 1); - } else { - mj_inverseSkip(m, d, mjSTAGE_VEL, 1); - } - - // undo perturbation - target[i] = original[i]; - - // compute column i of derivative 2 - for (int j=0; jqvel[i] += eps; - - // evaluate dynamics, with center warmstart - if (isforward) { - mju_copy(d->qacc_warmstart, warmstart, m->nv); - mj_forwardSkip(m, d, mjSTAGE_POS, 1); - } else { - mj_inverseSkip(m, d, mjSTAGE_POS, 1); - } - - // undo perturbation - d->qvel[i] = dmain->qvel[i]; - - // compute column i of derivative 1 - for (int j=0; jdof_jntid[i]; - - // get quaternion address and dof position within quaternion (-1: not in quaternion) - int quatadr = -1, dofpos = 0; - if (m->jnt_type[jid]==mjJNT_BALL) { - quatadr = m->jnt_qposadr[jid]; - dofpos = i - m->jnt_dofadr[jid]; - } else if (m->jnt_type[jid]==mjJNT_FREE && i>=m->jnt_dofadr[jid]+3) { - quatadr = m->jnt_qposadr[jid] + 3; - dofpos = i - m->jnt_dofadr[jid] - 3; - } - - // apply quaternion or simple perturbation - if (quatadr>=0) { - mjtNum angvel[3] = {0, 0, 0}; - angvel[dofpos] = eps; - mju_quatIntegrate(d->qpos+quatadr, angvel, 1); - } else { - d->qpos[m->jnt_qposadr[jid] + i - m->jnt_dofadr[jid]] += eps; - } - - // evaluate dynamics, with center warmstart - if (isforward) { - mju_copy(d->qacc_warmstart, warmstart, m->nv); - mj_forwardSkip(m, d, mjSTAGE_NONE, 1); - } else { - mj_inverseSkip(m, d, mjSTAGE_NONE, 1); - } - - // undo perturbation - mju_copy(d->qpos, dmain->qpos, m->nq); - - // compute column i of derivative 0 - for (int j=0; jnv; - - // allocate space - mj_markStack(d); - mjtNum* mat = mj_stackAllocNum(d, nv*nv); - - // get pointers to derivative matrices - mjtNum* G0 = deriv; // dinv/dpos - mjtNum* G1 = deriv + nv*nv; // dinv/dvel - mjtNum* G2 = deriv + 2*nv*nv; // dinv/dacc - mjtNum* F0 = deriv + 3*nv*nv; // dacc/dpos - mjtNum* F1 = deriv + 4*nv*nv; // dacc/dvel - mjtNum* F2 = deriv + 5*nv*nv; // dacc/dfrc - - // G2*F2 - I - mju_mulMatMat(mat, G2, F2, nv, nv, nv); - for (int i=0; i2) { - std::sscanf(argv[2], "%d", &nthread); - } - if (argc>3) { - std::sscanf(argv[3], "%d", &niter); - } - if (argc>4) { - std::sscanf(argv[4], "%d", &nwarmup); - } - if (argc>5) { - std::sscanf(argv[5], "%d", &nepoch); - } - if (argc>6) { - std::sscanf(argv[6], "%d", &nstep); - } - if (argc>7) { - std::sscanf(argv[7], "%lf", &eps); - } - - // check number of threads - if (nthread<1 || nthread>MAXTHREAD) { - std::printf("nthread must be between 1 and %d\n", MAXTHREAD); - return 1; - } - - // check number of epochs - if (nepoch<1 || nepoch>MAXEPOCH) { - std::printf("nepoch must be between 1 and %d\n", MAXEPOCH); - return 1; - } - - // load model - mjModel* m = 0; - if (std::strlen(argv[1])>4 && !std::strcmp(argv[1]+std::strlen(argv[1])-4, ".mjb")) { - m = mj_loadModel(argv[1], NULL); - } else { - m = mj_loadXML(argv[1], NULL, NULL, 0); - } - if (!m) { - std::printf("Could not load modelfile '%s'\n", argv[1]); - return 1; - } - - // print arguments -#if defined(_OPENMP) - std::printf("\nnthread : %d (OpenMP)\n", nthread); -#else - std::printf("\nnthread : %d (serial)\n", nthread); -#endif - std::printf("niter : %d\n", niter); - std::printf("nwarmup : %d\n", nwarmup); - std::printf("nepoch : %d\n", nepoch); - std::printf("nstep : %d\n", nstep); - std::printf("eps : %g\n\n", eps); - - // make mjData: main, per-thread - mjData* dmain = mj_makeData(m); - mjData* d[MAXTHREAD]; - for (int n=0; nnv*m->nv); - - // set up OpenMP (if not enabled, this does nothing) - omp_set_dynamic(0); - omp_set_num_threads(nthread); - - // save solver options - int save_iterations = m->opt.iterations; - mjtNum save_tolerance = m->opt.tolerance; - - // allocate statistics - int nefc = 0; - double cputm[MAXEPOCH][2]; - mjtNum error[MAXEPOCH][8]; - - // run epochs, collect statistics - for (int epoch=0; epochopt.iterations = save_iterations; - m->opt.tolerance = save_tolerance; - - // advance main simulation for nstep - for (int i=0; inefc; - - // set solver options for finite differences - m->opt.iterations = niter; - m->opt.tolerance = 0; - - // test forward and inverse - for (isforward=0; isforward<2; isforward++) { - // start timer - double starttm = omp_get_wtime(); - - // run worker threads in parallel if OpenMP is enabled - #pragma omp parallel for schedule(static) - for (int n=0; nnv, nefc/nepoch); - std::printf("inverse : %.2f ms\n", mcputm[0]/nepoch); - std::printf("forward : %.2f ms\n\n", mcputm[1]/nepoch); - std::printf("accuracy: log10(residual L1 relnorm)\n"); - std::printf("------------------------------------\n"); - for (int ie=0; ie<8; ie++) { - std::printf(" %s : %.2g\n", accuracy[ie], merror[ie]/nepoch); - } - std::printf("\n"); - - // shut down - mju_free(deriv); - mj_deleteData(dmain); - for (int n=0; n