Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
46f925a79This file is pinned to the selected release. Download the original to open it in your design tool.
// Copyright 2026 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_core_util.c.
#include "src/engine/engine_core_util.h"
#include <algorithm>
#include <cstddef>
#include <limits>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mujoco/mjdata.h>
#include <mujoco/mujoco.h>
#include "test/fixture.h"
namespace mujoco {
namespace {
using ::testing::NotNull;
using ::testing::Pointwise;
using FlexGatherStateTest = MujocoTest;
TEST_F(FlexGatherStateTest, mju_flexGatherState_Grid) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<flexcomp name="flex0" type="grid" count="3 3 3" spacing=".1 .1 .1"
dim="3" mass="1" radius="0.01" dof="trilinear">
<elasticity young="5e4" poisson="0.2"/>
<contact selfcollide="none"/>
</flexcomp>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
MjDataPtr data = MakeData(model);
mj_forward(model.get(), data.get());
ASSERT_EQ(model->nflex, 1);
int f = 0;
int nodenum = model->flex_nodenum[f];
int nstart = model->flex_nodeadr[f];
// Simulate a rotated state (90 degrees around Z axis)
ASSERT_TRUE(model->flex_centered[f]);
for (int i = 0; i < nodenum; i++) {
int b = model->flex_nodebodyid[nstart + i];
mjtNum x = data->xpos[3 * b + 0];
mjtNum y = data->xpos[3 * b + 1];
mjtNum z = data->xpos[3 * b + 2];
// Rotate 90 degrees around Z: (x, y, z) -> (-y, x, z)
data->xpos[3 * b + 0] = -y;
data->xpos[3 * b + 1] = x;
data->xpos[3 * b + 2] = z;
}
std::vector<mjtNum> xpos(3 * nodenum);
mju_flexGatherState(model.get(), data.get(), f, xpos.data(), NULL);
// Verify that gathered xpos matches the rotated data->xpos
for (int i = 0; i < nodenum; i++) {
int b = model->flex_nodebodyid[nstart + i];
EXPECT_NEAR(xpos[3 * i + 0], data->xpos[3 * b + 0], 1e-5);
EXPECT_NEAR(xpos[3 * i + 1], data->xpos[3 * b + 1], 1e-5);
EXPECT_NEAR(xpos[3 * i + 2], data->xpos[3 * b + 2], 1e-5);
}
}
TEST_F(FlexGatherStateTest, mju_flexGatherState_ShellMode) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<flexcomp name="flex0" type="grid" count="3 3 3" spacing=".1 .1 .1"
dim="3" mass="1" radius="0.01" dof="trilinear" cellcount="2 2 2">
<elasticity young="5e4" poisson="0.2" elastic2d="stretch" thickness="0.02"/>
<contact selfcollide="none"/>
</flexcomp>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
ASSERT_EQ(model->nflex, 1);
int f = 0;
model->flex_interp[f] = -1;
MjDataPtr data = MakeData(model);
mj_forward(model.get(), data.get());
int nodenum = model->flex_nodenum[f];
int nstart = model->flex_nodeadr[f];
// Move boundary nodes, keep interior node stuck (it is pinned)
mjtNum shift[3] = {0.1, 0.2, 0.3};
for (int i = 0; i < nodenum; i++) {
if (i == 13) continue; // Skip center node
int b = model->flex_nodebodyid[nstart + i];
data->xpos[3 * b + 0] += shift[0];
data->xpos[3 * b + 1] += shift[1];
data->xpos[3 * b + 2] += shift[2];
}
std::vector<mjtNum> xpos(3 * nodenum);
mju_flexGatherState(model.get(), data.get(), f, xpos.data(), NULL);
// Verify that gathered xpos for center node (13) is the TFI reconstructed
// position
EXPECT_NEAR(xpos[3 * 13 + 0], shift[0], 1e-5);
EXPECT_NEAR(xpos[3 * 13 + 1], shift[1], 1e-5);
EXPECT_NEAR(xpos[3 * 13 + 2], shift[2], 1e-5);
}
using AngMomMatTest = MujocoTest;
static constexpr char AngMomTestingModel[] = R"(
<mujoco>
<option>
<flag gravity="disable"/>
</option>
<worldbody>
<body name="link1" pos="0 0 0.5">
<freejoint/>
<geom type="ellipsoid" size="0.15 0.17 0.19" quat="1 .2 .3 .4"/>
<body name="link2" >
<joint type="hinge" axis="1 0 0" />
<geom type="capsule" size="0.05" fromto="0 0 0 0 0.5 0"/>
<body pos="0 0.6 0">
<joint type="slide" axis="1 0 0"/>
<geom type="capsule" size="0.05 0.2" quat="0.707 0 0.707 0"/>
<body name="link3">
<joint type="ball" pos="0.2 0 0"/>
<geom type="capsule" pos="0.2 0 0" size="0.03 0.4"/>
</body>
</body>
</body>
</body>
</worldbody>
<keyframe>
<key qvel="0 0 0 .1 .2 .3 .4 .5 .4 .3 .2"/>
</keyframe>
</mujoco>
)";
// compare subtree angular momentum computed in two ways
TEST_F(AngMomMatTest, CompareAngMom) {
char error[1024];
MjModelPtr model =
LoadModelFromString(AngMomTestingModel, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
int nv = model->nv;
int bodyid = mj_name2id(model.get(), mjOBJ_BODY, "link1");
MjDataPtr data = MakeData(model);
// reset to the keyframe with some angular velocities
mj_resetDataKeyframe(model.get(), data.get(), 0);
mj_forward(model.get(), data.get());
// get the reference value of angular momentum
mj_subtreeVel(model.get(), data.get());
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.get(), data.get(), angmom_mat, bodyid);
mjtNum angmom_test[3];
mju_mulMatVec(angmom_test, angmom_mat, data->qvel, 3, nv);
// compare the two angular momentum values
for (int i = 0; i < 3; i++) {
EXPECT_THAT(angmom_ref[i], MjNear(angmom_test[i], 1e-8, 1e-4));
}
mju_free(angmom_mat);
}
// compare subtree angular momentum matrix: analytical and findiff
TEST_F(AngMomMatTest, CompareAngMomMats) {
char error[1024];
MjModelPtr model =
LoadModelFromString(AngMomTestingModel, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
int nv = model->nv;
int bodyid = mj_name2id(model.get(), mjOBJ_BODY, "link1");
MjDataPtr data = 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.get(), data.get(), 0);
mj_forward(model.get(), data.get());
// compute the angular momentum matrix using the analytical method
mj_angmomMat(model.get(), data.get(), angmom_mat, bodyid);
// compute the angular momentum matrix using finite differences
static const mjtNum eps = MjEps(1e-6, 1e-3);
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.get(), data.get());
mj_subtreeVel(model.get(), data.get());
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.get(), data.get());
mj_subtreeVel(model.get(), data.get());
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
for (int i = 0; i < 3 * nv; i++) {
EXPECT_THAT(angmom_mat_fd[i], MjNear(angmom_mat[i], 1e-8, 2e-4));
}
mju_free(angmom_mat_fd);
mju_free(angmom_mat);
}
using JacobianTest = MujocoTest;
static const mjtNum max_abs_err = std::numeric_limits<float>::epsilon();
static constexpr char kJacobianTestingModel[] = R"(
<mujoco>
<worldbody>
<body name="distractor1" pos="0 0 .3">
<freejoint/>
<geom size=".1"/>
</body>
<body name="main">
<freejoint/>
<geom size=".1"/>
<body pos=".1 0 0">
<joint axis="0 1 0"/>
<geom type="capsule" size=".03" fromto="0 0 0 .2 0 0"/>
</body>
<body pos="0 .1 0">
<joint type="ball"/>
<geom type="capsule" size=".03" fromto="0 0 0 0 .2 0"/>
<body pos="0 .2 0">
<joint type="slide" axis="1 1 1"/>
<geom size=".05"/>
</body>
</body>
</body>
<body name="distractor2" pos="0 0 -.3">
<freejoint/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
// compare analytic and finite-differenced subtree-com Jacobian
TEST_F(JacobianTest, SubtreeJac) {
char error[1024];
MjModelPtr model =
LoadModelFromString(kJacobianTestingModel, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
int nv = model->nv;
int bodyid = mj_name2id(model.get(), mjOBJ_BODY, "main");
MjDataPtr data = 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.get(), data.get());
mj_comPos(model.get(), data.get());
// get subtree CoM Jacobian of free body
mj_jacSubtreeCom(model.get(), data.get(), 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.get(), data->qpos, nudge, eps);
nudge[i] = 0;
// kinematics and comPos to get nudged com
mj_kinematics(model.get(), data.get());
mj_comPos(model.get(), data.get());
// 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], MjNear(findiff, eps, 1e-2));
}
}
mju_free(nudge);
mju_free(qpos);
mju_free(jac_subtree);
}
// 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) {
char error[1024];
MjModelPtr model =
LoadModelFromString(kJacobianTestingModel, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
int nv = model->nv;
int bodyid = mj_name2id(model.get(), mjOBJ_BODY, "main");
MjDataPtr data = 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.get(), data.get());
mj_comPos(model.get(), data.get());
// get subtree CoM Jacobian of free body
mj_jacSubtreeCom(model.get(), data.get(), 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.get(), data.get());
// 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.get(), data.get(), 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], MjNear(expected, max_abs_err, 1e-4));
}
}
mju_free(jac_subtree);
}
static constexpr char kQuat[] = R"(
<mujoco>
<worldbody>
<body name="query">
<joint type="ball"/>
<geom size="1"/>
<site name="query" pos=".1 .2 .3"/>
</body>
</worldbody>
<keyframe>
<key qvel="2 3 5"/>
</keyframe>
</mujoco>
)";
static constexpr char kFreeBall[] = R"(
<mujoco>
<worldbody>
<body name="distractor1" pos="0 0 .3">
<freejoint/>
<geom size=".1"/>
</body>
<body name="main">
<freejoint/>
<geom size=".1"/>
<body pos=".1 0 0">
<joint axis="0 1 0"/>
<geom type="capsule" size=".03" fromto="0 0 0 .2 0 0"/>
<body pos=".2 0 0">
<joint type="ball" stiffness="20"/>
<geom type="capsule" size=".03" fromto="0 0 0 0 .2 0"/>
<body name="query" pos="0 .2 0">
<joint type="slide" axis="1 1 1"/>
<geom size=".05"/>
<site name="query" pos=".1 .2 .3"/>
</body>
</body>
</body>
</body>
<body name="distractor2" pos="0 0 -.3">
<freejoint/>
<geom size=".1"/>
</body>
</worldbody>
<keyframe>
<key qvel="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"/>
</keyframe>
</mujoco>
)";
static constexpr char kQuatlessPendulum[] = R"(
<mujoco>
<option integrator="implicit">
<flag constraint="disable"/>
</option>
<worldbody>
<body pos="0.15 0 0">
<joint type="hinge" axis="0 1 0"/>
<geom type="capsule" size="0.02" fromto="0 0 0 .1 0 0"/>
<body pos="0.1 0 0">
<joint type="slide" axis="1 0 0" stiffness="200"/>
<geom type="capsule" size="0.015" fromto="-.1 0 0 .1 0 0"/>
<body pos=".1 0 0">
<joint axis="1 0 0"/>
<joint axis="0 1 0"/>
<joint axis="0 0 1"/>
<geom type="box" size=".02" fromto="0 0 0 0 .1 0"/>
<body name="query" pos="0 .1 0">
<joint axis="1 0 0"/>
<geom type="capsule" size="0.02" fromto="0 0 0 0 .1 0"/>
<site name="query" pos=".1 0 0"/>
</body>
</body>
</body>
</body>
</worldbody>
</mujoco>
)";
static constexpr char kTelescope[] = R"(
<mujoco>
<worldbody>
<body>
<joint type="ball"/>
<geom type="capsule" size="0.02" fromto="0 .02 0 .1 .02 0"/>
<body pos=".1 .02 0">
<joint type="slide" axis="1 0 0"/>
<geom type="capsule" size="0.02" fromto="0 0 0 .1 0 0"/>
<body pos=".1 .02 0">
<joint type="slide" axis="1 0 0"/>
<geom type="capsule" size="0.02" fromto="0 0 0 .1 0 0"/>
<body pos=".1 .02 0" name="query">
<joint type="slide" axis="1 0 0"/>
<geom type="capsule" size="0.02" fromto="0 0 0 .1 0 0"/>
<site name="query" pos=".1 0 0"/>
</body>
</body>
</body>
</body>
</worldbody>
<keyframe>
<key qvel="1 1 1 1 1 1"/>
</keyframe>
</mujoco>
)";
static constexpr char kHinge[] = R"(
<mujoco>
<worldbody>
<body name="query">
<joint name="link1" axis="0 1 0"/>
<geom type="capsule" size=".02" fromto="0 0 0 0 0 -1"/>
<site name="query" pos="0 0 -1"/>
</body>
</worldbody>
<keyframe>
<key qpos="1" qvel="1"/>
</keyframe>
</mujoco>
)";
// compare mj_jacDot with finite-differenced mj_jac
TEST_F(JacobianTest, JacDot) {
for (auto xml : {kHinge, kQuat, kTelescope, kFreeBall, kQuatlessPendulum}) {
char error[1024];
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
int nv = model->nv;
MjDataPtr data = MakeData(model);
// load keyframe if present, step for a bit
if (model->nkey) mj_resetDataKeyframe(model.get(), data.get(), 0);
while (data->time < 0.1) {
mj_step(model.get(), data.get());
}
// minimal call required for mj_jacDot outputs to be valid
mj_kinematics(model.get(), data.get());
mj_comPos(model.get(), data.get());
mj_comVel(model.get(), data.get());
// get bodyid
int bodyid = mj_name2id(model.get(), mjOBJ_BODY, "query");
EXPECT_GT(bodyid, 0);
// get site position
int siteid = mj_name2id(model.get(), mjOBJ_SITE, "query");
EXPECT_GT(siteid, -1);
mjtNum point[3];
mju_copy3(point, data->site_xpos + 3 * siteid);
// jac, jac_dot
std::vector<mjtNum> jacp(3 * nv);
std::vector<mjtNum> jacr(3 * nv);
mj_jac(model.get(), data.get(), jacp.data(), jacr.data(), point, bodyid);
std::vector<mjtNum> jacp_dot(3 * nv);
std::vector<mjtNum> jacr_dot(3 * nv);
mj_jacDot(model.get(), data.get(), jacp_dot.data(), jacr_dot.data(), point,
bodyid);
// jac_h: jacobian after integrating qpos with a timestep of h
const mjtNum h = MjTol(1e-7, 5e-4);
mj_integratePos(model.get(), data->qpos, data->qvel, h);
mj_kinematics(model.get(), data.get());
mj_comPos(model.get(), data.get());
std::vector<mjtNum> jacp_h(3 * nv);
std::vector<mjtNum> jacr_h(3 * nv);
mju_copy3(point,
data->site_xpos + 3 * siteid); // get updated site position
mj_jac(model.get(), data.get(), jacp_h.data(), jacr_h.data(), point,
bodyid);
// jac_dot_h finite-difference approximation
std::vector<mjtNum> jacp_dot_h(3 * nv);
mju_sub(jacp_dot_h.data(), jacp_h.data(), jacp.data(), 3 * nv);
mju_scl(jacp_dot_h.data(), jacp_dot_h.data(), 1 / h, 3 * nv);
std::vector<mjtNum> jacr_dot_h(3 * nv);
mju_sub(jacr_dot_h.data(), jacr_h.data(), jacr.data(), 3 * nv);
mju_scl(jacr_dot_h.data(), jacr_dot_h.data(), 1 / h, 3 * nv);
// compare finite-differenced and analytic
mjtNum tol = 1e-5;
EXPECT_THAT(jacp_dot, Pointwise(MjNear(tol, 5e-2), jacp_dot_h));
EXPECT_THAT(jacr_dot, Pointwise(MjNear(tol, 5e-2), jacr_dot_h));
}
}
// compare mj_jacDotSparse with dense mj_jacDot
TEST_F(JacobianTest, JacDotSparse) {
for (auto xml : {kHinge, kQuat, kTelescope, kFreeBall, kQuatlessPendulum}) {
char error[1024];
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model.get(), NotNull()) << error;
int nv = model->nv;
MjDataPtr data = MakeData(model);
// load keyframe if present, step for a bit
if (model->nkey) mj_resetDataKeyframe(model.get(), data.get(), 0);
while (data->time < 0.1) {
mj_step(model.get(), data.get());
}
// minimal call required for mj_jacDot outputs to be valid
mj_kinematics(model.get(), data.get());
mj_comPos(model.get(), data.get());
mj_comVel(model.get(), data.get());
// get bodyid and site position
int bodyid = mj_name2id(model.get(), mjOBJ_BODY, "query");
EXPECT_GT(bodyid, 0);
int siteid = mj_name2id(model.get(), mjOBJ_SITE, "query");
EXPECT_GT(siteid, -1);
mjtNum point[3];
mju_copy3(point, data->site_xpos + 3 * siteid);
// dense jacDot
std::vector<mjtNum> jacp_dense(3 * nv);
std::vector<mjtNum> jacr_dense(3 * nv);
mj_jacDot(model.get(), data.get(), jacp_dense.data(), jacr_dense.data(),
point, bodyid);
// compute body chain using public mjModel fields
std::vector<int> chain(nv);
int NV = 0;
int weldbody = model->body_weldid[bodyid];
if (weldbody) {
int da = model->body_dofadr[weldbody] + model->body_dofnum[weldbody] - 1;
while (da >= 0) {
chain[NV++] = da;
da = model->dof_parentid[da];
}
std::reverse(chain.begin(), chain.begin() + NV);
}
EXPECT_GT(NV, 0);
// sparse jacDot
std::vector<mjtNum> jacp_sparse(3 * NV);
std::vector<mjtNum> jacr_sparse(3 * NV);
mj_jacDotSparse(model.get(), data.get(), jacp_sparse.data(),
jacr_sparse.data(), point, bodyid, NV, chain.data());
// expand sparse to dense and compare
std::vector<mjtNum> jacp_expanded(3 * nv, 0);
std::vector<mjtNum> jacr_expanded(3 * nv, 0);
for (int ci = 0; ci < NV; ci++) {
int di = chain[ci];
for (int r = 0; r < 3; r++) {
jacp_expanded[di + r * nv] = jacp_sparse[ci + r * NV];
jacr_expanded[di + r * nv] = jacr_sparse[ci + r * NV];
}
}
// expect bitwise equality
EXPECT_EQ(jacp_expanded, jacp_dense);
EXPECT_EQ(jacr_expanded, jacr_dense);
}
}
// validate rotational Jacobian used in welds
TEST_F(JacobianTest, WeldRotJacobian) {
#ifdef mjUSESINGLE
GTEST_SKIP() << "FD Jacobian with eps=1e-6 below float32 precision";
#endif
constexpr char xml[] = R"(
<mujoco>
<option jacobian="dense"/>
<worldbody>
<body>
<joint type="ball"/>
<geom size=".1"/>
</body>
<body pos=".5 0 0">
<joint axis="1 0 0" pos="0 0 .01"/>
<joint axis="0 1 0" pos=".02 0 0"/>
<joint axis="0 0 1" pos="0 .03 0"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(model.get(), testing::NotNull()) << error;
ASSERT_EQ(model->nq, 7);
ASSERT_EQ(model->nv, 6);
static const int nv = 6; // for increased readability
MjDataPtr data = MakeData(model);
// arbitrary initial values for the ball and hinge joints
mjtNum qpos0[7] = {.5, .5, .5, .5, .7, .8, .9};
// compute required quantities using mj_step1
mj_step1(model.get(), data.get());
// get orientation error
mjtNum res[3];
// compute rotation residual following formula in mj_instantiateEquality
auto RotationResidual = [](const mjModel* model, mjData* data,
const mjtNum qpos[7], const mjtNum dqpos[6],
mjtNum res[3]) {
// copy configuration, compute required quantities with mj_step1
mju_copy(data->qpos, qpos, 7);
// perturb configuration if given
if (dqpos) {
mj_integratePos(model, data->qpos, dqpos, 1);
}
// update relevant quantities
mj_step1(model, data);
// compute orientation residual
mjtNum quat1[4], quat2[4], quat3[4];
mju_copy4(quat1, data->xquat + 4 * 1);
mju_negQuat(quat2, data->xquat + 4 * 2);
mju_mulQuat(quat3, quat2, quat1);
mju_copy3(res, quat3 + 1);
};
RotationResidual(model.get(), data.get(), qpos0, NULL, res);
// compute Jacobian with finite-differencing
mjtNum jacFD[3 * nv];
mjtNum dqpos[nv] = {0};
mjtNum dres[3];
const mjtNum eps = 1e-6;
for (int i = 0; i < nv; i++) {
// nudge i-th dof
dqpos[i] = eps;
// get nudged residual
RotationResidual(model.get(), data.get(), qpos0, dqpos, dres);
// remove nudge
dqpos[i] = 0.0;
// compute Jacobian column
for (int j = 0; j < 3; j++) {
jacFD[nv * j + i] = (dres[j] - res[j]) / eps;
}
}
// reset mjData to qpos0
mju_copy(data->qpos, qpos0, 7);
mj_step1(model.get(), data.get());
// intermediate quaternions quat1 and quat2
mjtNum quat1[4], negQuat2[4];
mju_copy4(quat1, data->xquat + 4 * 1);
mju_negQuat(negQuat2, data->xquat + 4 * 2);
// get analytical Jacobian following formula in mj_instantiateEquality
mjtNum jacdif[3 * nv], jac0[3 * nv], jac1[3 * nv];
mjtNum point[3] = {0};
// rotational Jacobian difference
mj_jacDifPair(model.get(), data.get(), NULL, 2, 1, point, point, NULL, NULL,
NULL, jac0, jac1, jacdif, mj_isSparse(model.get()),
/*flg_skipcommon=*/0);
// formula: 0.5 * neg(quat2) * (jac1-jac2) * quat1
mjtNum axis[3], quat3[4], quat4[4];
for (int j = 0; j < nv; j++) {
// axis = [jac1-jac2]_col(j)
axis[0] = jacdif[0 * nv + j];
axis[1] = jacdif[1 * nv + j];
axis[2] = jacdif[2 * nv + j];
// apply formula
mju_mulQuatAxis(quat3, negQuat2, axis);
mju_mulQuat(quat4, quat3, quat1);
// correct Jacobian
jacdif[0 * nv + j] = 0.5 * quat4[1];
jacdif[1 * nv + j] = 0.5 * quat4[2];
jacdif[2 * nv + j] = 0.5 * quat4[3];
}
// test that analytical and finite-differenced Jacobians match
EXPECT_THAT(AsVector(jacFD, 3 * nv),
Pointwise(MjNear(eps, 1e-3), AsVector(jacdif, 3 * nv)));
}
} // namespace
} // namespace mujoco