Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
7fb444268This file is pinned to the selected release. Download the original to open it in your design tool.
// 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_core_constraint.c.
#include <cstddef>
#include <cstring>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mujoco.h>
#include "src/engine/engine_core_constraint.h"
#include "src/engine/engine_support.h"
#include "test/fixture.h"
namespace mujoco {
namespace {
using ::testing::DoubleNear;
using ::testing::Pointwise;
using CoreConstraintTest = MujocoTest;
std::vector<mjtNum> AsVector(const mjtNum* array, int n) {
return std::vector<mjtNum>(array, array + n);
}
// compute rotation residual following formula in mj_instantiateEquality
void 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);
}
// validate rotational Jacobian used in welds
TEST_F(CoreConstraintTest, WeldRotJacobian) {
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>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, testing::NotNull());
ASSERT_EQ(model->nq, 7);
ASSERT_EQ(model->nv, 6);
static const int nv = 6; // for increased readability
mjData* data = mj_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, data);
// get orientation error
mjtNum res[3];
RotationResidual(model, data, 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, data, 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, data);
// 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, data, NULL, 2, 1, point, point,
NULL, NULL, NULL, jac0, jac1, jacdif);
// 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(DoubleNear(eps), AsVector(jacdif, 3*nv)));
mj_deleteData(data);
mj_deleteModel(model);
}
// test formulas for penetration at rest
TEST_F(CoreConstraintTest, RestPenetration) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<geom type="plane" size="1 1 1"/>
<body pos="0 0 .2">
<joint type="slide" axis="0 0 1"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, testing::NotNull());
mjtNum gravity = -model->opt.gravity[2];
mjtNum damping_ratio = 0.8;
mjData* data = mj_makeData(model);
for (const mjtNum reference : {-100.0, -10.0, 0.1, 0.01}) {
for (const mjtNum impedance : {0.3, 0.9, 0.99}) {
// set solimp
for (int i=0; i < model->ngeom; i++) {
model->geom_solimp[i*mjNIMP + 0] = impedance;
model->geom_solimp[i*mjNIMP + 1] = impedance;
}
// set solref
for (int i=0; i < model->ngeom; i++) {
model->geom_solref[i*mjNREF + 0] = reference;
model->geom_solref[i*mjNREF + 1] = reference < 0 ? -10 : damping_ratio;
}
// simulate for 50 seconds
mj_resetData(model, data);
while (data->time < 50) {
mj_step(model, data);
}
mjtNum depth = -data->contact[0].dist;
mjtNum expected_depth;
if (reference < 0) {
expected_depth = gravity * (1 - impedance) / -reference;
} else {
mjtNum tc_dr = reference * damping_ratio;
expected_depth = gravity * (1 - impedance) * tc_dr * tc_dr;
}
EXPECT_THAT(depth, DoubleNear(expected_depth, 1e-10));
}
}
mj_deleteData(data);
mj_deleteModel(model);
}
static const char* const kDoflessContactPath =
"engine/testdata/core_constraint/dofless_contact.xml";
static const char* const kDoflessTendonFrictionalPath =
"engine/testdata/core_constraint/dofless_tendon_frictional.xml";
static const char* const kDoflessTendonLimitedPath =
"engine/testdata/core_constraint/dofless_tendon_limited.xml";
static const char* const kDoflessTendonLimitedMarginPath =
"engine/testdata/core_constraint/dofless_tendon_limitedmargin.xml";
static const char* const kDoflessWeldPath =
"engine/testdata/core_constraint/dofless_weld.xml";
static const char* const kJointLimitedBilateralMarginPath =
"engine/testdata/core_constraint/joint_limited_bilateral_margin.xml";
static const char* const kTendonLimitedBilateralMarginPath =
"engine/testdata/core_constraint/tendon_limited_bilateral_margin.xml";
TEST_F(CoreConstraintTest, JacobianPreAllocate) {
for (const char* local_path :
{kDoflessContactPath, kDoflessTendonFrictionalPath,
kDoflessTendonLimitedPath, kDoflessTendonLimitedMarginPath,
kDoflessWeldPath, kJointLimitedBilateralMarginPath,
kTendonLimitedBilateralMarginPath}) {
const std::string xml_path = GetTestDataFilePath(local_path);
// iterate through dense and sparse
for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) {
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0);
model->opt.jacobian = sparsity;
mjData* data = mj_makeData(model);
mj_step(model, data);
mj_deleteData(data);
mj_deleteModel(model);
}
}
}
static const char* const kIlslandEfcPath =
"engine/testdata/island/island_efc.xml";
TEST_F(CoreConstraintTest, MulJacVecIsland) {
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_nv, fill with arbitrary values
mjtNum* vec_nv = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv);
for (int i=0; i < model->nv; i++) {
vec_nv[i] = 0.2 + 0.3*i;
}
// iterate through dense and sparse
for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) {
model->opt.jacobian = sparsity;
// simulate for 0.2 seconds
mj_resetData(model, data);
while (data->time < 0.2) {
mj_step(model, data);
}
mj_forward(model, data);
// multiply by Jacobian: vec_nefc = J * vec_nv
mjtNum* vec_nefc = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc);
mj_mulJacVec(model, data, vec_nefc, vec_nv);
mjtNum* vec_nefc_tmp = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc);
// iterate over islands
for (int i=0; i < data->nisland; i++) {
// allocate dof and efc vectors for island
int dofnum = data->island_dofnum[i];
mjtNum* vec_nvi = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum);
int efcnum = data->island_efcnum[i];
mjtNum* vec_nefci = (mjtNum*)mju_malloc(sizeof(mjtNum) * efcnum);
// get indices
int* dofind = data->island_dofind + data->island_dofadr[i];
int* efcind = data->island_efcind + data->island_efcadr[i];
// copy values into vec_nvi
for (int j=0; j < dofnum; j++) {
vec_nvi[j] = vec_nv[dofind[j]];
}
// ===== both compressed
int flg_resunc = 0;
int flg_vecunc = 0;
mju_zero(vec_nefci, efcnum); // clear output
mj_mulJacVec_island(model, data, vec_nefci, vec_nvi,
i, flg_resunc, flg_vecunc);
// expect corresponding values to match
for (int j=0; j < efcnum; j++) {
EXPECT_THAT(vec_nefci[j], DoubleNear(vec_nefc[efcind[j]], 1e-12));
}
// ===== input uncompressed: read from vec_nv
flg_resunc = 0;
flg_vecunc = 1;
mju_zero(vec_nefci, efcnum); // clear output
mj_mulJacVec_island(model, data, vec_nefci, vec_nv,
i, flg_resunc, flg_vecunc);
// expect corresponding values to match
for (int j=0; j < efcnum; j++) {
EXPECT_THAT(vec_nefci[j], DoubleNear(vec_nefc[efcind[j]], 1e-12));
}
// ===== output uncompressed: write to vec_nefc_tmp
flg_resunc = 1;
flg_vecunc = 0;
mju_zero(vec_nefc_tmp, data->nefc); // clear output
mj_mulJacVec_island(model, data, vec_nefc_tmp, vec_nvi,
i, flg_resunc, flg_vecunc);
// expect corresponding values to match
for (int j=0; j < efcnum; j++) {
EXPECT_THAT(vec_nefc_tmp[efcind[j]],
DoubleNear(vec_nefc[efcind[j]], 1e-12));
}
mju_free(vec_nvi);
mju_free(vec_nefci);
}
mju_free(vec_nefc_tmp);
mju_free(vec_nefc);
}
mju_free(vec_nv);
mj_deleteData(data);
mj_deleteModel(model);
}
TEST_F(CoreConstraintTest, MulJacTVecIsland) {
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_nv
mjtNum* vec_nv = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv);
mjtNum* vec_nv_tmp = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv);
// iterate through dense and sparse
for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) {
model->opt.jacobian = sparsity;
// simulate for 0.3 seconds
mj_resetData(model, data);
while (data->time < 0.3) {
mj_step(model, data);
}
mj_forward(model, data);
// allocate vec_nefc, fill with arbitrary values
mjtNum* vec_nefc = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc);
for (int i=0; i < data->nefc; i++) {
vec_nefc[i] = 0.2 + 0.3*i;
}
// multiply by Jacobian: vec_nv = J^T * vec_nefc
mj_mulJacTVec(model, data, vec_nv, vec_nefc);
// iterate over islands
for (int i=0; i < data->nisland; i++) {
// allocate dof and efc vectors for island
int dofnum = data->island_dofnum[i];
mjtNum* vec_nvi = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum);
int efcnum = data->island_efcnum[i];
mjtNum* vec_nefci = (mjtNum*)mju_malloc(sizeof(mjtNum) * efcnum);
// get indices
int* efcind = data->island_efcind + data->island_efcadr[i];
int* dofind = data->island_dofind + data->island_dofadr[i];
// copy values into vec_nefci
for (int j=0; j < efcnum; j++) {
vec_nefci[j] = vec_nefc[efcind[j]];
}
// ==== both compressed
int flg_resunc = 0;
int flg_vecunc = 0;
mju_zero(vec_nvi, dofnum); // clear output
mj_mulJacTVec_island(model, data, vec_nvi, vec_nefci,
i, flg_resunc, flg_vecunc);
// expect corresponding values to match
for (int j=0; j < dofnum; j++) {
EXPECT_THAT(vec_nvi[j], DoubleNear(vec_nv[dofind[j]], 1e-12));
}
// ===== input uncompressed: read from vec_nefc
flg_resunc = 0;
flg_vecunc = 1;
mju_zero(vec_nvi, dofnum); // clear output
mj_mulJacTVec_island(model, data, vec_nvi, vec_nefc,
i, flg_resunc, flg_vecunc);
// expect corresponding values to match
for (int j=0; j < dofnum; j++) {
EXPECT_THAT(vec_nvi[j], DoubleNear(vec_nv[dofind[j]], 1e-12));
}
// ===== output uncompressed: write to vec_nv_tmp
flg_resunc = 1;
flg_vecunc = 0;
mju_zero(vec_nv_tmp, model->nv); // clear output
mj_mulJacTVec_island(model, data, vec_nv_tmp, vec_nefci,
i, flg_resunc, flg_vecunc);
// expect corresponding values to match
for (int j=0; j < dofnum; j++) {
EXPECT_THAT(vec_nv_tmp[dofind[j]],
DoubleNear(vec_nv[dofind[j]], 1e-12));
}
mju_free(vec_nvi);
mju_free(vec_nefci);
}
mju_free(vec_nefc);
}
mju_free(vec_nv_tmp);
mju_free(vec_nv);
mj_deleteData(data);
mj_deleteModel(model);
}
TEST_F(CoreConstraintTest, ConstraintUpdateIsland) {
const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath);
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0);
mjData* data1 = mj_makeData(model);
mjData* data2 = mj_makeData(model);
// iterate over sparsity and cone
for (mjtJacobian sparsity : {mjJAC_SPARSE, mjJAC_DENSE}) {
for (mjtCone cone : {mjCONE_PYRAMIDAL, mjCONE_ELLIPTIC}) {
model->opt.jacobian = sparsity;
model->opt.cone = cone;
// simulate for 0.2 seconds
mj_resetData(model, data1);
mj_resetData(model, data2);
while (data1->time < 0.2) {
mj_step(model, data1);
mj_step(model, data2);
}
mj_forward(model, data1);
mj_forward(model, data2);
// get sizes
int nefc = data1->nefc;
int nv = model->nv;
int nisland = data1->nisland;
EXPECT_GT(nisland, 0);
// get jar = J*a - aref
mjtNum* jar = (mjtNum*)mju_malloc(sizeof(mjtNum) * nefc);
mj_mulJacVec(model, data1, jar, data1->qacc);
mju_subFrom(jar, data1->efc_aref, nefc);
// constraint update for data1 given jar
mjtNum cost1;
mj_constraintUpdate(model, data1, jar, &cost1, /*flg_coneHessian=*/1);
// iterate over islands, check match
mjtNum cost2 = 0;
for (int island=0; island < nisland; island++) {
// clear outputs from data2
for (int i=0; i < nefc; i++) data2->efc_state[i] = -1;
mju_zero(data2->efc_force, nefc);
mju_zero(data2->qfrc_constraint, nv);
for (int i=0; i < data2->ncon; i++) mju_zero(data2->contact[i].H, 36);
// sizes and indices, in this island
int dofnum = data2->island_dofnum[island];
int efcnum = data2->island_efcnum[island];
int* dofind = data2->island_dofind + data2->island_dofadr[island];
int* efcind = data2->island_efcind + data2->island_efcadr[island];
// get jar restricted to island
mjtNum* jari = (mjtNum*)mju_malloc(sizeof(mjtNum) * efcnum);
for (int c=0; c < efcnum; c++) {
jari[c] = jar[efcind[c]];
}
// update constraints for this island
mjtNum cost2i;
mj_constraintUpdate_island(model, data2, jari, &cost2i,
/*flg_coneHessian=*/1, island);
// compare nefc vectors
for (int c=0; c < efcnum; c++) {
int i = efcind[c];
EXPECT_EQ(data2->efc_island[i], island);
EXPECT_EQ(data2->efc_state[i], data1->efc_state[i]);
EXPECT_THAT(data2->efc_force[i],
DoubleNear(data1->efc_force[i], 1e-12));
}
// compare qfrc_constraint
for (int c=0; c < dofnum; c++) {
int i = dofind[c];
EXPECT_THAT(data2->qfrc_constraint[i],
DoubleNear(data1->qfrc_constraint[i], 1e-12));
}
// compare cone Hessians
for (int c=0; c < data2->ncon; c++) {
int efcadr = data2->contact[c].efc_address;
if (data2->efc_island[efcadr] == island) {
for (int j=0; j < 36; j++) {
EXPECT_THAT(data2->contact[c].H[j],
DoubleNear(data2->contact[c].H[j], 1e-12));
}
}
}
// add island cost to total cost
cost2 += cost2i;
mju_free(jari);
}
// expect monolithic total cost
EXPECT_THAT(cost1, DoubleNear(cost2, 1e-12));
mju_free(jar);
}
}
mj_deleteData(data2);
mj_deleteData(data1);
mj_deleteModel(model);
}
} // namespace
} // namespace mujoco