// 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. // Tests for engine/engine_support.c. #include "src/engine/engine_support.h" #include #include #include #include #include #include #include #include "test/fixture.h" namespace mujoco { namespace { using ::testing::DoubleNear; using ::testing::Eq; using ::testing::ContainsRegex; // NOLINT using ::testing::MatchesRegex; using ::testing::Pointwise; using ::testing::ElementsAreArray; using ::testing::Pointwise; using AngMomMatTest = MujocoTest; static constexpr char AngMomTestingModel[] = R"( )"; // compare subtree angular momentum computed in two ways TEST_F(AngMomMatTest, CompareAngMom) { mjModel* model = LoadModelFromString(AngMomTestingModel); int nv = model->nv; int bodyid = mj_name2id(model, mjOBJ_BODY, "link1"); mjData* data = mj_makeData(model); // reset to the keyframe with some angular velocities mj_resetDataKeyframe(model, data, 0); mj_forward(model, data); // get the reference value of angular momentum mj_subtreeVel(model, data); mjtNum angmom_ref[3]; mju_copy3(angmom_ref, data->subtree_angmom+3*bodyid); // compute angular momentum using the angular momentum matrix mjtNum* angmom_mat = (mjtNum*) mju_malloc(sizeof(mjtNum)*3*nv); mj_angmomMat(model, data, angmom_mat, bodyid); mjtNum angmom_test[3]; mju_mulMatVec(angmom_test, angmom_mat, data->qvel, 3, nv); // compare the two angular momentum values static const mjtNum tol = 1e-8; for (int i = 0; i < 3; i++) { EXPECT_THAT(angmom_ref[i], DoubleNear(angmom_test[i], tol)); } mju_free(angmom_mat); mj_deleteData(data); mj_deleteModel(model); } // compare subtree angular momentum matrix: analytical and findiff TEST_F(AngMomMatTest, CompareAngMomMats) { mjModel* model = LoadModelFromString(AngMomTestingModel); int nv = model->nv; int bodyid = mj_name2id(model, mjOBJ_BODY, "link1"); mjData* data = mj_makeData(model); mjtNum* angmom_mat = (mjtNum*) mju_malloc(sizeof(mjtNum)*3*nv); mjtNum* angmom_mat_fd = (mjtNum*) mju_malloc(sizeof(mjtNum)*3*nv); // reset to the keyframe with some angular velocities mj_resetDataKeyframe(model, data, 0); mj_forward(model, data); // compute the angular momentum matrix using the analytical method mj_angmomMat(model, data, angmom_mat, bodyid); // compute the angular momentum matrix using finite differences static const mjtNum eps = 1e-6; for (int i = 0; i < nv; i++) { // reset vel, forward nudge i-th dof, get angmom mju_copy(data->qvel, model->key_qvel, model->nv); data->qvel[i] += eps; mj_forward(model, data); mj_subtreeVel(model, data); mjtNum agmf[3]; mju_copy3(agmf, data->subtree_angmom+3*bodyid); // reset vel, backward nudge i-th dof, get angmom mju_copy(data->qvel, model->key_qvel, model->nv); data->qvel[i] -= eps; mj_forward(model, data); mj_subtreeVel(model, data); mjtNum agmb[3]; mju_copy3(agmb, data->subtree_angmom+3*bodyid); // finite-difference the angmom matrix for (int j = 0; j < 3; j++) { angmom_mat_fd[nv*j+i] = (agmf[j] - agmb[j]) / (2 * eps); } } // compare the two matrices static const mjtNum tol = 1e-8; for (int i = 0; i < 3*nv; i++) { EXPECT_THAT(angmom_mat_fd[i], DoubleNear(angmom_mat[i], tol)); } mju_free(angmom_mat_fd); mju_free(angmom_mat); mj_deleteData(data); mj_deleteModel(model); } using JacobianTest = MujocoTest; static const mjtNum max_abs_err = std::numeric_limits::epsilon(); static constexpr char kJacobianTestingModel[] = R"( )"; // compare analytic and finite-differenced subtree-com Jacobian TEST_F(JacobianTest, SubtreeJac) { mjModel* model = LoadModelFromString(kJacobianTestingModel); int nv = model->nv; int bodyid = mj_name2id(model, mjOBJ_BODY, "main"); mjData* data = mj_makeData(model); mjtNum* jac_subtree = (mjtNum*) mju_malloc(sizeof(mjtNum)*3*nv); mjtNum* qpos = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nq); mjtNum* nudge = (mjtNum*) mju_malloc(sizeof(mjtNum)*nv); // all we need for Jacobians are kinematics and CoM-related quantities mj_kinematics(model, data); mj_comPos(model, data); // get subtree CoM Jacobian of free body mj_jacSubtreeCom(model, data, jac_subtree, bodyid); // save current subtree-com and qpos, clear nudge mjtNum subtree_com[3]; mju_copy3(subtree_com, data->subtree_com+3*bodyid); mju_copy(qpos, data->qpos, model->nq); mju_zero(nudge, nv); // compare analytic Jacobian to finite-difference approximation static const mjtNum eps = 1e-6; for (int i=0; i < nv; i++) { // reset qpos, nudge i-th dof, update data->qpos, reset nudge mju_copy(data->qpos, qpos, model->nq); nudge[i] = 1; mj_integratePos(model, data->qpos, nudge, eps); nudge[i] = 0; // kinematics and comPos to get nudged com mj_kinematics(model, data); mj_comPos(model, data); // compare finite-differenced and analytic Jacobian for (int j=0; j < 3; j++) { mjtNum findiff = (data->subtree_com[3*bodyid+j] - subtree_com[j]) / eps; EXPECT_THAT(jac_subtree[nv*j+i], DoubleNear(findiff, eps)); } } mju_free(nudge); mju_free(qpos); mju_free(jac_subtree); mj_deleteData(data); mj_deleteModel(model); } // confirm that applying linear forces via the subtree-com Jacobian only creates // the expected linear accelerations (no accelerations of internal joints) TEST_F(JacobianTest, SubtreeJacNoInternalAcc) { mjModel* model = LoadModelFromString(kJacobianTestingModel); int nv = model->nv; int bodyid = mj_name2id(model, mjOBJ_BODY, "main"); mjData* data = mj_makeData(model); mjtNum* jac_subtree = (mjtNum*) mju_malloc(sizeof(mjtNum)*3*nv); // all we need for Jacobians are kinematics and CoM-related quantities mj_kinematics(model, data); mj_comPos(model, data); // get subtree CoM Jacobian of free body mj_jacSubtreeCom(model, data, jac_subtree, bodyid); // uncomment for debugging // mju_printMat(jac_subtree, 3, nv); // call fwdPosition since we'll need the factorised mass matrix in the test mj_fwdPosition(model, data); // treating the subtree Jacobian as the projection of 3 axis-aligned unit // forces into joint space, solve for the resulting accelerations in-place mj_solveM(model, data, jac_subtree, jac_subtree, 3); // expect to find accelerations of magnitude 1/subtreemass in the first 3 // coordinates of the free joint and 0s elsewhere, since applying forces to // the CoM should accelerate the whole mechanism without any internal motion int body_dofadr = model->body_dofadr[bodyid]; mjtNum invtreemass = 1.0/model->body_subtreemass[bodyid]; for (int r = 0; r < 3; r++) { for (int c = 0; c < nv; c++) { mjtNum expected = c - body_dofadr == r ? invtreemass : 0.0; EXPECT_THAT(jac_subtree[nv*r+c], DoubleNear(expected, max_abs_err)); } } mju_free(jac_subtree); mj_deleteData(data); mj_deleteModel(model); } static constexpr char kQuat[] = R"( )"; static constexpr char kFreeBall[] = R"( )"; static constexpr char kQuatlessPendulum[] = R"( )"; static constexpr char kTelescope[] = R"( )"; // compare mj_jacDot with finite-differenced mj_jac TEST_F(JacobianTest, JacDot) { for (auto xml : {kQuat, kFreeBall, kQuatlessPendulum, kTelescope}) { mjModel* model = LoadModelFromString(xml); int nv = model->nv; mjtNum point[3] = {.01, .02, .03}; mjData* data = mj_makeData(model); // load keyframe if present, step for a bit if (model->nkey) mj_resetDataKeyframe(model, data, 0); while (data->time < 0.1) { mj_step(model, data); } // minimal call required for mj_jacDot outputs to be valid mj_kinematics(model, data); mj_comPos(model, data); mj_comVel(model, data); // get bodyid int bodyid = mj_name2id(model, mjOBJ_BODY, "query"); EXPECT_GT(bodyid, 0); // jac, jac_dot mj_markStack(data); mjtNum* jac = mj_stackAllocNum(data, 6*nv); mj_jac(model, data, jac, jac+3*nv, point, bodyid); mjtNum* jac_dot = mj_stackAllocNum(data, 6*nv); mj_jacDot(model, data, jac_dot, jac_dot+3*nv, point, bodyid); // jac_h: jacobian after integrating qpos with a timestep of h mjtNum h = 1e-7; mj_integratePos(model, data->qpos, data->qvel, h); mj_kinematics(model, data); mj_comPos(model, data); mjtNum* jac_h = mj_stackAllocNum(data, 6*nv);; mj_jac(model, data, jac_h, jac_h+3*nv, point, bodyid); // jac_dot_h finite-difference approximation mjtNum* jac_dot_h = mj_stackAllocNum(data, 6*nv);; mju_sub(jac_dot_h, jac_h, jac, 6*nv); mju_scl(jac_dot_h, jac_dot_h, 1/h, 6*nv); // compare finite-differenced and analytic mjtNum tol = 1e-5; for (int j=0; j < 6; j++) { EXPECT_THAT(AsVector(jac_dot_h + j*nv, nv), Pointwise(DoubleNear(tol), AsVector(jac_dot + j*nv, nv))); } mj_freeStack(data); mj_deleteData(data); mj_deleteModel(model); } } using Name2idTest = MujocoTest; static constexpr char name2idTestingModel[] = R"( )"; TEST_F(Name2idTest, FindIds) { mjModel* model = LoadModelFromString(name2idTestingModel); EXPECT_THAT(mj_name2id(model, mjOBJ_BODY, "world"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_BODY, "body1"), 1); EXPECT_THAT(mj_name2id(model, mjOBJ_BODY, "body2"), 2); EXPECT_THAT(mj_name2id(model, mjOBJ_GEOM, "body1_geom1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_GEOM, "body1_geom2"), 1); EXPECT_THAT(mj_name2id(model, mjOBJ_JOINT, "joint2"), 1); EXPECT_THAT(mj_name2id(model, mjOBJ_MESH, "mesh1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_LIGHT, "light1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_CAMERA, "camera1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_SITE, "site2"), 1); EXPECT_THAT(mj_name2id(model, mjOBJ_MATERIAL, "material1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_TEXTURE, "texture1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_TENDON, "tendon1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_ACTUATOR, "actuator1"), 0); EXPECT_THAT(mj_name2id(model, mjOBJ_SENSOR, "sensor1"), 0); mj_deleteModel(model); } TEST_F(Name2idTest, MissingIds) { mjModel* model = LoadModelFromString(name2idTestingModel); EXPECT_THAT(mj_name2id(model, mjOBJ_BODY, "abody3"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_GEOM, "abody2_geom2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_JOINT, "joint3"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_MESH, "amesh2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_LIGHT, "alight2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_CAMERA, "acamera2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_SITE, "asite3"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_MATERIAL, "amaterial2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_TEXTURE, "atexture2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_TENDON, "atendon2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_ACTUATOR, "aactuator2"), -1); EXPECT_THAT(mj_name2id(model, mjOBJ_SENSOR, "asensor2"), -1); mj_deleteModel(model); } TEST_F(Name2idTest, EmptyIds) { mjModel* model = LoadModelFromString(name2idTestingModel); EXPECT_THAT(mj_name2id(model, mjOBJ_BODY, ""), -1); mj_deleteModel(model); } TEST_F(Name2idTest, Namespaces) { mjModel* model = LoadModelFromString(name2idTestingModel); EXPECT_THAT(mj_name2id(model, mjOBJ_GEOM, "camera1"), 3); mj_deleteModel(model); } using VersionTest = MujocoTest; TEST_F(VersionTest, MjVersion) { EXPECT_EQ(mj_version(), mjVERSION_HEADER); } TEST_F(VersionTest, MjVersionString) { #if GTEST_USES_SIMPLE_RE == 1 auto regex_matcher = ContainsRegex("^\\d+\\.\\d+\\.\\d+"); #else auto regex_matcher = MatchesRegex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9a-z]+)?$"); #endif EXPECT_THAT(std::string(mj_versionString()), regex_matcher); } using SupportTest = MujocoTest; // utility: generate two random quaternions with a given angle difference void randomQuatPair(mjtNum qa[4], mjtNum qb[4], mjtNum angle, int seed) { // make distribution using seed std::mt19937_64 rng; rng.seed(seed); std::normal_distribution dist(0, 1); // sample qa = qb for (int i=0; i < 4; i++) { qa[i] = qb[i] = dist(rng); } mju_normalize4(qa); mju_normalize4(qb); // integrate qb in random direction by angle mjtNum dir[3]; for (int i=0; i < 3; i++) { dir[i] = dist(rng); } mju_normalize3(dir); mju_quatIntegrate(qb, dir, angle); } static constexpr char ballJointModel[] = R"( )"; TEST_F(SupportTest, DifferentiatePosSubQuat) { const mjtNum eps = 1e-12; // epsilon for float comparison mjModel* model = LoadModelFromString(ballJointModel); int seed = 1; for (mjtNum angle : {0.0, 1e-5, 1e-2}) { for (mjtNum dt : {1e-6, 1e-3, 1e-1}) { // random quaternion pair with given angle difference mjtNum qpos1[4], qpos2[4]; randomQuatPair(qpos1, qpos2, angle, seed++); // get velocity given timestep mjtNum qvel[3]; mj_differentiatePos(model, qvel, dt, qpos1, qpos2); // equivalent computation mjtNum qneg[4], qdif[4], qvel_expect[3]; mju_negQuat(qneg, qpos1); mju_mulQuat(qdif, qneg, qpos2); mju_quat2Vel(qvel_expect, qdif, dt); // expect numerical equality EXPECT_THAT(AsVector(qvel, 3), Pointwise(DoubleNear(eps), qvel_expect)); } } mj_deleteModel(model); } static const char* const kDefaultModel = "testdata/model.xml"; TEST_F(SupportTest, GetSetStateStepEqual) { const std::string xml_path = GetTestDataFilePath(kDefaultModel); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); mjData* data = mj_makeData(model); // make distribution using seed std::mt19937_64 rng; rng.seed(3); std::normal_distribution dist(0, .01); // set controls and applied joint forces to random values for (int i=0; i < model->nu; i++) data->ctrl[i] = dist(rng); for (int i=0; i < model->nv; i++) data->qfrc_applied[i] = dist(rng); for (int i=0; i < model->neq; i++) data->eq_active[i] = dist(rng) > 0; // take one step mj_step(model, data); int spec = mjSTATE_INTEGRATION; int size = mj_stateSize(model, spec); // save the initial state and step std::vector state0a(size); mj_getState(model, data, state0a.data(), spec); // get the initial state, expect equality std::vector state0b(size); mj_getState(model, data, state0b.data(), spec); EXPECT_EQ(state0a, state0b); // take one step mj_step(model, data); // save the resulting state std::vector state1a(size); mj_getState(model, data, state1a.data(), spec); // expect the state to be different after stepping EXPECT_THAT(state0a, testing::Ne(state1a)); // reset to the saved state, step again, get the resulting state mj_setState(model, data, state0a.data(), spec); mj_step(model, data); std::vector state1b(size); mj_getState(model, data, state1b.data(), spec); // expect the state to be the same after re-stepping EXPECT_EQ(state1a, state1b); mj_deleteData(data); mj_deleteModel(model); } using AddMTest = MujocoTest; TEST_F(AddMTest, DenseSameAsSparse) { mjModel* m = LoadModelFromPath("humanoid/humanoid100.xml"); mjData* d = mj_makeData(m); int nv = m->nv; // force use of sparse matrices m->opt.jacobian = mjJAC_SPARSE; // warm-up rollout to get a typical state while (d->time < 2) { mj_step(m, d); } // dense zero matrix std::vector dst_sparse(nv * nv, 0.0); // sparse zero matrix std::vector dst_dense(nv * nv, 0.0); std::vector rownnz(nv, nv); std::vector rowadr(nv, 0); std::vector colind(nv * nv, 0); // set sparse structure for (int i = 0; i < nv; i++) { rowadr[i] = i * nv; for (int j = 0; j < nv; j++) { colind[rowadr[i] + j] = j; } } // sparse addM mj_addM(m, d, dst_sparse.data(), rownnz.data(), rowadr.data(), colind.data()); // dense addM mj_addM(m, d, dst_dense.data(), nullptr, nullptr, nullptr); // dense comparison, should be same matrix EXPECT_THAT(dst_dense, ElementsAreArray(dst_sparse)); // clean up mj_deleteData(d); mj_deleteModel(m); } static const char* const kIlslandEfcPath = "engine/testdata/island/island_efc.xml"; TEST_F(SupportTest, MulMIsland) { const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); mjData* data = mj_makeData(model); // allocate vec, fill with arbitrary values mjtNum* vec = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv); for (int i=0; i < model->nv; i++) { vec[i] = 0.2 + 0.3*i; } // simulate for 0.2 seconds mj_resetData(model, data); while (data->time < 0.2) { mj_step(model, data); } mj_forward(model, data); // multiply by Mass matrix: Mvec = M * vec mjtNum* Mvec = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc); mj_mulM(model, data, Mvec, vec); // iterate over islands for (int i=0; i < data->nisland; i++) { // allocate dof vectors for island int dofnum = data->island_dofnum[i]; mjtNum* vec_i = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); mjtNum* Mvec_i = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); // copy values into vec_i int* dofind = data->island_dofind + data->island_dofadr[i]; for (int j=0; j < dofnum; j++) { vec_i[j] = vec[dofind[j]]; } // === compressed: use vec_i // multiply by Jacobian, for this island int flg_vecunc = 0; mj_mulM_island(model, data, Mvec_i, vec_i, i, flg_vecunc); // expect corresponding values to match for (int j=0; j < dofnum; j++) { EXPECT_THAT(Mvec_i[j], DoubleNear(Mvec[dofind[j]], 1e-12)); } // === uncompressed: use vec mju_zero(Mvec_i, dofnum); // clear output // multiply by Jacobian, for this island flg_vecunc = 1; mj_mulM_island(model, data, Mvec_i, vec, i, flg_vecunc); // expect corresponding values to match for (int j=0; j < dofnum; j++) { EXPECT_THAT(Mvec_i[j], DoubleNear(Mvec[dofind[j]], 1e-12)); } mju_free(vec_i); mju_free(Mvec_i); } mju_free(Mvec); mju_free(vec); mj_deleteData(data); mj_deleteModel(model); } static constexpr char GeomDistanceTestingModel[] = R"( )"; TEST_F(SupportTest, GeomDistance) { mjModel* model = LoadModelFromString(GeomDistanceTestingModel); mjData* data = mj_makeData(model); mj_kinematics(model, data); // plane-sphere, distmax too small mjtNum distmax = 0.5; EXPECT_EQ(mj_geomDistance(model, data, 0, 1, distmax, nullptr), 0.5); mjtNum fromto[6]; EXPECT_EQ(mj_geomDistance(model, data, 0, 1, distmax, fromto), 0.5); EXPECT_THAT(fromto, Pointwise(Eq(), std::vector{0, 0, 0, 0, 0, 0})); // plane-sphere distmax = 1.0; EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 0, 1, 1.0, fromto), 0.8); mjtNum eps = 1e-12; EXPECT_THAT(fromto, Pointwise(DoubleNear(eps), std::vector{0, 0, 0, 0, 0, 0.8})); // sphere-plane EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 1, 0, 1.0, fromto), 0.8); EXPECT_THAT(fromto, Pointwise(DoubleNear(eps), std::vector{0, 0, 0.8, 0, 0, 0})); // sphere-sphere EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 1, 2, 1.0, fromto), 0.5); EXPECT_THAT(fromto, Pointwise(DoubleNear(eps), std::vector{.2, 0, 1, .7, 0, 1})); // sphere-sphere, flipped order EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 2, 1, 1.0, fromto), 0.5); EXPECT_THAT(fromto, Pointwise(DoubleNear(eps), std::vector{.7, 0, 1, .2, 0, 1})); // mesh-sphere (close distmax) distmax = 0.701; eps = model->opt.ccd_tolerance; EXPECT_THAT(mj_geomDistance(model, data, 3, 1, distmax, fromto), DoubleNear(0.7, eps)); EXPECT_THAT(fromto, Pointwise(DoubleNear(eps), std::vector{0, 0, .8, 0, 0, .1})); // mesh-sphere (far distmax) distmax = 1.0; EXPECT_THAT(mj_geomDistance(model, data, 3, 1, distmax, fromto), DoubleNear(0.7, eps)); EXPECT_THAT(fromto, Pointwise(DoubleNear(eps), std::vector{0, 0, .8, 0, 0, .1})); mj_deleteData(data); mj_deleteModel(model); } static constexpr char kSetKeyframeTestingModel[] = R"( )"; TEST_F(SupportTest, SetKeyframe) { mjModel* model = LoadModelFromString(kSetKeyframeTestingModel); mjData* data = mj_makeData(model); data->ctrl[0] = 1; while (data->time < 1) { mj_step(model, data); } mj_setKeyframe(model, data, 1); EXPECT_EQ(data->time, model->key_time[1]); EXPECT_EQ(data->ctrl[0], model->key_ctrl[model->nu * 1]); EXPECT_EQ(data->qpos[0], model->key_qpos[model->nq * 1]); EXPECT_EQ(data->qvel[0], model->key_qvel[model->nv * 1]); EXPECT_EQ(data->act[0], model->key_act[model->na * 1]); mj_step(model, data); mj_setKeyframe(model, data, 0); EXPECT_EQ(data->time, model->key_time[0]); EXPECT_EQ(data->ctrl[0], model->key_ctrl[model->nu * 0]); EXPECT_EQ(data->qpos[0], model->key_qpos[model->nq * 0]); EXPECT_EQ(data->qvel[0], model->key_qvel[model->nv * 0]); EXPECT_EQ(data->act[0], model->key_act[model->na * 0]); mj_deleteData(data); mj_deleteModel(model); } } // namespace } // namespace mujoco