Merge branch 'main' into newton-schemas
This commit is contained in:
@@ -22,7 +22,6 @@
|
||||
#include <absl/base/attributes.h>
|
||||
#include <mujoco/mjdata.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "src/engine/engine_memory.h"
|
||||
#include "src/engine/engine_support.h"
|
||||
#include "src/engine/engine_util_solve.h"
|
||||
#include "src/engine/engine_util_sparse.h"
|
||||
@@ -68,13 +67,15 @@ struct HessianData {
|
||||
// D diagonal
|
||||
std::vector<mjtNum> D;
|
||||
|
||||
int nefc;
|
||||
|
||||
void Setup(const mjModel* m, mjData* d) {
|
||||
// initialize simulation state
|
||||
mj_resetDataKeyframe(m, d, 0);
|
||||
mj_forward(m, d);
|
||||
|
||||
nv = m->nv;
|
||||
int nefc = d->nefc;
|
||||
nefc = d->nefc;
|
||||
|
||||
// compute D corresponding to quad states
|
||||
D.resize(nefc);
|
||||
@@ -205,14 +206,27 @@ mjModel* GetModel() {
|
||||
return m;
|
||||
}
|
||||
|
||||
template <Size S>
|
||||
HessianData& GetHessianData() {
|
||||
static HessianData data;
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
data.Setup(m, d);
|
||||
mj_deleteData(d);
|
||||
initialized = true;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// old implementation benchmark
|
||||
template <Size S>
|
||||
static void BM_chol_old(benchmark::State& state) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
HessianData hd;
|
||||
hd.Setup(m, d);
|
||||
HessianData& hd = GetHessianData<S>();
|
||||
|
||||
std::vector<mjtNum> L_work(hd.nL);
|
||||
std::vector<int> L_colind_work(hd.nL);
|
||||
@@ -239,8 +253,7 @@ static void BM_chol_symbolic(benchmark::State& state) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
HessianData hd;
|
||||
hd.Setup(m, d);
|
||||
HessianData& hd = GetHessianData<S>();
|
||||
|
||||
std::vector<int> L_colind_work(hd.nL);
|
||||
std::vector<int> LT_rownnz_work(hd.nv);
|
||||
@@ -266,8 +279,7 @@ static void BM_chol_numeric(benchmark::State& state) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
HessianData hd;
|
||||
hd.Setup(m, d);
|
||||
HessianData& hd = GetHessianData<S>();
|
||||
|
||||
std::vector<mjtNum> L_work(hd.nL);
|
||||
std::vector<int> L_colind_work(hd.nL);
|
||||
@@ -339,10 +351,6 @@ constexpr int kNumUpdateVectors = 25;
|
||||
int ABSL_ATTRIBUTE_NOINLINE mju_cholUpdateSparse_old(
|
||||
mjtNum* mat, mjtNum* x, int n, int flg_plus, const int* rownnz,
|
||||
const int* rowadr, const int* colind, int x_nnz, int* x_ind, mjData* d) {
|
||||
mj_markStack(d);
|
||||
int* buf_ind = mjSTACKALLOC(d, n, int);
|
||||
mjtNum* sparse_buf = mjSTACKALLOC(d, n, mjtNum);
|
||||
|
||||
int rank = n, i = x_nnz - 1;
|
||||
while (i >= 0) {
|
||||
int nnz = rownnz[x_ind[i]], adr = rowadr[x_ind[i]];
|
||||
@@ -359,10 +367,9 @@ int ABSL_ATTRIBUTE_NOINLINE mju_cholUpdateSparse_old(
|
||||
mju_combineSparseInc(mat + adr, x, n, 1 / c, (flg_plus ? s / c : -s / c),
|
||||
nnz - 1, i, colind + adr, x_ind);
|
||||
int new_x_nnz = mju_combineSparse(x, mat + adr, c, -s, i, nnz - 1, x_ind,
|
||||
colind + adr, sparse_buf, buf_ind);
|
||||
colind + adr);
|
||||
i = i - 1 + (new_x_nnz - i);
|
||||
}
|
||||
mj_freeStack(d);
|
||||
return rank;
|
||||
}
|
||||
|
||||
@@ -371,9 +378,10 @@ template <Size S>
|
||||
static void BM_update_old(benchmark::State& state) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
mj_resetDataKeyframe(m, d, 0);
|
||||
mj_forward(m, d);
|
||||
|
||||
HessianData hd;
|
||||
hd.Setup(m, d);
|
||||
HessianData& hd = GetHessianData<S>();
|
||||
|
||||
int nv = hd.nv;
|
||||
|
||||
@@ -433,9 +441,10 @@ template <Size S>
|
||||
static void BM_update_new(benchmark::State& state) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
mj_resetDataKeyframe(m, d, 0);
|
||||
mj_forward(m, d);
|
||||
|
||||
HessianData hd;
|
||||
hd.Setup(m, d);
|
||||
HessianData& hd = GetHessianData<S>();
|
||||
|
||||
int nv = hd.nv;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
// A benchmark for comparing different implementations of mj_solveLD.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -31,124 +30,180 @@ namespace {
|
||||
|
||||
using CombineFuncPtr = decltype(&mju_combineSparse);
|
||||
using TransposeFuncPtr = decltype(&mju_transposeSparse);
|
||||
using SqrMatTDFuncPtr = decltype(&mju_sqrMatTDSparse);
|
||||
|
||||
// number of steps to roll out before benchmarking
|
||||
static const int kNumWarmupSteps = 500;
|
||||
// ================================ Cached Data ================================
|
||||
|
||||
// ----------------------------- old functions --------------------------------
|
||||
// ---- MatVecSparse data ----
|
||||
struct MatVecData {
|
||||
int nv;
|
||||
int nefc;
|
||||
int nJ;
|
||||
std::vector<mjtNum> efc_J;
|
||||
std::vector<int> efc_J_rownnz, efc_J_rowadr, efc_J_colind, efc_J_rowsuper;
|
||||
std::vector<mjtNum> vec;
|
||||
};
|
||||
|
||||
void ABSL_ATTRIBUTE_NOINLINE mju_sqrMatTDSparse_baseline(
|
||||
mjtNum* res, const mjtNum* mat, const mjtNum* matT, const mjtNum* diag,
|
||||
int nr, int nc, int* res_rownnz, int* res_rowadr, int* res_colind,
|
||||
const int* rownnz, const int* rowadr, const int* colind,
|
||||
const int* rowsuper, const int* rownnzT, const int* rowadrT,
|
||||
const int* colindT, const int* rowsuperT, mjData* d, int* unused) {
|
||||
mj_markStack(d);
|
||||
int* chain = mj_stackAllocInt(d, 2 * nc);
|
||||
mjtNum* buffer = mj_stackAllocNum(d, nc);
|
||||
MatVecData& GetMatVecData() {
|
||||
static MatVecData data = [] {
|
||||
MatVecData d;
|
||||
mjModel* m = LoadModelFromPath("flex/flag.xml");
|
||||
mjData* dat = mj_makeData(m);
|
||||
|
||||
for (int r = 0; r < nc; r++) {
|
||||
res_rowadr[r] = r * nc;
|
||||
}
|
||||
|
||||
for (int r = 0; r < nc; r++) {
|
||||
if (rowsuperT && r > 0 && rowsuperT[r - 1] > 0) {
|
||||
res_rownnz[r] = res_rownnz[r - 1];
|
||||
memcpy(res_colind + res_rowadr[r], res_colind + res_rowadr[r - 1],
|
||||
res_rownnz[r] * sizeof(int));
|
||||
|
||||
if (rownnzT[r]) {
|
||||
res_colind[res_rowadr[r] + res_rownnz[r]] = r;
|
||||
res_rownnz[r]++;
|
||||
}
|
||||
} else {
|
||||
int nchain = 0;
|
||||
int inew = 0, iold = nc;
|
||||
int lastadded = -1;
|
||||
for (int i = 0; i < rownnzT[r]; i++) {
|
||||
int c = colindT[rowadrT[r] + i];
|
||||
if (rowsuper && lastadded >= 0 &&
|
||||
(c - lastadded) <= rowsuper[lastadded]) {
|
||||
continue;
|
||||
} else {
|
||||
lastadded = c;
|
||||
}
|
||||
|
||||
int adr = inew;
|
||||
inew = iold;
|
||||
iold = adr;
|
||||
|
||||
int nnewchain = 0;
|
||||
adr = 0;
|
||||
int end = rowadr[c] + rownnz[c];
|
||||
for (int adr1 = rowadr[c]; adr1 < end; adr1++) {
|
||||
int col_mat = colind[adr1];
|
||||
while (adr < nchain && chain[iold + adr] < col_mat &&
|
||||
chain[iold + adr] <= r) {
|
||||
chain[inew + nnewchain++] = chain[iold + adr++];
|
||||
}
|
||||
|
||||
if (col_mat > r) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (adr < nchain && chain[iold + adr] == col_mat) {
|
||||
adr++;
|
||||
}
|
||||
chain[inew + nnewchain++] = col_mat;
|
||||
}
|
||||
|
||||
while (adr < nchain && chain[iold + adr] <= r) {
|
||||
chain[inew + nnewchain++] = chain[iold + adr++];
|
||||
}
|
||||
nchain = nnewchain;
|
||||
}
|
||||
res_rownnz[r] = nchain;
|
||||
if (nchain) {
|
||||
memcpy(res_colind + res_rowadr[r], chain + inew, nchain * sizeof(int));
|
||||
}
|
||||
for (int i = 0; i < 500; i++) {
|
||||
mj_step(m, dat);
|
||||
}
|
||||
}
|
||||
|
||||
for (int r = 0; r < nc; r++) {
|
||||
int adr = res_rowadr[r];
|
||||
for (int i = 0; i < res_rownnz[r]; i++) {
|
||||
buffer[res_colind[adr + i]] = 0;
|
||||
}
|
||||
for (int i = 0; i < rownnzT[r]; i++) {
|
||||
int c = colindT[rowadrT[r] + i];
|
||||
mjtNum matTrc = matT[rowadrT[r] + i];
|
||||
if (diag) {
|
||||
matTrc *= diag[c];
|
||||
}
|
||||
d.nv = m->nv;
|
||||
d.nefc = dat->nefc;
|
||||
d.nJ = dat->nJ;
|
||||
d.efc_J.assign(dat->efc_J, dat->efc_J + d.nJ);
|
||||
d.efc_J_rownnz.assign(dat->efc_J_rownnz, dat->efc_J_rownnz + d.nefc);
|
||||
d.efc_J_rowadr.assign(dat->efc_J_rowadr, dat->efc_J_rowadr + d.nefc);
|
||||
d.efc_J_colind.assign(dat->efc_J_colind, dat->efc_J_colind + d.nJ);
|
||||
d.efc_J_rowsuper.assign(dat->efc_J_rowsuper, dat->efc_J_rowsuper + d.nefc);
|
||||
|
||||
int end = rowadr[c] + rownnz[c];
|
||||
for (int adr = rowadr[c]; adr < end; adr++) {
|
||||
int adr1;
|
||||
if ((adr1 = colind[adr]) > r) {
|
||||
break;
|
||||
}
|
||||
buffer[adr1] += matTrc * mat[adr];
|
||||
}
|
||||
// compute direction: vec = -M^{-1} * (Ma - qfrc_smooth - qfrc_constraint)
|
||||
mj_markStack(dat);
|
||||
mjtNum* Ma = mj_stackAllocNum(dat, m->nv);
|
||||
mjtNum* grad = mj_stackAllocNum(dat, m->nv);
|
||||
mjtNum* Mgrad = mj_stackAllocNum(dat, m->nv);
|
||||
mj_mulM(m, dat, Ma, dat->qacc);
|
||||
for (int i = 0; i < m->nv; i++) {
|
||||
grad[i] = Ma[i] - dat->qfrc_smooth[i] - dat->qfrc_constraint[i];
|
||||
}
|
||||
adr = res_rowadr[r];
|
||||
for (int i = 0; i < res_rownnz[r]; i++) {
|
||||
res[adr + i] = buffer[res_colind[adr + i]];
|
||||
}
|
||||
}
|
||||
for (int r = 1; r < nc; r++) {
|
||||
int end = res_rowadr[r] + res_rownnz[r] - 1;
|
||||
for (int adr = res_rowadr[r]; adr < end; adr++) {
|
||||
int adr1 = res_rowadr[res_colind[adr]] + res_rownnz[res_colind[adr]]++;
|
||||
res[adr1] = res[adr];
|
||||
res_colind[adr1] = r;
|
||||
}
|
||||
}
|
||||
mj_solveM(m, dat, Mgrad, grad, 1);
|
||||
d.vec.resize(m->nv);
|
||||
mju_scl(d.vec.data(), Mgrad, -1, m->nv);
|
||||
mj_freeStack(dat);
|
||||
|
||||
mj_freeStack(d);
|
||||
mj_deleteData(dat);
|
||||
mj_deleteModel(m);
|
||||
return d;
|
||||
}();
|
||||
return data;
|
||||
}
|
||||
|
||||
// ---- CombineSparse data ----
|
||||
struct CombineData {
|
||||
int nv;
|
||||
std::vector<mjtNum> H;
|
||||
std::vector<int> rownnz, rowadr, colind;
|
||||
};
|
||||
|
||||
CombineData& GetCombineData() {
|
||||
static CombineData data = [] {
|
||||
CombineData cd;
|
||||
mjModel* m = LoadModelFromPath("humanoid/humanoid.xml");
|
||||
m->opt.jacobian = mjJAC_SPARSE;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
for (int i = 0; i < 500; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
cd.nv = m->nv;
|
||||
mj_markStack(d);
|
||||
mjtNum* H = mj_stackAllocNum(d, m->nv*m->nv);
|
||||
int* rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* colind = mj_stackAllocInt(d, m->nv*m->nv);
|
||||
int* diagind = mj_stackAllocInt(d, m->nv);
|
||||
|
||||
mjtNum* D = mj_stackAllocNum(d, d->nefc);
|
||||
for (int i = 0; i < d->nefc; i++) {
|
||||
if (d->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
|
||||
D[i] = d->efc_D[i];
|
||||
} else {
|
||||
D[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int* JT_rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_rowsuper = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_colind = mj_stackAllocInt(d, d->nJ);
|
||||
mjtNum* JT = mj_stackAllocNum(d, d->nJ);
|
||||
mju_transposeSparse(JT, d->efc_J, d->nefc, m->nv,
|
||||
JT_rownnz, JT_rowadr, JT_colind, JT_rowsuper,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
|
||||
// compute H = J'*D*J, uncompressed layout
|
||||
mju_sqrMatTDUncompressedInit(rowadr, m->nv);
|
||||
mju_sqrMatTDSparse(H, d->efc_J, JT, D, d->nefc, m->nv,
|
||||
rownnz, rowadr, colind,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr,
|
||||
d->efc_J_colind, d->efc_J_rowsuper,
|
||||
JT_rownnz, JT_rowadr,
|
||||
JT_colind, JT_rowsuper, d,
|
||||
diagind);
|
||||
|
||||
// compute H = M + J'*D*J
|
||||
mj_addM(m, d, H, rownnz, rowadr, colind);
|
||||
|
||||
// copy to persistent storage
|
||||
int nH = rowadr[m->nv-1] + m->nv; // uncompressed: rowadr[r] = r*nv
|
||||
cd.H.assign(H, H + nH);
|
||||
cd.rownnz.assign(rownnz, rownnz + m->nv);
|
||||
cd.rowadr.assign(rowadr, rowadr + m->nv);
|
||||
cd.colind.assign(colind, colind + nH);
|
||||
|
||||
mj_freeStack(d);
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
return cd;
|
||||
}();
|
||||
return data;
|
||||
}
|
||||
|
||||
// ---- TransposeSparse data ----
|
||||
struct TransposeData {
|
||||
int nv;
|
||||
int nefc;
|
||||
int nJ;
|
||||
std::vector<mjtNum> efc_J;
|
||||
std::vector<int> efc_J_rownnz, efc_J_rowadr, efc_J_colind;
|
||||
};
|
||||
|
||||
enum class Size { H2_100, H100 };
|
||||
|
||||
template <Size S>
|
||||
const char* ModelPath() {
|
||||
if constexpr (S == Size::H2_100) {
|
||||
return "../test/benchmark/testdata/2humanoid100_chol.xml";
|
||||
} else {
|
||||
return "../test/benchmark/testdata/100_humanoids_chol.xml";
|
||||
}
|
||||
}
|
||||
|
||||
template <Size S>
|
||||
TransposeData& GetTransposeData() {
|
||||
static TransposeData data = [] {
|
||||
TransposeData td;
|
||||
mjModel* m = LoadModelFromPath(ModelPath<S>());
|
||||
m->opt.jacobian = mjJAC_SPARSE;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
while (d->time < 2) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
td.nv = m->nv;
|
||||
td.nefc = d->nefc;
|
||||
td.nJ = d->nJ;
|
||||
td.efc_J.assign(d->efc_J, d->efc_J + d->nJ);
|
||||
td.efc_J_rownnz.assign(d->efc_J_rownnz, d->efc_J_rownnz + d->nefc);
|
||||
td.efc_J_rowadr.assign(d->efc_J_rowadr, d->efc_J_rowadr + d->nefc);
|
||||
td.efc_J_colind.assign(d->efc_J_colind, d->efc_J_colind + d->nJ);
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
return td;
|
||||
}();
|
||||
return data;
|
||||
}
|
||||
|
||||
// ================================ old functions ==============================
|
||||
|
||||
// transpose sparse matrix (uncompressed)
|
||||
void ABSL_ATTRIBUTE_NOINLINE transposeSparse_baseline(
|
||||
mjtNum* res, const mjtNum* mat, int nr, int nc, int* res_rownnz,
|
||||
@@ -208,8 +263,7 @@ int ABSL_ATTRIBUTE_NOINLINE combineSparse_baseline(mjtNum* dst,
|
||||
mjtNum a, mjtNum b,
|
||||
int dst_nnz, int src_nnz,
|
||||
int* dst_ind,
|
||||
const int* src_ind,
|
||||
mjtNum* buf, int* buf_ind) {
|
||||
const int* src_ind) {
|
||||
// check for identical pattern
|
||||
if (compare_baseline(dst_ind, src_ind, dst_nnz)) {
|
||||
// combine mjtNum data directly
|
||||
@@ -225,8 +279,7 @@ int ABSL_ATTRIBUTE_NOINLINE combineSparse_new(mjtNum* dst,
|
||||
mjtNum a, mjtNum b,
|
||||
int dst_nnz, int src_nnz,
|
||||
int* dst_ind,
|
||||
const int* src_ind,
|
||||
mjtNum* buf, int* buf_ind) {
|
||||
const int* src_ind) {
|
||||
// check for identical pattern
|
||||
if (compare_memcmp(dst_ind, src_ind, dst_nnz)) {
|
||||
// combine mjtNum data directly
|
||||
@@ -340,61 +393,31 @@ void ABSL_ATTRIBUTE_NOINLINE mulMatVecSparse_8(mjtNum* res,
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- benchmark ------------------------------------
|
||||
// ----------------------------- benchmark -------------------------------------
|
||||
|
||||
static void BM_MatVecSparse(benchmark::State& state, int unroll) {
|
||||
static mjModel* m = LoadModelFromPath("flex/flag.xml");
|
||||
mjData* d = mj_makeData(m);
|
||||
MatVecData& data = GetMatVecData();
|
||||
std::vector<mjtNum> res(data.nefc);
|
||||
|
||||
// warm-up rollout to get a typical state
|
||||
for (int i=0; i < kNumWarmupSteps; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
// allocate gradient
|
||||
mj_markStack(d);
|
||||
mjtNum *Ma = mj_stackAllocNum(d, m->nv);
|
||||
mjtNum *vec = mj_stackAllocNum(d, m->nv);
|
||||
mjtNum *res = mj_stackAllocNum(d, d->nefc);
|
||||
mjtNum *grad = mj_stackAllocNum(d, m->nv);
|
||||
mjtNum *Mgrad = mj_stackAllocNum(d, m->nv);
|
||||
|
||||
// compute gradient
|
||||
mj_mulM(m, d, Ma, d->qacc);
|
||||
for (int i=0; i < m->nv; i++) {
|
||||
grad[i] = Ma[i] - d->qfrc_smooth[i] - d->qfrc_constraint[i];
|
||||
}
|
||||
|
||||
// compute search direction
|
||||
mj_solveM(m, d, Mgrad, grad, 1);
|
||||
mju_scl(vec, Mgrad, -1, m->nv);
|
||||
|
||||
// save state
|
||||
std::vector<mjtNum> qpos = AsVector(d->qpos, m->nq);
|
||||
std::vector<mjtNum> qvel = AsVector(d->qvel, m->nv);
|
||||
std::vector<mjtNum> act = AsVector(d->act, m->na);
|
||||
std::vector<mjtNum> warmstart = AsVector(d->qacc_warmstart, m->nv);
|
||||
|
||||
// time benchmark
|
||||
for (auto s : state) {
|
||||
if (unroll == 4) {
|
||||
mju_mulMatVecSparse(res, d->efc_J, vec, d->nefc,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr,
|
||||
d->efc_J_colind, d->efc_J_rowsuper);
|
||||
mju_mulMatVecSparse(res.data(), data.efc_J.data(), data.vec.data(),
|
||||
data.nefc, data.efc_J_rownnz.data(),
|
||||
data.efc_J_rowadr.data(), data.efc_J_colind.data(),
|
||||
data.efc_J_rowsuper.data());
|
||||
} else if (unroll == 1) {
|
||||
mulMatVecSparse_1(res, d->efc_J, vec, d->nefc,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr,
|
||||
d->efc_J_colind, d->efc_J_rowsuper);
|
||||
mulMatVecSparse_1(res.data(), data.efc_J.data(), data.vec.data(),
|
||||
data.nefc, data.efc_J_rownnz.data(),
|
||||
data.efc_J_rowadr.data(), data.efc_J_colind.data(),
|
||||
data.efc_J_rowsuper.data());
|
||||
} else if (unroll == 8) {
|
||||
mulMatVecSparse_8(res, d->efc_J, vec, d->nefc,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr,
|
||||
d->efc_J_colind, d->efc_J_rowsuper);
|
||||
mulMatVecSparse_8(res.data(), data.efc_J.data(), data.vec.data(),
|
||||
data.nefc, data.efc_J_rownnz.data(),
|
||||
data.efc_J_rowadr.data(), data.efc_J_colind.data(),
|
||||
data.efc_J_rowsuper.data());
|
||||
}
|
||||
}
|
||||
|
||||
// finalize
|
||||
mj_freeStack(d);
|
||||
mj_deleteData(d);
|
||||
state.SetItemsProcessed(state.iterations());
|
||||
}
|
||||
|
||||
@@ -420,75 +443,30 @@ void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_MatVecSparse_1(
|
||||
BENCHMARK(BM_MatVecSparse_1);
|
||||
|
||||
static void BM_combineSparse(benchmark::State& state, CombineFuncPtr func) {
|
||||
static mjModel* m = LoadModelFromPath("humanoid/humanoid.xml");
|
||||
m->opt.jacobian = mjJAC_SPARSE;
|
||||
CombineData& data = GetCombineData();
|
||||
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// warm-up rollout to get a typical state
|
||||
for (int i=0; i < kNumWarmupSteps; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
// allocate
|
||||
mj_markStack(d);
|
||||
mjtNum* H = mj_stackAllocNum(d, m->nv*m->nv);
|
||||
int* rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* colind = mj_stackAllocInt(d, m->nv*m->nv);
|
||||
int* diagind = mj_stackAllocInt(d, m->nv);
|
||||
|
||||
// compute D corresponding to quad states
|
||||
mjtNum* D = mj_stackAllocNum(d, d->nefc);
|
||||
for (int i = 0; i < d->nefc; i++) {
|
||||
if (d->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
|
||||
D[i] = d->efc_D[i];
|
||||
} else {
|
||||
D[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int* JT_rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_rowsuper = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_colind = mj_stackAllocInt(d, d->nJ);
|
||||
mjtNum* JT = mj_stackAllocNum(d, d->nJ);
|
||||
mju_transposeSparse(JT, d->efc_J, d->nefc, m->nv,
|
||||
JT_rownnz, JT_rowadr, JT_colind, JT_rowsuper,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
|
||||
// compute H = J'*D*J, uncompressed layout
|
||||
mju_sqrMatTDUncompressedInit(rowadr, m->nv);
|
||||
mju_sqrMatTDSparse(H, d->efc_J, JT, D, d->nefc, m->nv,
|
||||
rownnz, rowadr, colind,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr,
|
||||
d->efc_J_colind, d->efc_J_rowsuper,
|
||||
JT_rownnz, JT_rowadr,
|
||||
JT_colind, JT_rowsuper, d,
|
||||
diagind);
|
||||
|
||||
// compute H = M + J'*D*J
|
||||
mj_addM(m, d, H, rownnz, rowadr, colind);
|
||||
// make working copies that get modified each iteration
|
||||
std::vector<mjtNum> H = data.H;
|
||||
std::vector<int> rownnz = data.rownnz;
|
||||
std::vector<int> rowadr = data.rowadr;
|
||||
std::vector<int> colind = data.colind;
|
||||
|
||||
// time benchmark
|
||||
for (auto s : state) {
|
||||
for (int r = m->nv-1; r >= 0; r--) {
|
||||
for (int r = data.nv-1; r >= 0; r--) {
|
||||
for (int i = 0; i < rownnz[r]-1; i++) {
|
||||
int adr = rowadr[r];
|
||||
int c = colind[adr+i];
|
||||
// true arguments should be i+1 and colind+rowadr[r]
|
||||
// but instead we repeat rownnz[c] and colind+rowadr[c]
|
||||
// in order to trigger all if's in combineSparse
|
||||
func(H+rowadr[c], H+rowadr[r], 1, -H[adr+i],
|
||||
func(H.data()+rowadr[c], H.data()+rowadr[r], 1, -H[adr+i],
|
||||
rownnz[c], rownnz[c],
|
||||
colind+rowadr[c], colind+rowadr[c], NULL, NULL);
|
||||
colind.data()+rowadr[c], colind.data()+rowadr[c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finalize
|
||||
mj_freeStack(d);
|
||||
mj_deleteData(d);
|
||||
state.SetItemsProcessed(state.iterations());
|
||||
}
|
||||
|
||||
@@ -512,172 +490,98 @@ enum class Supernode {
|
||||
Inline
|
||||
};
|
||||
|
||||
template <Size S>
|
||||
static void BM_transposeSparse(benchmark::State& state, TransposeFuncPtr func,
|
||||
Supernode super) {
|
||||
static mjModel* m = LoadModelFromPath("humanoid/humanoid100.xml");
|
||||
TransposeData& data = GetTransposeData<S>();
|
||||
|
||||
// force use of sparse matrices
|
||||
m->opt.jacobian = mjJAC_SPARSE;
|
||||
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// warm-up rollout to get a typical state
|
||||
while (d->time < 2) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
mj_markStack(d);
|
||||
|
||||
// need uncompressed layout
|
||||
mjtNum* res = mj_stackAllocNum(d, m->nv * d->nefc);
|
||||
int* res_rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* res_rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* res_rowsuper = mj_stackAllocInt(d, m->nv);
|
||||
int* res_colind = mj_stackAllocInt(d, m->nv * d->nefc);
|
||||
// allocate output buffers (uncompressed layout)
|
||||
std::vector<mjtNum> res(data.nv * data.nefc);
|
||||
std::vector<int> res_rownnz(data.nv);
|
||||
std::vector<int> res_rowadr(data.nv);
|
||||
std::vector<int> res_rowsuper(data.nv);
|
||||
std::vector<int> res_colind(data.nv * data.nefc);
|
||||
|
||||
// time benchmark
|
||||
for (auto s : state) {
|
||||
int* rowsuper = (super == Supernode::Inline) ? res_rowsuper : nullptr;
|
||||
func(res, d->efc_J, d->nefc, m->nv,
|
||||
res_rownnz, res_rowadr, res_colind, rowsuper,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
int* rowsuper =
|
||||
(super == Supernode::Inline) ? res_rowsuper.data() : nullptr;
|
||||
func(res.data(), data.efc_J.data(), data.nefc, data.nv,
|
||||
res_rownnz.data(), res_rowadr.data(), res_colind.data(), rowsuper,
|
||||
data.efc_J_rownnz.data(), data.efc_J_rowadr.data(),
|
||||
data.efc_J_colind.data());
|
||||
if (super == Supernode::PostProcess) {
|
||||
mju_superSparse(m->nv, res_rowsuper,
|
||||
res_rownnz, res_rowadr, res_colind);
|
||||
mju_superSparse(data.nv, res_rowsuper.data(),
|
||||
res_rownnz.data(), res_rowadr.data(), res_colind.data());
|
||||
}
|
||||
}
|
||||
|
||||
mj_freeStack(d);
|
||||
mj_deleteData(d);
|
||||
state.SetItemsProcessed(state.iterations());
|
||||
}
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_transposeSparse_old(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_transposeSparse(state, &transposeSparse_baseline, Supernode::None);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_old);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_transposeSparse_new(benchmark::State& state) {
|
||||
BM_transposeSparse_2H100_old(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_transposeSparse(state, &mju_transposeSparse, Supernode::None);
|
||||
BM_transposeSparse<Size::H2_100>(state, &transposeSparse_baseline,
|
||||
Supernode::None);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_new);
|
||||
BENCHMARK(BM_transposeSparse_2H100_old);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_transposeSparse_superpost(benchmark::State& state) {
|
||||
BM_transposeSparse_2H100_new(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_transposeSparse(state, &mju_transposeSparse, Supernode::PostProcess);
|
||||
BM_transposeSparse<Size::H2_100>(state, &mju_transposeSparse,
|
||||
Supernode::None);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_superpost);
|
||||
BENCHMARK(BM_transposeSparse_2H100_new);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_transposeSparse_superinline(benchmark::State& state) {
|
||||
BM_transposeSparse_2H100_superpost(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_transposeSparse(state, &mju_transposeSparse, Supernode::Inline);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_superinline);
|
||||
|
||||
static void BM_sqrMatTDSparse(benchmark::State& state, SqrMatTDFuncPtr func) {
|
||||
static mjModel* m =
|
||||
LoadModelFromPath("../test/benchmark/testdata/2humanoid100.xml");
|
||||
|
||||
// force use of sparse matrices, Newton solver, no islands
|
||||
m->opt.jacobian = mjJAC_SPARSE;
|
||||
m->opt.solver = mjSOL_NEWTON;
|
||||
m->opt.disableflags |= mjDSBL_ISLAND;
|
||||
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// warm-up rollout to get a typical state
|
||||
while (d->time < 2) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
// allocate
|
||||
mj_markStack(d);
|
||||
mjtNum* H = mj_stackAllocNum(d, m->nv * m->nv);
|
||||
int* rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* colind = mj_stackAllocInt(d, m->nv * m->nv);
|
||||
int* diagind = mj_stackAllocInt(d, m->nv);
|
||||
|
||||
// compute D corresponding to quad states
|
||||
mjtNum* D = mj_stackAllocNum(d, d->nefc);
|
||||
for (int i = 0; i < d->nefc; i++) {
|
||||
if (d->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
|
||||
D[i] = d->efc_D[i];
|
||||
} else {
|
||||
D[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int* JT_rownnz = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_rowadr = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_rowsuper = mj_stackAllocInt(d, m->nv);
|
||||
int* JT_colind = mj_stackAllocInt(d, d->nJ);
|
||||
mjtNum* JT = mj_stackAllocNum(d, d->nJ);
|
||||
mju_transposeSparse(JT, d->efc_J, d->nefc, m->nv,
|
||||
JT_rownnz, JT_rowadr, JT_colind, JT_rowsuper,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
|
||||
// time benchmark
|
||||
if (func) {
|
||||
mju_sqrMatTDSparseCount(rownnz, rowadr, m->nv,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind,
|
||||
JT_rownnz, JT_rowadr,
|
||||
JT_colind, nullptr, d, 1);
|
||||
|
||||
for (auto s : state) {
|
||||
// compute H = J'*D*J, compressed layout
|
||||
func(H, d->efc_J, JT, D, d->nefc, m->nv, rownnz, rowadr, colind,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, NULL,
|
||||
JT_rownnz, JT_rowadr, JT_colind,
|
||||
JT_rowsuper, d, diagind);
|
||||
}
|
||||
} else {
|
||||
for (auto s : state) {
|
||||
// baseline depends on efc_J_rowsuper
|
||||
mju_superSparse(d->nefc, d->efc_J_rowsuper,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
|
||||
// compute H = J'*D*J, uncompressed layout
|
||||
mju_sqrMatTDSparse_baseline(
|
||||
H, d->efc_J, JT, D, d->nefc, m->nv, rownnz, rowadr, colind,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, d->efc_J_rowsuper,
|
||||
JT_rownnz, JT_rowadr, JT_colind,
|
||||
JT_rowsuper, d, /*unused=*/nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// finalize
|
||||
mj_freeStack(d);
|
||||
mj_deleteData(d);
|
||||
state.SetItemsProcessed(state.iterations());
|
||||
BM_transposeSparse<Size::H2_100>(state, &mju_transposeSparse,
|
||||
Supernode::PostProcess);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_2H100_superpost);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_sqrMatTDSparse_col(benchmark::State& state) {
|
||||
BM_transposeSparse_2H100_superinline(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTDSparse(state, &mju_sqrMatTDSparse);
|
||||
BM_transposeSparse<Size::H2_100>(state, &mju_transposeSparse,
|
||||
Supernode::Inline);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTDSparse_col);
|
||||
BENCHMARK(BM_transposeSparse_2H100_superinline);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_sqrMatTDSparse_row(benchmark::State& state) {
|
||||
BM_transposeSparse_100H_old(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTDSparse(state, &mju_sqrMatTDSparse_row);
|
||||
BM_transposeSparse<Size::H100>(state, &transposeSparse_baseline,
|
||||
Supernode::None);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTDSparse_row);
|
||||
BENCHMARK(BM_transposeSparse_100H_old);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_sqrMatTDSparse_uncompressed(benchmark::State& state) {
|
||||
BM_transposeSparse_100H_new(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTDSparse(state, nullptr);
|
||||
BM_transposeSparse<Size::H100>(state, &mju_transposeSparse, Supernode::None);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTDSparse_uncompressed);
|
||||
BENCHMARK(BM_transposeSparse_100H_new);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_transposeSparse_100H_superpost(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_transposeSparse<Size::H100>(state, &mju_transposeSparse,
|
||||
Supernode::PostProcess);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_100H_superpost);
|
||||
|
||||
void ABSL_ATTRIBUTE_NO_TAIL_CALL
|
||||
BM_transposeSparse_100H_superinline(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_transposeSparse<Size::H100>(state, &mju_transposeSparse,
|
||||
Supernode::Inline);
|
||||
}
|
||||
BENCHMARK(BM_transposeSparse_100H_superinline);
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
// 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.
|
||||
|
||||
// Benchmarks for sparse matrix operations.
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "benchmark/benchmark.h"
|
||||
#include <absl/base/attributes.h>
|
||||
#include <mujoco/mjdata.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "src/engine/engine_util_sparse.h"
|
||||
#include "test/fixture.h"
|
||||
|
||||
namespace mujoco {
|
||||
namespace {
|
||||
|
||||
// ================================ Test Data ==================================
|
||||
// Stores pre-computed sparse matrix inputs extracted from MuJoCo simulations.
|
||||
// Each benchmark computes its own outputs (H, L, etc.) from these inputs.
|
||||
|
||||
struct SparseTestData {
|
||||
// Dimensions
|
||||
int nv; // number of DoFs
|
||||
int nefc; // number of constraint rows
|
||||
int nJ; // nnz in J
|
||||
|
||||
// J (Jacobian) - nefc x nv sparse
|
||||
std::vector<mjtNum> J;
|
||||
std::vector<int> J_rownnz, J_rowadr, J_colind, J_rowsuper;
|
||||
|
||||
// J' (transpose)
|
||||
std::vector<mjtNum> JT;
|
||||
std::vector<int> JT_rownnz, JT_rowadr, JT_colind, JT_rowsuper;
|
||||
|
||||
// D (diagonal weights for constraints)
|
||||
std::vector<mjtNum> D;
|
||||
|
||||
// M structure (mass matrix, lower triangle)
|
||||
std::vector<int> M_rownnz, M_rowadr, M_colind;
|
||||
|
||||
void Setup(const mjModel* m, mjData* d) {
|
||||
// initialize simulation state
|
||||
mj_resetDataKeyframe(m, d, 0);
|
||||
mj_step(m, d);
|
||||
mj_forward(m, d);
|
||||
nv = m->nv;
|
||||
nefc = d->nefc;
|
||||
nJ = d->nJ;
|
||||
|
||||
// copy J
|
||||
J.assign(d->efc_J, d->efc_J + nJ);
|
||||
J_rownnz.assign(d->efc_J_rownnz, d->efc_J_rownnz + nefc);
|
||||
J_rowadr.assign(d->efc_J_rowadr, d->efc_J_rowadr + nefc);
|
||||
J_colind.assign(d->efc_J_colind, d->efc_J_colind + nJ);
|
||||
J_rowsuper.assign(d->efc_J_rowsuper, d->efc_J_rowsuper + nefc);
|
||||
|
||||
// transpose J
|
||||
JT.assign(nJ, 0);
|
||||
JT_rownnz.assign(nv, 0);
|
||||
JT_rowadr.assign(nv, 0);
|
||||
JT_colind.assign(nJ, 0);
|
||||
JT_rowsuper.assign(nv, 0);
|
||||
mju_transposeSparse(JT.data(), J.data(), nefc, nv, JT_rownnz.data(),
|
||||
JT_rowadr.data(), JT_colind.data(), JT_rowsuper.data(),
|
||||
J_rownnz.data(), J_rowadr.data(), J_colind.data());
|
||||
|
||||
// compute D corresponding to quadratic constraint states
|
||||
D.resize(nefc);
|
||||
for (int i = 0; i < nefc; i++) {
|
||||
if (d->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
|
||||
D[i] = d->efc_D[i];
|
||||
} else {
|
||||
D[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// copy M structure
|
||||
M_rownnz.assign(m->M_rownnz, m->M_rownnz + nv);
|
||||
M_rowadr.assign(m->M_rowadr, m->M_rowadr + nv);
|
||||
int nM = M_rowadr[nv - 1] + M_rownnz[nv - 1];
|
||||
M_colind.assign(m->M_colind, m->M_colind + nM);
|
||||
}
|
||||
};
|
||||
|
||||
// ================================ Model Sizes ================================
|
||||
|
||||
enum class Size { H2_100, H100 };
|
||||
|
||||
template <Size S>
|
||||
const char* ModelPath() {
|
||||
if constexpr (S == Size::H2_100) {
|
||||
return "../test/benchmark/testdata/2humanoid100_chol.xml";
|
||||
} else {
|
||||
return "../test/benchmark/testdata/100_humanoids_chol.xml";
|
||||
}
|
||||
}
|
||||
|
||||
template <Size S>
|
||||
mjModel* GetModel() {
|
||||
static mjModel* m = LoadModelFromPath(ModelPath<S>());
|
||||
m->opt.jacobian = mjJAC_SPARSE;
|
||||
m->opt.solver = mjSOL_NEWTON;
|
||||
m->opt.disableflags |= mjDSBL_ISLAND;
|
||||
return m;
|
||||
}
|
||||
|
||||
template <Size S>
|
||||
SparseTestData& GetData() {
|
||||
static SparseTestData data;
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
data.Setup(m, d);
|
||||
mj_deleteData(d);
|
||||
initialized = true;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// ========================== Baseline Implementations =========================
|
||||
|
||||
|
||||
|
||||
// Baseline sqrMatTD (uncompressed layout, from old implementation)
|
||||
void ABSL_ATTRIBUTE_NOINLINE mju_sqrMatTDSparse_baseline(
|
||||
mjtNum* res, const mjtNum* mat, const mjtNum* matT, const mjtNum* diag,
|
||||
int nr, int nc, int* res_rownnz, int* res_rowadr, int* res_colind,
|
||||
const int* rownnz, const int* rowadr, const int* colind,
|
||||
const int* rowsuper, const int* rownnzT, const int* rowadrT,
|
||||
const int* colindT, const int* rowsuperT, mjData* d) {
|
||||
mj_markStack(d);
|
||||
int* chain = mj_stackAllocInt(d, 2 * nc);
|
||||
mjtNum* buffer = mj_stackAllocNum(d, nc);
|
||||
|
||||
for (int r = 0; r < nc; r++) {
|
||||
res_rowadr[r] = r * nc;
|
||||
}
|
||||
|
||||
for (int r = 0; r < nc; r++) {
|
||||
if (rowsuperT && r > 0 && rowsuperT[r - 1] > 0) {
|
||||
res_rownnz[r] = res_rownnz[r - 1];
|
||||
memcpy(res_colind + res_rowadr[r], res_colind + res_rowadr[r - 1],
|
||||
res_rownnz[r] * sizeof(int));
|
||||
if (rownnzT[r]) {
|
||||
res_colind[res_rowadr[r] + res_rownnz[r]] = r;
|
||||
res_rownnz[r]++;
|
||||
}
|
||||
} else {
|
||||
int nchain = 0;
|
||||
int inew = 0, iold = nc;
|
||||
int lastadded = -1;
|
||||
for (int i = 0; i < rownnzT[r]; i++) {
|
||||
int c = colindT[rowadrT[r] + i];
|
||||
if (rowsuper && lastadded >= 0 &&
|
||||
(c - lastadded) <= rowsuper[lastadded]) {
|
||||
continue;
|
||||
} else {
|
||||
lastadded = c;
|
||||
}
|
||||
|
||||
int adr = inew;
|
||||
inew = iold;
|
||||
iold = adr;
|
||||
|
||||
int nnewchain = 0;
|
||||
adr = 0;
|
||||
int end = rowadr[c] + rownnz[c];
|
||||
for (int adr1 = rowadr[c]; adr1 < end; adr1++) {
|
||||
int col_mat = colind[adr1];
|
||||
while (adr < nchain && chain[iold + adr] < col_mat &&
|
||||
chain[iold + adr] <= r) {
|
||||
chain[inew + nnewchain++] = chain[iold + adr++];
|
||||
}
|
||||
if (col_mat > r) {
|
||||
break;
|
||||
}
|
||||
if (adr < nchain && chain[iold + adr] == col_mat) {
|
||||
adr++;
|
||||
}
|
||||
chain[inew + nnewchain++] = col_mat;
|
||||
}
|
||||
|
||||
while (adr < nchain && chain[iold + adr] <= r) {
|
||||
chain[inew + nnewchain++] = chain[iold + adr++];
|
||||
}
|
||||
nchain = nnewchain;
|
||||
}
|
||||
res_rownnz[r] = nchain;
|
||||
if (nchain) {
|
||||
memcpy(res_colind + res_rowadr[r], chain + inew, nchain * sizeof(int));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int r = 0; r < nc; r++) {
|
||||
int adr = res_rowadr[r];
|
||||
for (int i = 0; i < res_rownnz[r]; i++) {
|
||||
buffer[res_colind[adr + i]] = 0;
|
||||
}
|
||||
for (int i = 0; i < rownnzT[r]; i++) {
|
||||
int c = colindT[rowadrT[r] + i];
|
||||
mjtNum matTrc = matT[rowadrT[r] + i];
|
||||
if (diag) {
|
||||
matTrc *= diag[c];
|
||||
}
|
||||
|
||||
int end = rowadr[c] + rownnz[c];
|
||||
for (int adr2 = rowadr[c]; adr2 < end; adr2++) {
|
||||
int adr1;
|
||||
if ((adr1 = colind[adr2]) > r) {
|
||||
break;
|
||||
}
|
||||
buffer[adr1] += matTrc * mat[adr2];
|
||||
}
|
||||
}
|
||||
adr = res_rowadr[r];
|
||||
for (int i = 0; i < res_rownnz[r]; i++) {
|
||||
res[adr + i] = buffer[res_colind[adr + i]];
|
||||
}
|
||||
}
|
||||
for (int r = 1; r < nc; r++) {
|
||||
int end = res_rowadr[r] + res_rownnz[r] - 1;
|
||||
for (int adr = res_rowadr[r]; adr < end; adr++) {
|
||||
int adr1 = res_rowadr[res_colind[adr]] + res_rownnz[res_colind[adr]]++;
|
||||
res[adr1] = res[adr];
|
||||
res_colind[adr1] = r;
|
||||
}
|
||||
}
|
||||
|
||||
mj_freeStack(d);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========================== SqrMatTD Benchmarks ==============================
|
||||
|
||||
enum class SqrMatTDVariant {
|
||||
kBaseline,
|
||||
kRow,
|
||||
kCol,
|
||||
kSplitCol
|
||||
};
|
||||
|
||||
template <Size S>
|
||||
static void BM_sqrMatTD_impl(benchmark::State& state, SqrMatTDVariant variant) {
|
||||
SparseTestData& data = GetData<S>();
|
||||
mjModel* m = GetModel<S>();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
int nv = data.nv;
|
||||
|
||||
// nothing to benchmark if no constraints
|
||||
if (data.nefc == 0) {
|
||||
for (auto s : state) {}
|
||||
mj_deleteData(d);
|
||||
return;
|
||||
}
|
||||
|
||||
// allocate H output (uncompressed for baseline, compressed for others)
|
||||
int max_nnz = (variant == SqrMatTDVariant::kBaseline) ? nv * nv : 0;
|
||||
std::vector<mjtNum> H;
|
||||
std::vector<int> H_rownnz(nv);
|
||||
std::vector<int> H_rowadr(nv);
|
||||
std::vector<int> H_colind;
|
||||
std::vector<int> diagind(nv);
|
||||
|
||||
if (variant == SqrMatTDVariant::kBaseline) {
|
||||
H.resize(max_nnz);
|
||||
H_colind.resize(max_nnz);
|
||||
} else if (variant == SqrMatTDVariant::kSplitCol ||
|
||||
variant == SqrMatTDVariant::kCol) {
|
||||
// use symbolic to count nnz
|
||||
int nH = mju_sqrMatTDSparseSymbolic(
|
||||
H_rownnz.data(), H_rowadr.data(), nullptr, nullptr,
|
||||
data.nefc, nv, data.J_rownnz.data(), data.J_rowadr.data(),
|
||||
data.J_colind.data(), data.JT_rownnz.data(), data.JT_rowadr.data(),
|
||||
data.JT_colind.data(), data.JT_rowsuper.data(), d);
|
||||
H.resize(nH);
|
||||
H_colind.resize(nH);
|
||||
} else {
|
||||
// row: use Count (lower triangle only)
|
||||
mju_sqrMatTDSparseCount(
|
||||
H_rownnz.data(), H_rowadr.data(), nv, data.J_rownnz.data(),
|
||||
data.J_rowadr.data(), data.J_colind.data(), data.JT_rownnz.data(),
|
||||
data.JT_rowadr.data(), data.JT_colind.data(), nullptr, d, 0);
|
||||
int nH = H_rowadr[nv - 1] + H_rownnz[nv - 1];
|
||||
H.resize(nH);
|
||||
H_colind.resize(nH);
|
||||
}
|
||||
|
||||
for (auto s : state) {
|
||||
switch (variant) {
|
||||
case SqrMatTDVariant::kBaseline:
|
||||
mju_superSparse(data.nefc, data.J_rowsuper.data(), data.J_rownnz.data(),
|
||||
data.J_rowadr.data(), data.J_colind.data());
|
||||
mju_sqrMatTDSparse_baseline(
|
||||
H.data(), data.J.data(), data.JT.data(), data.D.data(), data.nefc,
|
||||
nv, H_rownnz.data(), H_rowadr.data(), H_colind.data(),
|
||||
data.J_rownnz.data(), data.J_rowadr.data(), data.J_colind.data(),
|
||||
data.J_rowsuper.data(), data.JT_rownnz.data(),
|
||||
data.JT_rowadr.data(), data.JT_colind.data(),
|
||||
data.JT_rowsuper.data(), d);
|
||||
break;
|
||||
case SqrMatTDVariant::kRow:
|
||||
mju_sqrMatTDSparseCount(
|
||||
H_rownnz.data(), H_rowadr.data(), nv, data.J_rownnz.data(),
|
||||
data.J_rowadr.data(), data.J_colind.data(), data.JT_rownnz.data(),
|
||||
data.JT_rowadr.data(), data.JT_colind.data(), nullptr, d, 0);
|
||||
mju_sqrMatTDSparse_row(
|
||||
H.data(), data.J.data(), data.JT.data(), data.D.data(), data.nefc,
|
||||
nv, H_rownnz.data(), H_rowadr.data(), H_colind.data(),
|
||||
data.J_rownnz.data(), data.J_rowadr.data(), data.J_colind.data(),
|
||||
nullptr, data.JT_rownnz.data(), data.JT_rowadr.data(),
|
||||
data.JT_colind.data(), data.JT_rowsuper.data(), d, nullptr);
|
||||
break;
|
||||
case SqrMatTDVariant::kCol:
|
||||
mju_sqrMatTDSparseCount(
|
||||
H_rownnz.data(), H_rowadr.data(), nv, data.J_rownnz.data(),
|
||||
data.J_rowadr.data(), data.J_colind.data(), data.JT_rownnz.data(),
|
||||
data.JT_rowadr.data(), data.JT_colind.data(), nullptr, d, 0);
|
||||
mju_sqrMatTDSparse(
|
||||
H.data(), data.J.data(), data.JT.data(), data.D.data(), data.nefc,
|
||||
nv, H_rownnz.data(), H_rowadr.data(), H_colind.data(),
|
||||
data.J_rownnz.data(), data.J_rowadr.data(), data.J_colind.data(),
|
||||
nullptr, data.JT_rownnz.data(), data.JT_rowadr.data(),
|
||||
data.JT_colind.data(), data.JT_rowsuper.data(), d, nullptr);
|
||||
break;
|
||||
|
||||
case SqrMatTDVariant::kSplitCol:
|
||||
mju_sqrMatTDSparseSymbolic(
|
||||
H_rownnz.data(), H_rowadr.data(), nullptr, nullptr,
|
||||
data.nefc, nv, data.J_rownnz.data(), data.J_rowadr.data(),
|
||||
data.J_colind.data(), data.JT_rownnz.data(), data.JT_rowadr.data(),
|
||||
data.JT_colind.data(), data.JT_rowsuper.data(), d);
|
||||
mju_sqrMatTDSparseSymbolic(
|
||||
H_rownnz.data(), H_rowadr.data(), H_colind.data(), nullptr,
|
||||
data.nefc, nv, data.J_rownnz.data(), data.J_rowadr.data(),
|
||||
data.J_colind.data(), data.JT_rownnz.data(), data.JT_rowadr.data(),
|
||||
data.JT_colind.data(), data.JT_rowsuper.data(), d);
|
||||
mju_sqrMatTDSparseNumeric(
|
||||
H.data(), nv, H_rownnz.data(), H_rowadr.data(),
|
||||
H_colind.data(), nullptr, data.J.data(), data.J_rownnz.data(),
|
||||
data.J_rowadr.data(), data.J_colind.data(), data.JT.data(),
|
||||
data.JT_rownnz.data(), data.JT_rowadr.data(), data.JT_colind.data(),
|
||||
data.JT_rowsuper.data(), data.D.data(), d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
state.SetItemsProcessed(state.iterations());
|
||||
}
|
||||
|
||||
void BM_sqrMatTD_2H100_baseline(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H2_100>(state, SqrMatTDVariant::kBaseline);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_2H100_baseline);
|
||||
|
||||
void BM_sqrMatTD_2H100_row(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H2_100>(state, SqrMatTDVariant::kRow);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_2H100_row);
|
||||
|
||||
void BM_sqrMatTD_2H100_col(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H2_100>(state, SqrMatTDVariant::kCol);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_2H100_col);
|
||||
|
||||
void BM_sqrMatTD_2H100_splitCol(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H2_100>(state, SqrMatTDVariant::kSplitCol);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_2H100_splitCol);
|
||||
|
||||
void BM_sqrMatTD_100H_baseline(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H100>(state, SqrMatTDVariant::kBaseline);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_100H_baseline);
|
||||
|
||||
void BM_sqrMatTD_100H_row(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H100>(state, SqrMatTDVariant::kRow);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_100H_row);
|
||||
|
||||
void BM_sqrMatTD_100H_col(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H100>(state, SqrMatTDVariant::kCol);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_100H_col);
|
||||
|
||||
void BM_sqrMatTD_100H_splitCol(benchmark::State& state) {
|
||||
MujocoErrorTestGuard guard;
|
||||
BM_sqrMatTD_impl<Size::H100>(state, SqrMatTDVariant::kSplitCol);
|
||||
}
|
||||
BENCHMARK(BM_sqrMatTD_100H_splitCol);
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
benchmark::Initialize(&argc, argv);
|
||||
benchmark::RunSpecifiedBenchmarks();
|
||||
return 0;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ TEST_F(MjcConvexTest, CylinderBox) {
|
||||
EXPECT_EQ(data->ncon, 5);
|
||||
|
||||
// with multiCCD disabled, should find 1 contact
|
||||
model->opt.enableflags &= ~mjENBL_MULTICCD;
|
||||
model->opt.disableflags |= mjDSBL_MULTICCD;
|
||||
mj_forward(model, data);
|
||||
EXPECT_EQ(data->ncon, 1);
|
||||
|
||||
|
||||
@@ -390,5 +390,73 @@ TEST_F(MjCollisionTest, MarginSumming) {
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(MjCollisionTest, MaxContact) {
|
||||
constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option>
|
||||
<flag multiccd="enable"/>
|
||||
</option>
|
||||
<asset>
|
||||
<mesh name="smallbox"
|
||||
vertex="-1 -1 -1 1 -1 -1 1 1 -1
|
||||
1 1 1 1 -1 1 -1 1 -1
|
||||
-1 1 1 -1 -1 1"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom name="mesh" type="mesh" mesh="smallbox"/>
|
||||
<geom name="box" type="box" size="1 1 1"/>
|
||||
<geom name="plane" type="plane" size="1 1 1"/>
|
||||
<geom name="sphere" type="sphere" size="1"/>
|
||||
<geom name="capsule" type="capsule" size="1 1"/>
|
||||
<geom name="ellipsoid" type="ellipsoid" size="1 1 1"/>
|
||||
<geom name="cylinder" type="cylinder" size="1 1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
char error[1024];
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
ASSERT_THAT(d, NotNull());
|
||||
|
||||
int mesh = mj_name2id(m, mjOBJ_GEOM, "mesh");
|
||||
int box = mj_name2id(m, mjOBJ_GEOM, "box");
|
||||
int plane = mj_name2id(m, mjOBJ_GEOM, "plane");
|
||||
int sphere = mj_name2id(m, mjOBJ_GEOM, "sphere");
|
||||
int capsule = mj_name2id(m, mjOBJ_GEOM, "capsule");
|
||||
int ellipsoid = mj_name2id(m, mjOBJ_GEOM, "ellipsoid");
|
||||
int cylinder = mj_name2id(m, mjOBJ_GEOM, "cylinder");
|
||||
|
||||
EXPECT_EQ(mj_maxContact(m, mesh, box, -1), 4);
|
||||
EXPECT_EQ(mj_maxContact(m, mesh, plane, -1), 3);
|
||||
EXPECT_EQ(mj_maxContact(m, box, plane, -1), 4);
|
||||
EXPECT_EQ(mj_maxContact(m, mesh, mesh, -1), 4);
|
||||
EXPECT_EQ(mj_maxContact(m, box, box, -1), 8);
|
||||
EXPECT_EQ(mj_maxContact(m, capsule, capsule, -1), 2);
|
||||
EXPECT_EQ(mj_maxContact(m, capsule, box, -1), 4);
|
||||
EXPECT_EQ(mj_maxContact(m, capsule, plane, -1), 2);
|
||||
EXPECT_EQ(mj_maxContact(m, cylinder, plane, -1), 4);
|
||||
EXPECT_EQ(mj_maxContact(m, sphere, sphere, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, sphere, capsule, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, sphere, box, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, sphere, mesh, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, sphere, plane, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, sphere, cylinder, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, ellipsoid, ellipsoid, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, ellipsoid, box, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, ellipsoid, mesh, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, ellipsoid, plane, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, ellipsoid, cylinder, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, ellipsoid, capsule, -1), 1);
|
||||
EXPECT_EQ(mj_maxContact(m, capsule, cylinder, -1), 5);
|
||||
EXPECT_EQ(mj_maxContact(m, capsule, mesh, -1), 5);
|
||||
EXPECT_EQ(mj_maxContact(m, cylinder, cylinder, -1), 5);
|
||||
EXPECT_EQ(mj_maxContact(m, cylinder, box, -1), 5);
|
||||
EXPECT_EQ(mj_maxContact(m, cylinder, mesh, -1), 5);
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -61,14 +61,6 @@ constexpr char kEllipsoidXml[] = R"(
|
||||
</keyframe>
|
||||
</mujoco>)";
|
||||
|
||||
void* CCDAllocate(void* data, std::size_t nbytes) {
|
||||
return new std::byte[nbytes];
|
||||
}
|
||||
|
||||
void CCDFree(void* data, void* buffer) {
|
||||
delete [] (std::byte*)buffer;
|
||||
}
|
||||
|
||||
mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3],
|
||||
mjtNum x2[3], mjtNum cutoff = mjMAX_LIMIT) {
|
||||
mjCCDConfig config;
|
||||
@@ -79,6 +71,7 @@ mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3],
|
||||
config.tolerance = kTolerance,
|
||||
config.max_contacts = 0; // no geom contacts needed
|
||||
config.dist_cutoff = cutoff;
|
||||
config.buffer = nullptr;
|
||||
|
||||
mjCCDObj obj1, obj2;
|
||||
mjc_initCCDObj(&obj1, m, d, g1, 0);
|
||||
@@ -129,14 +122,13 @@ int Penetration(mjCCDStatus& status, mjtNum& depth, std::vector<mjtNum>& dir,
|
||||
mjCCDConfig config;
|
||||
|
||||
// set config
|
||||
auto buffer = std::vector<std::byte>(mjc_ccdSize(kMaxIterations));
|
||||
config.max_iterations = kMaxIterations;
|
||||
config.tolerance = kTolerance;
|
||||
config.max_contacts = max_contacts;
|
||||
config.dist_cutoff = 0; // no geom distances needed
|
||||
config.max_contacts = max_contacts;
|
||||
config.context = nullptr;
|
||||
config.alloc = CCDAllocate;
|
||||
config.free = CCDFree;
|
||||
config.buffer = buffer.data();
|
||||
|
||||
mjtNum dist = mjc_ccd(&config, &status, &obj1, &obj2);
|
||||
if (dist < 0) {
|
||||
@@ -2006,7 +1998,7 @@ TEST_F(MjGjkTest, CylinderBoxMargin) {
|
||||
<mujoco>
|
||||
<statistic meansize="0.15"/>
|
||||
<option>
|
||||
<flag gravity="disable"/>
|
||||
<flag gravity="disable" multiccd="disable"/>
|
||||
</option>
|
||||
|
||||
<worldbody>
|
||||
@@ -2027,8 +2019,10 @@ TEST_F(MjGjkTest, CylinderBoxMargin) {
|
||||
mjData* data = mj_makeData(model);
|
||||
mj_forward(model, data);
|
||||
|
||||
// margin=0.1 means forces generated when dist<0.1
|
||||
// the contact at dist~0.015 is within margin, so forces are generated
|
||||
EXPECT_EQ(data->ncon, 1);
|
||||
EXPECT_LT(data->contact[0].efc_address, 0);
|
||||
EXPECT_GE(data->contact[0].efc_address, 0);
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
// Tests for engine/engine_core_constraint.c.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -35,136 +33,9 @@ namespace {
|
||||
|
||||
using ::testing::NotNull;
|
||||
using ::testing::Pointwise;
|
||||
|
||||
using CoreConstraintTest = MujocoTest;
|
||||
|
||||
// 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) {
|
||||
#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];
|
||||
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model, testing::NotNull()) << error;
|
||||
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, mj_isSparse(model),
|
||||
/*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)));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// test formulas for penetration at rest
|
||||
TEST_F(CoreConstraintTest, RestPenetration) {
|
||||
constexpr char xml[] = R"(
|
||||
@@ -749,6 +620,191 @@ TEST_F(CoreConstraintTest, StrainConstraintNoPinning) {
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// Test flex strain constraint with quadratic interpolation
|
||||
TEST_F(CoreConstraintTest, StrainConstraintQuadratic) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" jacobian="dense"/>
|
||||
<worldbody>
|
||||
<body name="parent">
|
||||
<joint type="free"/>
|
||||
<geom type="box" size=".01 .01 .01" mass=".1"/>
|
||||
<flexcomp name="test" type="box"
|
||||
spacing=".1 .1 .1" radius="0.001"
|
||||
pos="0 0 .5" dof="quadratic" mass="1" dim="3">
|
||||
<contact selfcollide="none"/>
|
||||
<edge equality="strain"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
mj_resetData(m, d);
|
||||
mj_forward(m, d);
|
||||
|
||||
// Check constraints generated
|
||||
EXPECT_GT(d->ne, 0) << "Expected strain constraints";
|
||||
|
||||
// Check that initial strain is ~0
|
||||
mjtNum max_pos = 0;
|
||||
for (int i = 0; i < d->ne; i++) {
|
||||
if (mju_abs(d->efc_pos[i]) > max_pos) {
|
||||
max_pos = mju_abs(d->efc_pos[i]);
|
||||
}
|
||||
}
|
||||
EXPECT_LT(max_pos, 1e-6) << "Initial strain should be ~0";
|
||||
|
||||
// Check Jacobian for NaN
|
||||
int nv = m->nv;
|
||||
bool has_bad_jacobian = false;
|
||||
for (int i = 0; i < d->ne; i++) {
|
||||
for (int j = 0; j < nv; j++) {
|
||||
if (mju_isBad(d->efc_J[i*nv + j])) {
|
||||
has_bad_jacobian = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(has_bad_jacobian) << "Jacobian has NaN";
|
||||
|
||||
// Run simulation for a few steps
|
||||
for (int i = 0; i < 100; i++) {
|
||||
mj_step(m, d);
|
||||
ASSERT_FALSE(mju_isBad(d->qpos[0]))
|
||||
<< "Simulation unstable at step " << i;
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(CoreConstraintTest, ShellModeBendZeroForceAtRest) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option gravity="0 0 0"/>
|
||||
<worldbody>
|
||||
<flexcomp type="grid" count="8 8 8" spacing=".07 .07 .07" pos="0 0 1"
|
||||
dim="3" cellcount="1 1 1" radius=".001" rgba="0 .7 .7 1"
|
||||
mass="5" name="softbody" dof="trilinear">
|
||||
<elasticity young="0" poisson="0.1" damping="0.01"
|
||||
elastic2d="bend" thickness="0.02"/>
|
||||
<edge equality="strain"/>
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024] = {0};
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, testing::NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
mj_forward(m, d);
|
||||
|
||||
// Check number of equalities
|
||||
EXPECT_EQ(m->neq, 6);
|
||||
|
||||
// Check total number of scalar equality constraints
|
||||
// 6 faces * 6 physical modes per face = 36
|
||||
// (2 spurious rigid-rotation modes from transverse shear are projected out)
|
||||
EXPECT_EQ(d->ne, 36);
|
||||
|
||||
// all constraint residuals should be zero at rest
|
||||
for (int i = 0; i < d->ne; i++) {
|
||||
EXPECT_NEAR(d->efc_pos[i], 0, 1e-10)
|
||||
<< "nonzero constraint residual at " << i;
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// Test quadratic passive forces (no constraints) for stability
|
||||
TEST_F(CoreConstraintTest, QuadraticPassiveForceStability) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" solver="CG" tolerance="1e-6"/>
|
||||
<worldbody>
|
||||
<geom type="plane" size="10 10 1"/>
|
||||
<flexcomp name="test" type="grid" count="3 3 3"
|
||||
spacing=".05 .05 .05" radius="0.001"
|
||||
pos="0 0 .3" dof="quadratic" mass="1" dim="3">
|
||||
<contact selfcollide="none"/>
|
||||
<elasticity young="1e4" damping="0.01"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// Run for 500 steps — should stay stable
|
||||
for (int i = 0; i < 500; i++) {
|
||||
mj_step(m, d);
|
||||
ASSERT_FALSE(mju_isBad(d->qpos[0]))
|
||||
<< "Passive quadratic unstable at step " << i;
|
||||
for (int j = 0; j < m->nv; j++) {
|
||||
ASSERT_LT(mju_abs(d->qvel[j]), 1000.0)
|
||||
<< "Velocity exploded at step " << i;
|
||||
}
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// Test quadratic with anisotropic cells (like what mesh bounding box creates)
|
||||
TEST_F(CoreConstraintTest, QuadraticAnisotropicStrain) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" solver="CG" tolerance="1e-6"/>
|
||||
<size memory="50M"/>
|
||||
<worldbody>
|
||||
<geom type="plane" size="10 10 1"/>
|
||||
<body name="parent">
|
||||
<joint type="free"/>
|
||||
<geom type="box" size=".01 .01 .01" mass=".1"/>
|
||||
<flexcomp name="test" type="grid" count="3 3 3"
|
||||
spacing=".1 .05 .08" radius="0.001"
|
||||
pos="0 0 .5" dof="quadratic" mass="1" dim="3">
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
<edge equality="strain" damping="0.01"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
mj_forward(m, d);
|
||||
EXPECT_GT(d->ne, 0) << "Expected strain constraints";
|
||||
|
||||
// Run for 200 steps with gravity + contact
|
||||
for (int i = 0; i < 200; i++) {
|
||||
mj_step(m, d);
|
||||
ASSERT_FALSE(mju_isBad(d->qpos[0]))
|
||||
<< "Anisotropic quadratic unstable at step " << i;
|
||||
for (int j = 0; j < m->nv; j++) {
|
||||
ASSERT_LT(mju_abs(d->qvel[j]), 1000.0)
|
||||
<< "Velocity exploded at step " << i
|
||||
<< ", qvel[" << j << "]=" << d->qvel[j];
|
||||
}
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(CoreConstraintTest, ContactSharedDofJacobian) {
|
||||
constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
@@ -787,6 +843,254 @@ TEST_F(CoreConstraintTest, ContactSharedDofJacobian) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
static const char* const kJdotvConnect2dPath =
|
||||
"engine/testdata/core_constraint/jdotv_connect_2d.xml";
|
||||
static const char* const kJdotvConnect3dPath =
|
||||
"engine/testdata/core_constraint/jdotv_connect_3d.xml";
|
||||
static const char* const kJdotvWeld3dPath =
|
||||
"engine/testdata/core_constraint/jdotv_weld_3d.xml";
|
||||
|
||||
// validate mj_Jdotv against finite-differenced constraint Jacobian
|
||||
TEST_F(CoreConstraintTest, JdotvFiniteDifference) {
|
||||
|
||||
for (const char* path : {kJdotvConnect2dPath,
|
||||
kJdotvConnect3dPath,
|
||||
kJdotvWeld3dPath}) {
|
||||
const std::string xml_path = GetTestDataFilePath(path);
|
||||
char err[1024];
|
||||
mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, err, sizeof(err));
|
||||
ASSERT_THAT(m, NotNull()) << err << " for " << path;
|
||||
int nv = m->nv;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// simulate for 1 second to accumulate velocity
|
||||
while (d->time < 1.0) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
// forward to populate constraints
|
||||
mj_forward(m, d);
|
||||
ASSERT_GT(d->ne, 0) << "no equality constraints for " << path;
|
||||
int ne = d->ne;
|
||||
|
||||
// get dense J_0 (ne x nv)
|
||||
std::vector<mjtNum> J0(ne * nv);
|
||||
if (mj_isSparse(m)) {
|
||||
mju_sparse2dense(J0.data(), d->efc_J, ne, nv,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
} else {
|
||||
mju_copy(J0.data(), d->efc_J, ne * nv);
|
||||
}
|
||||
|
||||
// compute mj_Jdotv at current state
|
||||
std::vector<mjtNum> jdv(ne, 0);
|
||||
mj_Jdotv(m, d, jdv.data());
|
||||
|
||||
// save qpos and qvel
|
||||
std::vector<mjtNum> qpos0(m->nq), qvel0(nv);
|
||||
mju_copy(qpos0.data(), d->qpos, m->nq);
|
||||
mju_copy(qvel0.data(), d->qvel, nv);
|
||||
|
||||
// integrate qpos forward by h using qvel
|
||||
const mjtNum h = MjTol(1e-7, 5e-4);
|
||||
mj_integratePos(m, d->qpos, d->qvel, h);
|
||||
mj_forward(m, d);
|
||||
|
||||
// get dense J_h (ne x nv)
|
||||
ASSERT_EQ(d->ne, ne) << "constraint count changed after integration";
|
||||
std::vector<mjtNum> Jh(ne * nv);
|
||||
if (mj_isSparse(m)) {
|
||||
mju_sparse2dense(Jh.data(), d->efc_J, ne, nv,
|
||||
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
|
||||
} else {
|
||||
mju_copy(Jh.data(), d->efc_J, ne * nv);
|
||||
}
|
||||
|
||||
// FD: Jdotv_fd[i] = -sum_j (Jh[i,j] - J0[i,j]) / h * qvel[j]
|
||||
// (negated because mj_Jdotv subtracts)
|
||||
std::vector<mjtNum> jdv_fd(ne, 0);
|
||||
for (int i = 0; i < ne; i++) {
|
||||
for (int j = 0; j < nv; j++) {
|
||||
jdv_fd[i] -= (Jh[i*nv+j] - J0[i*nv+j]) / h * qvel0[j];
|
||||
}
|
||||
}
|
||||
|
||||
// compare
|
||||
EXPECT_THAT(AsVector(jdv.data(), ne),
|
||||
Pointwise(MjNear(1e-4, 1e-2), AsVector(jdv_fd.data(), ne)))
|
||||
<< "Jdotv FD mismatch for " << path;
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: forward-inverse identity preserved with Jdot*v correction
|
||||
TEST_F(CoreConstraintTest, JdotvFwdInvIdentity) {
|
||||
for (const char* path : {kJdotvConnect2dPath,
|
||||
kJdotvConnect3dPath,
|
||||
kJdotvWeld3dPath}) {
|
||||
const std::string xml_path = GetTestDataFilePath(path);
|
||||
char err[1024];
|
||||
mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, err, sizeof(err));
|
||||
ASSERT_THAT(m, NotNull()) << err;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// give initial velocity
|
||||
for (int i = 0; i < m->nv; i++) d->qvel[i] = 0.5 * (i + 1);
|
||||
|
||||
// forward (with correction ON by default)
|
||||
mj_forward(m, d);
|
||||
mj_compareFwdInv(m, d);
|
||||
mjtNum fwdinv = d->solver_fwdinv[0];
|
||||
|
||||
mjtNum epsilon = MjTol(1e-10, 1e-2);
|
||||
EXPECT_LT(fwdinv, epsilon)
|
||||
<< "fwdinv broken for " << path
|
||||
<< " (fwdinv=" << fwdinv << ")";
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------- strain constraint rotated parent ----------------
|
||||
|
||||
struct StrainConstraintTestCase {
|
||||
std::string test_name;
|
||||
std::string body_pos;
|
||||
std::string body_quat;
|
||||
std::string flex_spacing;
|
||||
std::string flex_xyaxes;
|
||||
};
|
||||
|
||||
class StrainConstraintRotatedTest : public CoreConstraintTest,
|
||||
public ::testing::WithParamInterface<
|
||||
StrainConstraintTestCase> {
|
||||
};
|
||||
|
||||
TEST_P(StrainConstraintRotatedTest, ResidualIsZero) {
|
||||
auto param = GetParam();
|
||||
std::string xml = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" jacobian="dense" gravity="0 0 0"/>
|
||||
<worldbody>
|
||||
<body name="parent" )";
|
||||
|
||||
if (!param.body_pos.empty()) {
|
||||
xml += "pos=\"" + param.body_pos + "\" ";
|
||||
}
|
||||
if (!param.body_quat.empty()) {
|
||||
xml += "quat=\"" + param.body_quat + "\" ";
|
||||
}
|
||||
xml += R"(>
|
||||
<joint type="free"/>
|
||||
<geom type="box" size=".01 .01 .01" mass=".1"/>
|
||||
<flexcomp name="test" type="box" )";
|
||||
|
||||
if (!param.flex_spacing.empty()) {
|
||||
xml += "spacing=\"" + param.flex_spacing + "\" ";
|
||||
}
|
||||
if (!param.flex_xyaxes.empty()) {
|
||||
xml += "xyaxes=\"" + param.flex_xyaxes + "\" ";
|
||||
}
|
||||
|
||||
xml += R"(radius="0.001"
|
||||
pos="0 0 0" dof="trilinear" mass="1" dim="3">
|
||||
<contact selfcollide="none"/>
|
||||
<edge equality="strain"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml.c_str(), error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
mj_forward(m, d);
|
||||
|
||||
// Check we have strain constraints
|
||||
EXPECT_GT(d->ne, 0) << "Expected strain constraints";
|
||||
|
||||
// The critical check: constraint residuals must be ~0 at the initial
|
||||
// (undeformed) configuration, even though the body is rotated.
|
||||
mjtNum max_pos = 0;
|
||||
for (int i = 0; i < d->ne; i++) {
|
||||
max_pos = mju_max(max_pos, mju_abs(d->efc_pos[i]));
|
||||
}
|
||||
EXPECT_LT(max_pos, 1e-6)
|
||||
<< "Strain constraint residual should be ~0"
|
||||
<< " (max_pos=" << max_pos << ")";
|
||||
|
||||
// Verify stability
|
||||
for (int i = 0; i < 200; i++) {
|
||||
mj_step(m, d);
|
||||
ASSERT_FALSE(mju_isBad(d->qpos[0]))
|
||||
<< "Simulation unstable at step " << i;
|
||||
for (int j = 0; j < m->nv; j++) {
|
||||
ASSERT_LT(mju_abs(d->qvel[j]), 1000.0)
|
||||
<< "Velocity exploded at step " << i
|
||||
<< ", qvel[" << j << "]=" << d->qvel[j];
|
||||
}
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
StrainConstraintRotatedTests, StrainConstraintRotatedTest,
|
||||
testing::ValuesIn<StrainConstraintTestCase>({
|
||||
// Test strain constraint with a rotated parent body.
|
||||
// The flexcomp is placed inside a parent body that has a non-identity
|
||||
// initial rotation. This reproduces the "grocery scene" bug where the
|
||||
// stiffness matrix eigenvectors and reference positions were computed
|
||||
// in world frame instead of the unrotated local frame, causing
|
||||
// spurious constraint forces.
|
||||
{
|
||||
"RotatedParent",
|
||||
"1 2 3",
|
||||
"0.707107 0 0.707107 0",
|
||||
".1 .1 .1",
|
||||
""
|
||||
},
|
||||
// Same test with an anisotropic box (different spacing per axis) and
|
||||
// arbitrary rotation (combined 45-deg Y + 30-deg X).
|
||||
{
|
||||
"RotatedParentAnisotropic",
|
||||
"0.5 -1 2",
|
||||
"0.8924 0.2392 0.3696 -0.0990",
|
||||
".15 .08 .05",
|
||||
""
|
||||
},
|
||||
// Test strain constraint with flexcomp-level xyaxes rotation.
|
||||
// This is the "grocery scene" pattern where the flexcomp grid itself is
|
||||
// rotated via xyaxes="0 1 0 0 0 1" (X->Y, Y->Z).
|
||||
{
|
||||
"FlexcompXyaxes",
|
||||
"",
|
||||
"",
|
||||
".1 .02 .1",
|
||||
"0 1 0 0 0 1"
|
||||
},
|
||||
// Test combining parent body rotation with flexcomp xyaxes rotation.
|
||||
// The total rotation is the composition of both.
|
||||
{
|
||||
"RotatedParentPlusXyaxes",
|
||||
"1 2 3",
|
||||
"0.707107 0 0.707107 0",
|
||||
".15 .08 .05",
|
||||
"0 1 0 0 0 1"
|
||||
}
|
||||
}),
|
||||
[](const testing::TestParamInfo<
|
||||
StrainConstraintRotatedTest::ParamType>& info) {
|
||||
return info.param.test_name;
|
||||
}
|
||||
);
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -191,10 +191,8 @@ TEST_F(CoreSmoothTest, TendonJdot) {
|
||||
|
||||
mj_forward(m, d);
|
||||
|
||||
// get current J and Jdot for the tendon
|
||||
// get current J for the tendon
|
||||
vector<mjtNum> ten_J(d->ten_J, d->ten_J + nv);
|
||||
vector<mjtNum> ten_Jdot(nv, 0);
|
||||
mj_tendonDot(m, d, 0, ten_Jdot.data());
|
||||
|
||||
// compute finite-differenced Jdot
|
||||
mjtNum h = MjTol(1e-7, 5e-4);
|
||||
@@ -206,7 +204,10 @@ TEST_F(CoreSmoothTest, TendonJdot) {
|
||||
mju_subFrom(ten_Jh.data(), ten_J.data(), nv);
|
||||
mju_scl(ten_Jh.data(), ten_Jh.data(), 1.0 / h, nv);
|
||||
|
||||
EXPECT_THAT(ten_Jdot, Pointwise(MjNear(1e-6, 2e-3), ten_Jh));
|
||||
// test dot product against finite differences
|
||||
mjtNum dot = mj_tendonDot(m, d, 0, d->qvel);
|
||||
mjtNum expected_dot = mju_dot(ten_Jh.data(), d->qvel, nv);
|
||||
EXPECT_NEAR(dot, expected_dot, MjTol(1e-5, 2e-3));
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
// 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];
|
||||
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
mjData* data = mj_makeData(model);
|
||||
mj_forward(model, data);
|
||||
|
||||
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, data, 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);
|
||||
}
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
|
||||
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];
|
||||
mjModel* model =
|
||||
LoadModelFromString(AngMomTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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
|
||||
for (int i = 0; i < 3; i++) {
|
||||
EXPECT_THAT(angmom_ref[i], MjNear(angmom_test[i], 1e-8, 1e-4));
|
||||
}
|
||||
|
||||
mju_free(angmom_mat);
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// compare subtree angular momentum matrix: analytical and findiff
|
||||
TEST_F(AngMomMatTest, CompareAngMomMats) {
|
||||
char error[1024];
|
||||
mjModel* model =
|
||||
LoadModelFromString(AngMomTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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 constexpr mjtNum eps = MjTol(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, 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
|
||||
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);
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
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];
|
||||
mjModel* model =
|
||||
LoadModelFromString(kJacobianTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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], MjNear(findiff, eps, 1e-2));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
char error[1024];
|
||||
mjModel* model =
|
||||
LoadModelFromString(kJacobianTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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], MjNear(expected, max_abs_err, 1e-4));
|
||||
}
|
||||
}
|
||||
|
||||
mju_free(jac_subtree);
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
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];
|
||||
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
int nv = model->nv;
|
||||
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);
|
||||
|
||||
// get site position
|
||||
int siteid = mj_name2id(model, 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, data, jacp.data(), jacr.data(), point, bodyid);
|
||||
std::vector<mjtNum> jacp_dot(3*nv);
|
||||
std::vector<mjtNum> jacr_dot(3*nv);
|
||||
mj_jacDot(model, data, jacp_dot.data(), jacr_dot.data(), point, bodyid);
|
||||
|
||||
// jac_h: jacobian after integrating qpos with a timestep of h
|
||||
constexpr mjtNum h = MjTol(1e-7, 5e-4);
|
||||
mj_integratePos(model, data->qpos, data->qvel, h);
|
||||
mj_kinematics(model, data);
|
||||
mj_comPos(model, data);
|
||||
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, data, 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));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
}
|
||||
|
||||
// compare mj_jacDotSparse with dense mj_jacDot
|
||||
TEST_F(JacobianTest, JacDotSparse) {
|
||||
for (auto xml : {kHinge, kQuat, kTelescope, kFreeBall, kQuatlessPendulum}) {
|
||||
char error[1024];
|
||||
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
int nv = model->nv;
|
||||
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 and site position
|
||||
int bodyid = mj_name2id(model, mjOBJ_BODY, "query");
|
||||
EXPECT_GT(bodyid, 0);
|
||||
int siteid = mj_name2id(model, 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, data, 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, data, 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);
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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];
|
||||
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model, testing::NotNull()) << error;
|
||||
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];
|
||||
// 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, 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, mj_isSparse(model),
|
||||
/*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)));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ static const char* const kDampedPendulumPath =
|
||||
"engine/testdata/derivative/damped_pendulum.xml";
|
||||
static const char* const kLinearPath =
|
||||
"engine/testdata/derivative/linear.xml";
|
||||
static const char* const kDCMotorPath =
|
||||
"engine/testdata/derivative/dcmotor.xml";
|
||||
static const char* const kModelPath = "testdata/model.xml";
|
||||
|
||||
// compare analytic and finite-difference d_smooth/d_qvel
|
||||
@@ -99,9 +101,12 @@ TEST_F(DerivativeTest, SmoothDvel) {
|
||||
for (const char* local_path : {kEnergyConservingPendulumPath,
|
||||
kTumblingThinObjectPath,
|
||||
kDampedActuatorsPath,
|
||||
kDamperActuatorsPath}) {
|
||||
kDamperActuatorsPath,
|
||||
kDCMotorPath}) {
|
||||
const std::string xml_path = GetTestDataFilePath(local_path);
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0);
|
||||
char error[1024] = "";
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
|
||||
ASSERT_THAT(model, testing::NotNull()) << "Failed to load model: " << error;
|
||||
int nD = model->nD;
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
@@ -315,17 +320,19 @@ TEST_F(DerivativeTest, PassiveDvel) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// get analytic derivatives
|
||||
mju_zero(data->qDeriv, model->nD);
|
||||
mjd_passive_vel(model, data);
|
||||
mju_copy(qDerivAnalytic, data->qDeriv, nD);
|
||||
|
||||
// clear qDeriv, get finite-difference derivatives
|
||||
mju_zero(data->qDeriv, nD);
|
||||
mju_zero(qDerivFD, nD);
|
||||
mjtNum eps = MjTol(1e-6, 1e-3);
|
||||
mjtNum eps = MjTol(1e-6, 1e-4);
|
||||
mjd_passive_velFD(model, data, eps);
|
||||
|
||||
// expect FD and analytic derivatives to be similar to tol precision
|
||||
EXPECT_THAT(AsVector(data->qDeriv, nD),
|
||||
Pointwise(MjNear(1e-4, 1e-3), AsVector(qDerivAnalytic, nD)));
|
||||
Pointwise(MjNear(1e-6, 1e-4), AsVector(qDerivAnalytic, nD)));
|
||||
}
|
||||
|
||||
mju_free(qDerivFD);
|
||||
@@ -758,9 +765,12 @@ TEST_F(DerivativeTest, DenseSparseRneEquivalent) {
|
||||
for (const char* local_path : {kEnergyConservingPendulumPath,
|
||||
kTumblingThinObjectPath,
|
||||
kDampedActuatorsPath,
|
||||
kDamperActuatorsPath}) {
|
||||
kDamperActuatorsPath,
|
||||
kDCMotorPath}) {
|
||||
const std::string xml_path = GetTestDataFilePath(local_path);
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0);
|
||||
char error[1024] = "";
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
|
||||
ASSERT_THAT(model, testing::NotNull()) << "Failed to load model: " << error;
|
||||
int nD = model->nD;
|
||||
mjtNum* qDeriv = (mjtNum*) mju_malloc(sizeof(mjtNum)*nD);
|
||||
mjData* data = mj_makeData(model);
|
||||
@@ -1300,6 +1310,123 @@ TEST_F(DerivativeTest, ActearlyDerivative) {
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
|
||||
// verify stateful DC motor derivative matches analytical formula
|
||||
TEST_F(DerivativeTest, DCMotorStatefulDerivative) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option timestep="0.002"/>
|
||||
<worldbody>
|
||||
<body>
|
||||
<joint name="j" type="slide"/>
|
||||
<geom type="sphere" size="0.1" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor name="dc" joint="j" motorconst="2.0" resistance="0.5"
|
||||
inductance="0 0.001" input="position" controller="10 0 5"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// set nonzero velocity and ctrl
|
||||
d->qvel[0] = 1.0;
|
||||
d->ctrl[0] = 0.5;
|
||||
|
||||
// forward to compute act_dot, etc.
|
||||
mj_forward(m, d);
|
||||
|
||||
// compute analytical derivatives
|
||||
mjd_smooth_vel(m, d, /* flg_bias = */ 1);
|
||||
|
||||
// extract diagonal of qDeriv
|
||||
mjtNum qDeriv_diag = d->qDeriv[m->D_rowadr[0] + m->D_rownnz[0] - 1];
|
||||
|
||||
// expected: K*(dVdw - K)*(1 - exp(-h/te))/R
|
||||
// with K=2, R=0.5, te=0.001, h=0.002, kd=5, dVdw=-5
|
||||
mjtNum K = 2.0, R = 0.5, te = 0.001, h = 0.002, kd = 5.0;
|
||||
mjtNum expected = K * (-kd - K) * (1 - mju_exp(-h / te)) / R;
|
||||
EXPECT_NEAR(qDeriv_diag, expected, 1e-10)
|
||||
<< "stateful DC motor derivative should match analytical formula";
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
|
||||
// verify that stateful DC motor derivative converges to stateless as te -> 0
|
||||
TEST_F(DerivativeTest, DCMotorStatefulConvergesToStateless) {
|
||||
// stateless DC motor with position controller
|
||||
static constexpr char xml_stateless[] = R"(
|
||||
<mujoco>
|
||||
<option timestep="0.002"/>
|
||||
<worldbody>
|
||||
<body>
|
||||
<joint name="j" type="slide"/>
|
||||
<geom type="sphere" size="0.1" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor name="dc" joint="j" motorconst="1.0" resistance="1.0"
|
||||
input="position" controller="10 0 5"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
// stateful DC motor with very small te
|
||||
static constexpr char xml_stateful[] = R"(
|
||||
<mujoco>
|
||||
<option timestep="0.002"/>
|
||||
<worldbody>
|
||||
<body>
|
||||
<joint name="j" type="slide"/>
|
||||
<geom type="sphere" size="0.1" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor name="dc" joint="j" motorconst="1.0" resistance="1.0"
|
||||
inductance="0 1e-8" input="position" controller="10 0 5"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
mjModel* m_sl = LoadModelFromString(xml_stateless, error, sizeof(error));
|
||||
ASSERT_THAT(m_sl, NotNull()) << error;
|
||||
mjData* d_sl = mj_makeData(m_sl);
|
||||
|
||||
mjModel* m_sf = LoadModelFromString(xml_stateful, error, sizeof(error));
|
||||
ASSERT_THAT(m_sf, NotNull()) << error;
|
||||
mjData* d_sf = mj_makeData(m_sf);
|
||||
|
||||
// set identical state
|
||||
d_sl->qvel[0] = d_sf->qvel[0] = 1.0;
|
||||
d_sl->ctrl[0] = d_sf->ctrl[0] = 0.5;
|
||||
|
||||
// forward and compute derivatives
|
||||
mj_forward(m_sl, d_sl);
|
||||
mj_forward(m_sf, d_sf);
|
||||
mjd_smooth_vel(m_sl, d_sl, 1);
|
||||
mjd_smooth_vel(m_sf, d_sf, 1);
|
||||
|
||||
// extract diagonals
|
||||
mjtNum diag_sl = d_sl->qDeriv[m_sl->D_rowadr[0] + m_sl->D_rownnz[0] - 1];
|
||||
mjtNum diag_sf = d_sf->qDeriv[m_sf->D_rowadr[0] + m_sf->D_rownnz[0] - 1];
|
||||
|
||||
EXPECT_NEAR(diag_sf, diag_sl, 1e-6)
|
||||
<< "stateful derivative should converge to stateless as te -> 0";
|
||||
|
||||
mj_deleteData(d_sf);
|
||||
mj_deleteModel(m_sf);
|
||||
mj_deleteData(d_sl);
|
||||
mj_deleteModel(m_sl);
|
||||
}
|
||||
|
||||
// Utility: Rotate flex grid
|
||||
void RotateFlexGrid(mjModel* model, mjData* data, const char* flex_name,
|
||||
double angle) {
|
||||
@@ -1346,6 +1473,26 @@ void RotateFlexGrid(mjModel* model, mjData* data, const char* flex_name,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: assemble flex stiffness into dense matrix via matrix-vector products.
|
||||
// Builds K column-by-column using mjd_flexInterp_mul.
|
||||
// Result is -(h^2 + h*damping) * J'KJ (negative sign matches the old addH
|
||||
// convention where stiffness is subtracted from the system matrix).
|
||||
static void mulKD_dense(mjModel* m, mjData* d, mjtNum* H_dense,
|
||||
int nv, mjtNum h) {
|
||||
std::vector<mjtNum> e_i(nv, 0);
|
||||
std::vector<mjtNum> col(nv, 0);
|
||||
for (int i = 0; i < nv; i++) {
|
||||
mju_zero(e_i.data(), nv);
|
||||
mju_zero(col.data(), nv);
|
||||
e_i[i] = 1.0;
|
||||
mjd_flexInterp_mul(m, d, col.data(), e_i.data(), h * h, h);
|
||||
// col = +(h^2 + h*damp)*K*e_i, negate to match addH convention (H -= K)
|
||||
for (int j = 0; j < nv; j++) {
|
||||
H_dense[j * nv + i] = -col[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compare analytic and fin-diff d_qfrc_passive/d_qvel for flex interp
|
||||
// Combined test for verify mjd_flexInterp_mulK (stiffness) and damping
|
||||
TEST_F(DerivativeTest, FlexInterpDerivatives) {
|
||||
@@ -1387,18 +1534,16 @@ TEST_F(DerivativeTest, FlexInterpDerivatives) {
|
||||
vec[i] = mju_Halton(i, 2) - 0.5;
|
||||
}
|
||||
|
||||
// use addH to compute K * vec
|
||||
// addH adds (h^2*K + h*D) to H
|
||||
// if we set h=1, damping=0, we get K added to H
|
||||
// use mulKD to compute K * vec
|
||||
// mulKD adds (h^2*K + h*D)*vec to res
|
||||
// if we set h=1, damping=0, we get K*vec
|
||||
mjtNum save_damping = model->flex_damping[0];
|
||||
model->flex_damping[0] = 0;
|
||||
|
||||
std::vector<mjtNum> H(nv * nv, 0);
|
||||
std::vector<int> dof_indices(nv);
|
||||
for (int i = 0; i < nv; i++) dof_indices[i] = i;
|
||||
|
||||
// assemble K into H
|
||||
mjd_flexInterp_addH(model, data, H.data(), dof_indices.data(), nv, 1.0);
|
||||
// assemble K into H column-by-column
|
||||
mulKD_dense(model, data, H.data(), nv, 1.0);
|
||||
|
||||
// restore damping
|
||||
model->flex_damping[0] = save_damping;
|
||||
@@ -1485,16 +1630,13 @@ TEST_F(DerivativeTest, FlexInterpDerivatives) {
|
||||
// check that we have non-zero damping (FD should find it)
|
||||
EXPECT_GT(mju_norm(qDerivFD.data(), nD), 1e-3);
|
||||
|
||||
// compute expected flex damping using mjd_flexInterp_addH
|
||||
// compute expected flex damping using mulKD_dense
|
||||
// D = 4*H(0.5) - H(1)
|
||||
vector<int> dof_indices(nv);
|
||||
for (int i = 0; i < nv; i++) dof_indices[i] = i;
|
||||
|
||||
vector<mjtNum> H1(nv * nv, 0);
|
||||
mjd_flexInterp_addH(model, data, H1.data(), dof_indices.data(), nv, 1.0);
|
||||
mulKD_dense(model, data, H1.data(), nv, 1.0);
|
||||
|
||||
vector<mjtNum> H2(nv * nv, 0);
|
||||
mjd_flexInterp_addH(model, data, H2.data(), dof_indices.data(), nv, 0.5);
|
||||
mulKD_dense(model, data, H2.data(), nv, 0.5);
|
||||
|
||||
vector<mjtNum> D(nv * nv);
|
||||
for (int i = 0; i < nv * nv; i++) {
|
||||
@@ -1562,14 +1704,11 @@ TEST_F(DerivativeTest, FlexInterpDerivativesDeformed) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// 1. Compute Analytic Jacobian (Approximate)
|
||||
// We use mjd_flexInterp_addH to get K_approx
|
||||
// We use mulKD_dense to get K_approx
|
||||
std::vector<mjtNum> H_approx(nv * nv, 0);
|
||||
std::vector<int> dof_indices(nv);
|
||||
for (int i = 0; i < nv; i++) dof_indices[i] = i;
|
||||
|
||||
// h=1, damping=0 => adds K to H
|
||||
mjd_flexInterp_addH(model, data, H_approx.data(), dof_indices.data(), nv,
|
||||
1.0);
|
||||
// h=1, damping=0 => gives K
|
||||
mulKD_dense(model, data, H_approx.data(), nv, 1.0);
|
||||
|
||||
// 2. Compute Finite Difference Jacobian (Ground Truth)
|
||||
// qfrc_passive = -dV/dq
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -73,9 +73,16 @@ TEST_F(InverseTest, DiscreteInverseMatch) {
|
||||
mjtNum* qvel_next = (mjtNum*)mju_malloc(nv * sizeof(mjtNum));
|
||||
mjtNum* qacc_fd = (mjtNum*)mju_malloc(nv * sizeof(mjtNum));
|
||||
|
||||
for (auto integrator : {mjINT_EULER, mjINT_IMPLICIT, mjINT_IMPLICITFAST}) {
|
||||
for (auto integrator : {mjINT_EULER, mjINT_IMPLICIT}) {
|
||||
model->opt.integrator = integrator;
|
||||
for (bool invdiscrete : {false, true}) {
|
||||
// set/unset mjENBL_INVDISCRETE flag (affects both forward and inverse)
|
||||
if (invdiscrete) {
|
||||
model->opt.enableflags |= mjENBL_INVDISCRETE;
|
||||
} else {
|
||||
model->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
}
|
||||
|
||||
// simulate
|
||||
mj_resetData(model, data);
|
||||
for (int i = 0; i < kSteps; ++i) {
|
||||
@@ -98,17 +105,9 @@ TEST_F(InverseTest, DiscreteInverseMatch) {
|
||||
mj_forward(model, data);
|
||||
mju_copy(data->qacc, qacc_fd, nv);
|
||||
|
||||
// set/unset mjENBL_INVDISCRETE flag
|
||||
if (invdiscrete) {
|
||||
model->opt.enableflags |= mjENBL_INVDISCRETE;
|
||||
} else {
|
||||
model->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
}
|
||||
|
||||
// call built-in testing function
|
||||
mj_compareFwdInv(model, data);
|
||||
|
||||
// depending on mjENBL_INVDISCRETE flag, expect mismatch to be small/large
|
||||
if (invdiscrete) {
|
||||
mjtNum epsilon = MjTol(1e-9, 0.05);
|
||||
EXPECT_LT(data->solver_fwdinv[0], epsilon);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// Tests for engine/engine_island.c.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
@@ -602,5 +603,41 @@ TEST_F(IslandTest, EqualityConstraintOfTendons) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(IslandTest, PGSIsland) {
|
||||
const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath);
|
||||
char error[1024];
|
||||
mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
|
||||
ASSERT_THAT(m, NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// simulate to get a non-trivial state
|
||||
while (d->time < 0.5) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
// switch to PGS, disable early termination
|
||||
m->opt.solver = mjSOL_PGS;
|
||||
m->opt.tolerance = 0;
|
||||
|
||||
// solve with islands
|
||||
m->opt.disableflags &= ~mjDSBL_ISLAND;
|
||||
mj_forward(m, d);
|
||||
ASSERT_GT(d->nisland, 1);
|
||||
std::vector<mjtNum> qfrc_island(d->qfrc_constraint,
|
||||
d->qfrc_constraint + m->nv);
|
||||
|
||||
// solve without islands
|
||||
m->opt.disableflags |= mjDSBL_ISLAND;
|
||||
mj_forward(m, d);
|
||||
std::vector<mjtNum> qfrc_mono(d->qfrc_constraint,
|
||||
d->qfrc_constraint + m->nv);
|
||||
|
||||
// expect close match (inexact due to randomized constraint visitation order)
|
||||
EXPECT_THAT(qfrc_island, Pointwise(MjNear(1e-3, 1e-3), qfrc_mono));
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -847,5 +847,187 @@ TEST_F(PassiveTest, PolynomialDampingTendon) {
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// shell-mode (elastic2d=stretch) flexcomp must have zero passive spring forces
|
||||
// at rest (initial configuration); any nonzero force indicates a rotation
|
||||
// mismatch between compile-time reference positions and runtime corotation.
|
||||
TEST_F(ElasticityTest, ShellModeZeroForceAtRest) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option gravity="0 0 0"/>
|
||||
<worldbody>
|
||||
<flexcomp type="grid" count="8 8 8" spacing=".07 .07 .07" pos="0 0 1"
|
||||
dim="3" cellcount="1 1 1" radius=".001" rgba="0 .7 .7 1"
|
||||
mass="5" name="softbody" dof="trilinear">
|
||||
<elasticity young="1e4" poisson="0.1" damping="0.01"
|
||||
elastic2d="stretch" thickness="0.02"/>
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024] = {0};
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, testing::NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
mj_forward(m, d);
|
||||
|
||||
// all spring forces should be zero at rest
|
||||
for (int i = 0; i < m->nv; i++) {
|
||||
EXPECT_NEAR(d->qfrc_spring[i], 0, 1e-10)
|
||||
<< "nonzero spring force at DOF " << i;
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// interpolated shell bending must produce zero spring forces at rest
|
||||
TEST_F(ElasticityTest, InterpBendingZeroForceAtRest) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option gravity="0 0 0"/>
|
||||
<worldbody>
|
||||
<flexcomp type="grid" count="8 8 8" spacing=".07 .07 .07" pos="0 0 1"
|
||||
dim="3" cellcount="2 2 1" radius=".001" rgba="0 .7 .7 1"
|
||||
mass="5" name="softbody" dof="trilinear">
|
||||
<elasticity young="1e4" poisson="0.1" damping="0"
|
||||
elastic2d="bend" thickness="0.02"/>
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024] = {0};
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, testing::NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// verify bending data was compiled
|
||||
const mjtNum* bdata = m->flex_bending + m->flex_bendingadr[0];
|
||||
int nedge = (int)bdata[0];
|
||||
EXPECT_GT(nedge, 0) << "no bending edges compiled";
|
||||
|
||||
mj_forward(m, d);
|
||||
|
||||
// all spring forces should be zero at rest
|
||||
for (int i = 0; i < m->nv; i++) {
|
||||
EXPECT_NEAR(d->qfrc_spring[i], 0, 1e-10)
|
||||
<< "nonzero spring force at DOF " << i;
|
||||
}
|
||||
|
||||
// verify per-edge bending data
|
||||
int n_flat = 0, n_corner = 0;
|
||||
for (int e = 0; e < nedge; e++) {
|
||||
const mjtNum* edata = bdata + 1 + e * 10;
|
||||
mjtNum stiffness = edata[6];
|
||||
mjtNum dn0[3] = {edata[7], edata[8], edata[9]};
|
||||
mjtNum dn0_norm = mju_norm3(dn0);
|
||||
|
||||
// stiffness must be positive
|
||||
EXPECT_GT(stiffness, 0) << "edge " << e << " has non-positive stiffness";
|
||||
|
||||
if (dn0_norm < 1e-10) {
|
||||
// intra-surface edge: coplanar faces, zero normal jump
|
||||
n_flat++;
|
||||
} else {
|
||||
// corner edge: 90° between perpendicular face normals, |dn0| = sqrt(2)
|
||||
n_corner++;
|
||||
EXPECT_NEAR(dn0_norm, mju_sqrt(2.0), 1e-10)
|
||||
<< "corner edge " << e << " has unexpected |dn0|=" << dn0_norm;
|
||||
}
|
||||
}
|
||||
|
||||
// for a 2x2x1 box: 12 intra-surface + 20 corner = 32 edges
|
||||
EXPECT_GT(n_flat, 0) << "no intra-surface edges found";
|
||||
EXPECT_GT(n_corner, 0) << "no corner edges found";
|
||||
EXPECT_EQ(n_flat + n_corner, nedge);
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// interpolated shell bending must produce zero forces after a rigid rotation
|
||||
TEST_F(ElasticityTest, InterpBendingRigidRotationInvariance) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option gravity="0 0 0"/>
|
||||
<worldbody>
|
||||
<flexcomp type="grid" count="8 2 12" spacing=".025 .05 .025" pos="0 0 1"
|
||||
dim="3" cellcount="6 1 6" radius=".001" rgba="0 .7 .7 1"
|
||||
mass="5" name="softbody" dof="trilinear">
|
||||
<elasticity young="1e5" poisson="0.3" damping="0"
|
||||
elastic2d="bend" thickness="0.03"/>
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024] = {0};
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, testing::NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// compute geometric center from body positions (skip world body)
|
||||
mjtNum center[3] = {0, 0, 0};
|
||||
int nnodes = 0;
|
||||
for (int b = 1; b < m->nbody; b++) {
|
||||
center[0] += m->body_pos[3*b + 0];
|
||||
center[1] += m->body_pos[3*b + 1];
|
||||
center[2] += m->body_pos[3*b + 2];
|
||||
nnodes++;
|
||||
}
|
||||
ASSERT_GT(nnodes, 0);
|
||||
center[0] /= nnodes; center[1] /= nnodes; center[2] /= nnodes;
|
||||
|
||||
// rotation: 45 degrees about (1,1,1)/sqrt(3)
|
||||
mjtNum angle = 45 * 3.14159265358979 / 180.0;
|
||||
mjtNum sa = mju_sin(angle / 2), ca = mju_cos(angle / 2);
|
||||
mjtNum inv_sqrt3 = 1.0 / mju_sqrt(3.0);
|
||||
mjtNum quat[4] = {ca, sa * inv_sqrt3, sa * inv_sqrt3, sa * inv_sqrt3};
|
||||
mjtNum neg_quat[4];
|
||||
mju_negQuat(neg_quat, quat);
|
||||
|
||||
// apply rigid rotation via slide joint displacements:
|
||||
// new_pos = center + R * (body_pos - center)
|
||||
// qpos = new_pos - body_pos
|
||||
for (int b = 1; b < m->nbody; b++) {
|
||||
mjtNum rel[3] = {m->body_pos[3*b+0] - center[0],
|
||||
m->body_pos[3*b+1] - center[1],
|
||||
m->body_pos[3*b+2] - center[2]};
|
||||
mjtNum rotated[3];
|
||||
mju_rotVecQuat(rotated, rel, neg_quat);
|
||||
|
||||
// each body has 3 slide joints (x, y, z)
|
||||
for (int j = 0; j < m->body_jntnum[b] && j < 3; j++) {
|
||||
int jid = m->body_jntadr[b] + j;
|
||||
int qadr = m->jnt_qposadr[jid];
|
||||
int axis = -1;
|
||||
for (int a = 0; a < 3; a++) {
|
||||
if (m->jnt_axis[3*jid + a] != 0) { axis = a; break; }
|
||||
}
|
||||
if (axis >= 0) {
|
||||
d->qpos[qadr] =
|
||||
(center[axis] + rotated[axis]) - m->body_pos[3 * b + axis];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mj_forward(m, d);
|
||||
|
||||
// spring forces should still be zero after rigid rotation
|
||||
constexpr mjtNum tol = MjTol(1e-6, 1e-3);
|
||||
for (int i = 0; i < m->nv; i++) {
|
||||
EXPECT_NEAR(d->qfrc_spring[i], 0, tol)
|
||||
<< "nonzero spring force at DOF " << i << " after rigid rotation";
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -1727,5 +1727,62 @@ TEST_F(SensorTest, TactileSkipTangents) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
|
||||
// insidesite uses subtree_com for massless flex parent bodies
|
||||
TEST_F(SensorTest, InsideSiteFlexBody) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option gravity="0 0 0"/>
|
||||
<worldbody>
|
||||
<body name="parent">
|
||||
<flexcomp name="soft" type="grid" count="3 3 3"
|
||||
radius="0.01" dim="3" mass="1">
|
||||
<elasticity young="5e4" poisson="0.2"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
|
||||
<!-- large site centered at origin, should contain the flex -->
|
||||
<site name="container" type="box" size="2 2 2"/>
|
||||
</worldbody>
|
||||
|
||||
<sensor>
|
||||
<insidesite name="inside" site="container"
|
||||
objtype="body" objname="parent"/>
|
||||
</sensor>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024] = {0};
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// flex is at origin, site is a large box at origin — should be inside
|
||||
mj_forward(m, d);
|
||||
EXPECT_EQ(d->sensordata[0], 1)
|
||||
<< "flex body should be inside the container site";
|
||||
|
||||
// shift all vertex/node bodies far outside the site via qpos
|
||||
// each body has 3 slide joints (x, y, z); shift z by +10
|
||||
int parent_id = mj_name2id(m, mjOBJ_BODY, "parent");
|
||||
for (int b = parent_id + 1; b < m->nbody; b++) {
|
||||
if (m->body_parentid[b] == parent_id) {
|
||||
int jadr = m->body_jntadr[b];
|
||||
if (jadr >= 0 && m->body_jntnum[b] == 3) {
|
||||
// z-slide is the 3rd joint
|
||||
d->qpos[m->jnt_qposadr[jadr + 2]] = 10.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
mj_forward(m, d);
|
||||
|
||||
// subtree_com should now be far outside; sensor should read 0
|
||||
EXPECT_EQ(d->sensordata[0], 0)
|
||||
<< "flex body should be outside the container site after displacement";
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -533,6 +533,59 @@ TEST_F(SleepTest, Equality) {
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
// Test that the midpoint integrator doesn't break the sleep qvel=0 invariant.
|
||||
// A standalone free body (eligible for midpoint) with high viscosity should
|
||||
// eventually go to sleep, and after sleeping, qvel/qacc must be exactly zero.
|
||||
TEST_F(SleepTest, MidpointSleepZeroVelocity) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" viscosity="10"
|
||||
sleep_tolerance="0.01">
|
||||
<flag sleep="enable" gravity="disable" constraint="disable"
|
||||
contact="disable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<body>
|
||||
<freejoint/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1" euler="10 20 30"
|
||||
pos=".03 .02 .01"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
mjModel* m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m, NotNull()) << error;
|
||||
mjData* d = mj_makeData(m);
|
||||
|
||||
// give initial velocity (both translational and angular)
|
||||
d->qvel[0] = 0.5;
|
||||
d->qvel[1] = 0.5;
|
||||
d->qvel[2] = 0.5;
|
||||
d->qvel[3] = 1.0;
|
||||
d->qvel[4] = 2.0;
|
||||
d->qvel[5] = 3.0;
|
||||
|
||||
// step until body goes to sleep
|
||||
for (int step = 0; step < 1000; step++) {
|
||||
mj_step(m, d);
|
||||
if (d->ntree_awake == 0) break;
|
||||
}
|
||||
|
||||
// body should have gone to sleep
|
||||
ASSERT_EQ(d->ntree_awake, 0) << "body did not go to sleep";
|
||||
|
||||
// qvel and qacc must be exactly zero for sleeping body
|
||||
for (int i = 0; i < 6; i++) {
|
||||
EXPECT_EQ(d->qvel[i], 0.0) << "qvel[" << i << "] not zero after sleep";
|
||||
EXPECT_EQ(d->qacc[i], 0.0) << "qacc[" << i << "] not zero after sleep";
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
static const char* const kInitIslandFailModel =
|
||||
"engine/testdata/sleep/init_island_fail.xml";
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ TEST_F(SolverTest, IslandsEquivalent) {
|
||||
model->opt.tolerance = 0; // set tolerance to 0
|
||||
model->opt.ls_tolerance = 0; // set ls_tolerance to 0
|
||||
model->opt.ccd_tolerance = 0; // set ccd_tolerance to 0
|
||||
model->opt.disableflags |= mjDSBL_MULTICCD; // disable multiccd
|
||||
|
||||
int nv = model->nv;
|
||||
|
||||
@@ -55,16 +56,21 @@ TEST_F(SolverTest, IslandsEquivalent) {
|
||||
mjData* data_island = mj_makeData(model);
|
||||
mjData* data_noisland = mj_makeData(model);
|
||||
|
||||
// Below are 3 tolerances associated with 3 different iteration counts,
|
||||
// they are only moderately tight, 12x higher than x86-64 failure on Linux,
|
||||
// i.e. in that case the test fails with rtol smaller than {6e-3, 6e-4, 6e-5}.
|
||||
constexpr int kNumTol = 3;
|
||||
mjtNum maxiter[kNumTol] = {30, 40, 60};
|
||||
// Below are 3 tolerances associated with 3 different iteration counts.
|
||||
// Tolerances are set to be ~12x higher than failure thresholds.
|
||||
// For float32, failure thresholds are ~6000x larger than for float64.
|
||||
// Line 99 adds a 500x factor for float32, so we need another ~12x in rtol.
|
||||
// The point of this test is to show that CG convergence is actually not very
|
||||
// precise, simply changing whether islands are used changes the solution by
|
||||
// quite a lot, even at high iteration count and zero {ls_}tolerance.
|
||||
// Increasing the iteration count higher than 60 does not improve convergence.
|
||||
constexpr int kNumTol = 3;
|
||||
mjtNum maxiter[kNumTol] = {30, 40, 60};
|
||||
mjtNum rtol[kNumTol] = {6e-2, 6e-3, 6e-4};
|
||||
mjtNum rtol[kNumTol] = {
|
||||
MjTol(6e-2, 7.2e-1),
|
||||
MjTol(6e-3, 7.2e-2),
|
||||
MjTol(6e-4, 7.2e-3)
|
||||
};
|
||||
|
||||
for (int i = 0; i < kNumTol; ++i) {
|
||||
model->opt.iterations = maxiter[i];
|
||||
|
||||
@@ -14,11 +14,9 @@
|
||||
|
||||
// Tests for engine/{engine_support.c and engine_core_util.c}
|
||||
|
||||
#include "src/engine/engine_core_util.h"
|
||||
#include "src/engine/engine_support.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -40,448 +38,9 @@ using ::testing::Ne;
|
||||
using ::testing::NotNull;
|
||||
using ::testing::Pointwise;
|
||||
|
||||
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];
|
||||
mjModel* model =
|
||||
LoadModelFromString(AngMomTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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
|
||||
for (int i = 0; i < 3; i++) {
|
||||
EXPECT_THAT(angmom_ref[i], MjNear(angmom_test[i], 1e-8, 1e-4));
|
||||
}
|
||||
|
||||
mju_free(angmom_mat);
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// compare subtree angular momentum matrix: analytical and findiff
|
||||
TEST_F(AngMomMatTest, CompareAngMomMats) {
|
||||
char error[1024];
|
||||
mjModel* model =
|
||||
LoadModelFromString(AngMomTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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 constexpr mjtNum eps = MjTol(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, 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
|
||||
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);
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
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];
|
||||
mjModel* model =
|
||||
LoadModelFromString(kJacobianTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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], MjNear(findiff, eps, 1e-2));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
char error[1024];
|
||||
mjModel* model =
|
||||
LoadModelFromString(kJacobianTestingModel, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
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], MjNear(expected, max_abs_err, 1e-4));
|
||||
}
|
||||
}
|
||||
|
||||
mju_free(jac_subtree);
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
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];
|
||||
mjModel* model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
int nv = model->nv;
|
||||
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);
|
||||
|
||||
// get site position
|
||||
int siteid = mj_name2id(model, mjOBJ_SITE, "query");
|
||||
EXPECT_GT(siteid, -1);
|
||||
mjtNum point[3];
|
||||
mju_copy3(point, data->site_xpos+3*siteid);
|
||||
|
||||
// jac, jac_dot
|
||||
vector<mjtNum> jacp(3*nv);
|
||||
vector<mjtNum> jacr(3*nv);
|
||||
mj_jac(model, data, jacp.data(), jacr.data(), point, bodyid);
|
||||
vector<mjtNum> jacp_dot(3*nv);
|
||||
vector<mjtNum> jacr_dot(3*nv);
|
||||
mj_jacDot(model, data, jacp_dot.data(), jacr_dot.data(), point, bodyid);
|
||||
|
||||
// jac_h: jacobian after integrating qpos with a timestep of h
|
||||
constexpr mjtNum h = MjTol(1e-7, 5e-4);
|
||||
mj_integratePos(model, data->qpos, data->qvel, h);
|
||||
mj_kinematics(model, data);
|
||||
mj_comPos(model, data);
|
||||
vector<mjtNum> jacp_h(3*nv);
|
||||
vector<mjtNum> jacr_h(3*nv);
|
||||
mju_copy3(point, data->site_xpos+3*siteid); // get updated site position
|
||||
mj_jac(model, data, jacp_h.data(), jacr_h.data(), point, bodyid);
|
||||
|
||||
// jac_dot_h finite-difference approximation
|
||||
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);
|
||||
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));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
}
|
||||
|
||||
using Name2idTest = MujocoTest;
|
||||
|
||||
@@ -989,7 +548,8 @@ TEST_F(InertiaTest, mulM) {
|
||||
|
||||
// dense M matrix
|
||||
vector<mjtNum> Mdense(nv*nv);
|
||||
mj_fullM(model, Mdense.data(), data->qM);
|
||||
mju_sym2dense(Mdense.data(), data->M, nv,
|
||||
model->M_rownnz, model->M_rowadr, model->M_colind);
|
||||
|
||||
// arbitrary RHS vector
|
||||
vector<mjtNum> vec(nv);
|
||||
@@ -1052,9 +612,9 @@ TEST_F(InertiaTest, FullM) {
|
||||
mjData* d = mj_makeData(m);
|
||||
mj_forward(m, d);
|
||||
|
||||
// get dense mass matrix from qM using mj_fullM
|
||||
// get dense mass matrix from M using mju_sym2dense
|
||||
vector<mjtNum> M(nv * nv);
|
||||
mj_fullM(m, M.data(), d->qM);
|
||||
mju_sym2dense(M.data(), d->M, nv, m->M_rownnz, m->M_rowadr, m->M_colind);
|
||||
|
||||
// get dense mass matrix from M using mju_sparse2dense
|
||||
vector<mjtNum> M_CSR(nv * nv);
|
||||
|
||||
@@ -430,13 +430,90 @@ TEST_F(InterpolationTest, mju_interpolate3D) {
|
||||
expected[0] = quadratic_function_1(sample[0], sample[1], sample[2]);
|
||||
expected[1] = quadratic_function_2(sample[0], sample[1], sample[2]);
|
||||
expected[2] = quadratic_function_3(sample[0], sample[1], sample[2]);
|
||||
mju_interpolate3D(res, sample, coeff, order);
|
||||
mju_interpolate3D(res, sample, coeff, order, NULL);
|
||||
EXPECT_NEAR(res[0], expected[0], MjTol(1e-10, 1e-5));
|
||||
EXPECT_NEAR(res[1], expected[1], MjTol(1e-10, 1e-5));
|
||||
EXPECT_NEAR(res[2], expected[2], MjTol(1e-10, 1e-5));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpolationTest, mju_cellLookup_SingleCell) {
|
||||
// single cell (1x1x1): local coords should equal global coords
|
||||
int cellnum[3] = {1, 1, 1};
|
||||
mjtNum coord[3] = {0.3, 0.7, 0.5};
|
||||
mjtNum local[3];
|
||||
int nodeindices[8];
|
||||
|
||||
int npc = mju_cellLookup(coord, cellnum, 1, local, nodeindices);
|
||||
EXPECT_EQ(npc, 8);
|
||||
EXPECT_NEAR(local[0], 0.3, MjTol(1e-12, 1e-6));
|
||||
EXPECT_NEAR(local[1], 0.7, MjTol(1e-12, 1e-6));
|
||||
EXPECT_NEAR(local[2], 0.5, MjTol(1e-12, 1e-6));
|
||||
|
||||
// for trilinear 1x1x1: nodes are 0..7 in lexicographic order
|
||||
for (int i = 0; i < 8; i++) {
|
||||
EXPECT_EQ(nodeindices[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpolationTest, mju_cellLookup_MultiCell) {
|
||||
// 2x3x4 grid, trilinear: 3x4x5 = 60 nodes
|
||||
int cellnum[3] = {2, 3, 4};
|
||||
int order = 1;
|
||||
int ny_g = 3*1 + 1; // 4
|
||||
int nz_g = 4*1 + 1; // 5
|
||||
|
||||
// point at (0.75, 0.5, 0.125) -> cell (1, 1, 0)
|
||||
mjtNum coord[3] = {0.75, 0.5, 0.125};
|
||||
mjtNum local[3];
|
||||
int nodeindices[8];
|
||||
|
||||
int npc = mju_cellLookup(coord, cellnum, order, local, nodeindices);
|
||||
EXPECT_EQ(npc, 8);
|
||||
|
||||
// cell (1,1,0): local = (0.75*2 - 1, 0.5*3 - 1, 0.125*4 - 0)
|
||||
EXPECT_NEAR(local[0], 0.5, 1e-12);
|
||||
EXPECT_NEAR(local[1], 0.5, 1e-12);
|
||||
EXPECT_NEAR(local[2], 0.5, 1e-12);
|
||||
|
||||
// expected node indices for cell (1,1,0), trilinear:
|
||||
// (gi, gj, gk) for li,lj,lk in {0,1}
|
||||
// gi = 1+li, gj = 1+lj, gk = 0+lk
|
||||
// gidx = gi*ny_g*nz_g + gj*nz_g + gk
|
||||
int expected[8];
|
||||
int ni = 0;
|
||||
for (int li = 0; li <= 1; li++) {
|
||||
for (int lj = 0; lj <= 1; lj++) {
|
||||
for (int lk = 0; lk <= 1; lk++) {
|
||||
expected[ni++] = (1+li)*ny_g*nz_g + (1+lj)*nz_g + lk;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 8; i++) {
|
||||
EXPECT_EQ(nodeindices[i], expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpolationTest, mju_cellLookup_Boundary) {
|
||||
// point exactly at coord=1.0 should clamp to last cell
|
||||
int cellnum[3] = {3, 3, 3};
|
||||
mjtNum coord[3] = {1.0, 1.0, 1.0};
|
||||
mjtNum local[3];
|
||||
|
||||
mju_cellLookup(coord, cellnum, 1, local, NULL);
|
||||
// cell (2,2,2), local = (1*3 - 2, 1*3 - 2, 1*3 - 2) = (1, 1, 1)
|
||||
EXPECT_NEAR(local[0], 1.0, 1e-12);
|
||||
EXPECT_NEAR(local[1], 1.0, 1e-12);
|
||||
EXPECT_NEAR(local[2], 1.0, 1e-12);
|
||||
|
||||
// point at coord=0.0 should map to first cell
|
||||
mjtNum coord0[3] = {0.0, 0.0, 0.0};
|
||||
mju_cellLookup(coord0, cellnum, 1, local, NULL);
|
||||
EXPECT_NEAR(local[0], 0.0, 1e-12);
|
||||
EXPECT_NEAR(local[1], 0.0, 1e-12);
|
||||
EXPECT_NEAR(local[2], 0.0, 1e-12);
|
||||
}
|
||||
|
||||
TEST_F(InterpolationTest, mju_defGradient) {
|
||||
int order = 1;
|
||||
mjtNum mat[9];
|
||||
@@ -521,7 +598,48 @@ TEST_F(InterpolationTest, mju_defGradient) {
|
||||
EXPECT_THAT(mat, Pointwise(MjNear(1e-8, 1e-6), rot7));
|
||||
}
|
||||
|
||||
// --------------------------------- Base64 ------------------------------------
|
||||
TEST_F(InterpolationTest, mju_flexInterpState_MultiCell) {
|
||||
int order = 1; // trilinear
|
||||
int cy = 2;
|
||||
int cz = 2;
|
||||
int nodenum = 27; // 3x3x3
|
||||
|
||||
std::vector<mjtNum> xpos(3 * nodenum);
|
||||
mjtNum quat[4];
|
||||
|
||||
// Populate xpos directly for a grid centered at origin, rotated 90 deg around
|
||||
// Z Original grid points: {-0.1, 0.0, 0.1}^3 Rotated: (x, y, z) -> (-y, x, z)
|
||||
int idx = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
for (int k = 0; k < 3; k++) {
|
||||
mjtNum x = (i - 1) * 0.1;
|
||||
mjtNum y = (j - 1) * 0.1;
|
||||
mjtNum z = (k - 1) * 0.1;
|
||||
|
||||
// Apply rotation
|
||||
xpos[3*idx + 0] = -y;
|
||||
xpos[3*idx + 1] = x;
|
||||
xpos[3*idx + 2] = z;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int npc = (order+1)*(order+1)*(order+1);
|
||||
std::vector<mjtNum> xpos_c(3 * npc);
|
||||
|
||||
mju_flexGatherCellState(order, cy, cz, 0, 0, 0, xpos.data(), NULL, NULL,
|
||||
xpos_c.data(), NULL, NULL, NULL, quat);
|
||||
|
||||
// Expected quaternion for -90 deg around Z (global to local):
|
||||
// [sqrt(0.5), 0, 0, -sqrt(0.5)]
|
||||
mjtNum expected_val = mju_sqrt(0.5);
|
||||
EXPECT_NEAR(quat[0], expected_val, 1e-5);
|
||||
EXPECT_NEAR(quat[1], 0.0, 1e-5);
|
||||
EXPECT_NEAR(quat[2], 0.0, 1e-5);
|
||||
EXPECT_NEAR(quat[3], -expected_val, 1e-5);
|
||||
}
|
||||
|
||||
using Base64Test = MujocoTest;
|
||||
|
||||
@@ -1047,5 +1165,390 @@ TEST_F(HistoryTest, CubicInterpolation) {
|
||||
EXPECT_NEAR(res[1], 1.0 - expected_0_8, MjTol(1e-9, 1e-9));
|
||||
}
|
||||
|
||||
// -------------------------------- Face State ---------------------------------
|
||||
|
||||
using FaceStateTest = MujocoTest;
|
||||
|
||||
// verify mju_flexGatherFaceState returns correct node indices for all 6 faces
|
||||
// of a 1x1x1 trilinear grid (2x2x2 = 8 nodes, 4 nodes per face)
|
||||
TEST_F(FaceStateTest, NodeIndicesSingleCell) {
|
||||
int order = 1;
|
||||
int cx = 1, cy = 1, cz = 1;
|
||||
int ny_g = cy * order + 1; // 2
|
||||
int nz_g = cz * order + 1; // 2
|
||||
|
||||
// nelem_fe = 2*(1*1 + 1*1 + 1*1) = 6 face elements
|
||||
// face 0: x=0, face 1: x=max, face 2: y=0, face 3: y=max,
|
||||
// face 4: z=0, face 5: z=max
|
||||
|
||||
// create dummy positions for 8 nodes
|
||||
std::vector<mjtNum> xpos(3 * 8, 0);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
xpos[3*i + 0] = (i / 4) * 1.0;
|
||||
xpos[3*i + 1] = ((i / 2) % 2) * 1.0;
|
||||
xpos[3*i + 2] = (i % 2) * 1.0;
|
||||
}
|
||||
|
||||
// helper: compute expected global node index from (gx, gy, gz)
|
||||
auto gidx = [&](int gx, int gy, int gz) {
|
||||
return gx * ny_g * nz_g + gy * nz_g + gz;
|
||||
};
|
||||
|
||||
// face 0: x=0 (fixed g[0]=0, varying g[1], g[2])
|
||||
// normal_axis=0, na0=1, na1=2
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 0, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 0, 0));
|
||||
EXPECT_EQ(indices[1], gidx(0, 0, 1));
|
||||
EXPECT_EQ(indices[2], gidx(0, 1, 0));
|
||||
EXPECT_EQ(indices[3], gidx(0, 1, 1));
|
||||
}
|
||||
|
||||
// face 1: x=max (fixed g[0]=1, varying g[1], g[2])
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 1, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(1, 0, 0));
|
||||
EXPECT_EQ(indices[1], gidx(1, 0, 1));
|
||||
EXPECT_EQ(indices[2], gidx(1, 1, 0));
|
||||
EXPECT_EQ(indices[3], gidx(1, 1, 1));
|
||||
}
|
||||
|
||||
// face 2: y=0 (fixed g[1]=0)
|
||||
// normal_axis=1, na0=2(z slow), na1=0(x fast)
|
||||
// loop order: l0→z, l1→x
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 2, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 0, 0)); // l0=0(z=0), l1=0(x=0)
|
||||
EXPECT_EQ(indices[1], gidx(1, 0, 0)); // l0=0(z=0), l1=1(x=1)
|
||||
EXPECT_EQ(indices[2], gidx(0, 0, 1)); // l0=1(z=1), l1=0(x=0)
|
||||
EXPECT_EQ(indices[3], gidx(1, 0, 1)); // l0=1(z=1), l1=1(x=1)
|
||||
}
|
||||
|
||||
// face 3: y=max (fixed g[1]=1)
|
||||
// normal_axis=1, na0=2(z slow), na1=0(x fast)
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 3, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 1, 0)); // l0=0(z=0), l1=0(x=0)
|
||||
EXPECT_EQ(indices[1], gidx(1, 1, 0)); // l0=0(z=0), l1=1(x=1)
|
||||
EXPECT_EQ(indices[2], gidx(0, 1, 1)); // l0=1(z=1), l1=0(x=0)
|
||||
EXPECT_EQ(indices[3], gidx(1, 1, 1)); // l0=1(z=1), l1=1(x=1)
|
||||
}
|
||||
|
||||
// face 4: z=0 (fixed g[2]=0, varying g[0], g[1])
|
||||
// normal_axis=2, na0=0, na1=1
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 4, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 0, 0));
|
||||
EXPECT_EQ(indices[1], gidx(0, 1, 0));
|
||||
EXPECT_EQ(indices[2], gidx(1, 0, 0));
|
||||
EXPECT_EQ(indices[3], gidx(1, 1, 0));
|
||||
}
|
||||
|
||||
// face 5: z=max (fixed g[2]=1)
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 5, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 0, 1));
|
||||
EXPECT_EQ(indices[1], gidx(0, 1, 1));
|
||||
EXPECT_EQ(indices[2], gidx(1, 0, 1));
|
||||
EXPECT_EQ(indices[3], gidx(1, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// verify node indices for a multi-cell grid (2x2x2 cells → 3x3x3 = 27 nodes)
|
||||
TEST_F(FaceStateTest, NodeIndicesMultiCell) {
|
||||
int order = 1;
|
||||
int cx = 2, cy = 2, cz = 2;
|
||||
int ny_g = 3, nz_g = 3; // (2*1+1) = 3
|
||||
|
||||
// nelem_fe = 2*(2*2 + 2*2 + 2*2) = 24 face elements
|
||||
// face 0: x=0, cy*cz = 4 quads (indices 0-3)
|
||||
// face 1: x=max, 4 quads (indices 4-7)
|
||||
// face 2: y=0, cx*cz = 4 quads (indices 8-11)
|
||||
// face 3: y=max, 4 quads (indices 12-15)
|
||||
// face 4: z=0, cx*cy = 4 quads (indices 16-19)
|
||||
// face 5: z=max, 4 quads (indices 20-23)
|
||||
|
||||
std::vector<mjtNum> xpos(3 * 27, 0);
|
||||
for (int i = 0; i < 27; i++) {
|
||||
int gi = i / 9;
|
||||
int gj = (i / 3) % 3;
|
||||
int gk = i % 3;
|
||||
xpos[3*i + 0] = gi * 0.1;
|
||||
xpos[3*i + 1] = gj * 0.1;
|
||||
xpos[3*i + 2] = gk * 0.1;
|
||||
}
|
||||
|
||||
auto gidx = [&](int gx, int gy, int gz) {
|
||||
return gx * ny_g * nz_g + gy * nz_g + gz;
|
||||
};
|
||||
|
||||
// face 0 (x=0), quad 0: (q0=0, q1=0) within cy*cz face
|
||||
// c1 = face_count1[0] = cz = 2, so quad (0,0) → within_face = 0
|
||||
// na0=1, na1=2: g[0]=0, g[1]=0..1, g[2]=0..1
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 0, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 0, 0));
|
||||
EXPECT_EQ(indices[1], gidx(0, 0, 1));
|
||||
EXPECT_EQ(indices[2], gidx(0, 1, 0));
|
||||
EXPECT_EQ(indices[3], gidx(0, 1, 1));
|
||||
}
|
||||
|
||||
// face 0 (x=0), quad 3: (q0=1, q1=1) → within_face = 1*2+1 = 3
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 3, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 1, 1));
|
||||
EXPECT_EQ(indices[1], gidx(0, 1, 2));
|
||||
EXPECT_EQ(indices[2], gidx(0, 2, 1));
|
||||
EXPECT_EQ(indices[3], gidx(0, 2, 2));
|
||||
}
|
||||
|
||||
// face 1 (x=max), quad 0: fe_idx = 4 (after face 0's 4 quads)
|
||||
// g[0] = cx*order = 2
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 4, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(2, 0, 0));
|
||||
EXPECT_EQ(indices[1], gidx(2, 0, 1));
|
||||
EXPECT_EQ(indices[2], gidx(2, 1, 0));
|
||||
EXPECT_EQ(indices[3], gidx(2, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// verify node indices for a non-cubic grid (cx != cz)
|
||||
TEST_F(FaceStateTest, NodeIndicesNonCubicGrid) {
|
||||
int order = 1;
|
||||
int cx = 2, cy = 1, cz = 3;
|
||||
int ny_g = cy * order + 1; // 2
|
||||
int nz_g = cz * order + 1; // 4
|
||||
|
||||
// create dummy positions for (2*1+1)*(1*1+1)*(3*1+1) = 3*2*4 = 24 nodes
|
||||
std::vector<mjtNum> xpos(3 * 24, 0);
|
||||
for (int i = 0; i < 24; i++) {
|
||||
int gi = i / 8;
|
||||
int gj = (i / 4) % 2;
|
||||
int gk = i % 4;
|
||||
xpos[3*i + 0] = gi * 0.1;
|
||||
xpos[3*i + 1] = gj * 0.1;
|
||||
xpos[3*i + 2] = gk * 0.1;
|
||||
}
|
||||
|
||||
auto gidx = [&](int gx, int gy, int gz) {
|
||||
return gx * ny_g * nz_g + gy * nz_g + gz;
|
||||
};
|
||||
|
||||
// face 2 (y=0): normal_axis=1, na0=2(z slow), na1=0(x fast)
|
||||
// counts: na0 -> cz = 3, na1 -> cx = 2
|
||||
// total quads on face 2 = 6
|
||||
// we test within_face = 2 (third quad)
|
||||
// correct: c1 = cx = 2. q0 = 2/2 = 1, q1 = 2%2 = 0
|
||||
//
|
||||
// face element index calculation:
|
||||
// face 0: cy*cz = 1*3 = 3 quads (indices 0-2)
|
||||
// face 1: cy*cz = 1*3 = 3 quads (indices 3-5)
|
||||
// face 2: cx*cz = 2*3 = 6 quads. Quad 2 is index 2 within this face.
|
||||
// Total flat index = 3 + 3 + 2 = 8
|
||||
{
|
||||
int indices[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 8, xpos.data(), NULL, NULL,
|
||||
NULL, NULL, NULL, indices, NULL);
|
||||
EXPECT_EQ(indices[0], gidx(0, 0, 1));
|
||||
EXPECT_EQ(indices[1], gidx(1, 0, 1));
|
||||
EXPECT_EQ(indices[2], gidx(0, 0, 2));
|
||||
EXPECT_EQ(indices[3], gidx(1, 0, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// verify data gathering: positions, velocities, and reference positions
|
||||
TEST_F(FaceStateTest, DataGathering) {
|
||||
int order = 1;
|
||||
int cx = 1, cy = 1, cz = 1;
|
||||
int npe = 4;
|
||||
int nnodes = 8;
|
||||
|
||||
// create positions and velocities for 8 nodes
|
||||
std::vector<mjtNum> xpos(3 * nnodes);
|
||||
std::vector<mjtNum> vel(3 * nnodes);
|
||||
std::vector<mjtNum> xpos0(3 * nnodes);
|
||||
for (int i = 0; i < nnodes; i++) {
|
||||
for (int d = 0; d < 3; d++) {
|
||||
xpos[3*i + d] = 10 * i + d;
|
||||
vel[3*i + d] = 100 * i + d;
|
||||
xpos0[3*i + d] = 1000 * i + d;
|
||||
}
|
||||
}
|
||||
|
||||
// gather face 4 (z=0): nodes at (0,0,0), (0,1,0), (1,0,0), (1,1,0)
|
||||
// = global indices 0, 2, 4, 6
|
||||
std::vector<mjtNum> xpos_f(3 * npe);
|
||||
std::vector<mjtNum> vel_f(3 * npe);
|
||||
std::vector<mjtNum> xpos0_f(3 * npe);
|
||||
int indices[4];
|
||||
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 4, xpos.data(), vel.data(),
|
||||
xpos0.data(), xpos_f.data(), vel_f.data(),
|
||||
xpos0_f.data(), indices, NULL);
|
||||
|
||||
for (int n = 0; n < npe; n++) {
|
||||
int gi = indices[n];
|
||||
for (int d = 0; d < 3; d++) {
|
||||
EXPECT_EQ(xpos_f[3*n + d], xpos[3*gi + d]);
|
||||
EXPECT_EQ(vel_f[3*n + d], vel[3*gi + d]);
|
||||
EXPECT_EQ(xpos0_f[3*n + d], xpos0[3*gi + d]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verify that flexInterpRotation2D produces identity for axis-aligned faces
|
||||
// (tested via mju_flexGatherFaceState with quat output)
|
||||
TEST_F(FaceStateTest, IdentityRotationAxisAligned) {
|
||||
int order = 1;
|
||||
int cx = 1, cy = 1, cz = 1;
|
||||
int npe = 4;
|
||||
|
||||
// create an axis-aligned unit cube: 8 nodes at {0,1}^3
|
||||
std::vector<mjtNum> xpos(3 * 8);
|
||||
int idx = 0;
|
||||
for (int i = 0; i <= 1; i++) {
|
||||
for (int j = 0; j <= 1; j++) {
|
||||
for (int k = 0; k <= 1; k++) {
|
||||
xpos[3*idx + 0] = i;
|
||||
xpos[3*idx + 1] = j;
|
||||
xpos[3*idx + 2] = k;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<mjtNum> xpos_f(3 * npe);
|
||||
mjtNum quat[4];
|
||||
|
||||
// test all 6 faces: each should give identity rotation (quat = [1,0,0,0])
|
||||
int nelem_fe = 6;
|
||||
for (int fe = 0; fe < nelem_fe; fe++) {
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, fe, xpos.data(), NULL, NULL,
|
||||
xpos_f.data(), NULL, NULL, NULL, quat);
|
||||
EXPECT_NEAR(mju_abs(quat[0]), 1.0, 1e-10) << "face " << fe;
|
||||
EXPECT_NEAR(quat[1], 0.0, 1e-10) << "face " << fe;
|
||||
EXPECT_NEAR(quat[2], 0.0, 1e-10) << "face " << fe;
|
||||
EXPECT_NEAR(quat[3], 0.0, 1e-10) << "face " << fe;
|
||||
}
|
||||
}
|
||||
|
||||
// verify that flexInterpRotation2D extracts the correct rotation for a
|
||||
// globally rotated cube (90° around z-axis)
|
||||
TEST_F(FaceStateTest, RotatedCubeRotation) {
|
||||
int order = 1;
|
||||
int cx = 1, cy = 1, cz = 1;
|
||||
int npe = 4;
|
||||
|
||||
// create an axis-aligned unit cube, then rotate 90° around z
|
||||
// rotation: (x,y,z) → (-y, x, z)
|
||||
std::vector<mjtNum> xpos(3 * 8);
|
||||
int idx = 0;
|
||||
for (int i = 0; i <= 1; i++) {
|
||||
for (int j = 0; j <= 1; j++) {
|
||||
for (int k = 0; k <= 1; k++) {
|
||||
mjtNum orig[3] = {(mjtNum)i, (mjtNum)j, (mjtNum)k};
|
||||
mjtNum axis[3] = {0, 0, 1};
|
||||
mjtNum rot_quat[4];
|
||||
mju_axisAngle2Quat(rot_quat, axis, mjPI / 2);
|
||||
mju_rotVecQuat(xpos.data() + 3*idx, orig, rot_quat);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<mjtNum> xpos_f(3 * npe);
|
||||
mjtNum quat[4];
|
||||
|
||||
// expected rotation: global→local is inverse of the 90° z rotation
|
||||
// 90° around z: quat = [cos(45°), 0, 0, sin(45°)]
|
||||
// inverse (global→local): [cos(45°), 0, 0, -sin(45°)]
|
||||
mjtNum sq2 = mju_sqrt(0.5);
|
||||
|
||||
// test face 4 (z=0): normal_axis=2, in-plane axes are (0,1)
|
||||
// tangent vectors should reflect the 90° z rotation
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 4, xpos.data(), NULL, NULL,
|
||||
xpos_f.data(), NULL, NULL, NULL, quat);
|
||||
|
||||
EXPECT_NEAR(quat[0], sq2, 1e-5);
|
||||
EXPECT_NEAR(quat[1], 0.0, 1e-5);
|
||||
EXPECT_NEAR(quat[2], 0.0, 1e-5);
|
||||
EXPECT_NEAR(quat[3], -sq2, 1e-5);
|
||||
|
||||
// test face 5 (z=max): should give same rotation
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, 5, xpos.data(), NULL, NULL,
|
||||
xpos_f.data(), NULL, NULL, NULL, quat);
|
||||
|
||||
EXPECT_NEAR(quat[0], sq2, 1e-5);
|
||||
EXPECT_NEAR(quat[1], 0.0, 1e-5);
|
||||
EXPECT_NEAR(quat[2], 0.0, 1e-5);
|
||||
EXPECT_NEAR(quat[3], -sq2, 1e-5);
|
||||
}
|
||||
|
||||
// verify that flexInterpRotation2D matches the 3D cell rotation for
|
||||
// the same globally-rotated cube
|
||||
TEST_F(FaceStateTest, RotationConsistencyWith3D) {
|
||||
int order = 1;
|
||||
int cx = 1, cy = 1, cz = 1;
|
||||
|
||||
// create 90° z-rotated unit cube
|
||||
std::vector<mjtNum> xpos(3 * 8);
|
||||
int idx = 0;
|
||||
for (int i = 0; i <= 1; i++) {
|
||||
for (int j = 0; j <= 1; j++) {
|
||||
for (int k = 0; k <= 1; k++) {
|
||||
mjtNum orig[3] = {(mjtNum)i, (mjtNum)j, (mjtNum)k};
|
||||
mjtNum axis[3] = {0, 0, 1};
|
||||
mjtNum rot_quat[4];
|
||||
mju_axisAngle2Quat(rot_quat, axis, mjPI / 6);
|
||||
mju_rotVecQuat(xpos.data() + 3*idx, orig, rot_quat);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get 3D cell rotation
|
||||
int npc = 8;
|
||||
std::vector<mjtNum> xpos_c(3 * npc);
|
||||
mjtNum quat_3d[4];
|
||||
mju_flexGatherCellState(order, cy, cz, 0, 0, 0, xpos.data(), NULL, NULL,
|
||||
xpos_c.data(), NULL, NULL, NULL, quat_3d);
|
||||
|
||||
// get 2D face rotation for each face and verify it matches the 3D rotation
|
||||
int npe = 4;
|
||||
std::vector<mjtNum> xpos_f(3 * npe);
|
||||
|
||||
int nelem_fe = 6;
|
||||
for (int fe = 0; fe < nelem_fe; fe++) {
|
||||
mjtNum quat_2d[4];
|
||||
mju_flexGatherFaceState(order, cx, cy, cz, fe, xpos.data(), NULL, NULL,
|
||||
xpos_f.data(), NULL, NULL, NULL, quat_2d);
|
||||
|
||||
// quaternions may differ by sign; compare unsigned
|
||||
mjtNum dot = quat_3d[0]*quat_2d[0] + quat_3d[1]*quat_2d[1] +
|
||||
quat_3d[2]*quat_2d[2] + quat_3d[3]*quat_2d[3];
|
||||
EXPECT_NEAR(mju_abs(dot), 1.0, 1e-5)
|
||||
<< "face " << fe << ": 2D rotation differs from 3D cell rotation";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -1041,5 +1041,147 @@ TEST_F(EngineUtilSolveTest, CholFactorSymbolicNumeric) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// ----------------------------- dense LU --------------------------------------
|
||||
|
||||
using DenseLUTest = MujocoTest;
|
||||
|
||||
// factor identity, solve recovers b exactly
|
||||
TEST_F(DenseLUTest, Identity) {
|
||||
constexpr int n = 4;
|
||||
mjtNum A[n*n] = {
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
};
|
||||
int pivot[n];
|
||||
mjtNum b[n] = {1, 2, 3, 4};
|
||||
mjtNum x[n];
|
||||
|
||||
EXPECT_EQ(mju_factorLU(A, n, pivot), 1);
|
||||
mju_solveLU(x, A, b, pivot, n);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
EXPECT_MJTNUM_EQ(x[i], b[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3x3 with known solution
|
||||
TEST_F(DenseLUTest, SmallKnown) {
|
||||
constexpr int n = 3;
|
||||
// A = [2 1 1; 4 3 3; 8 7 9], b = [1; 1; 1]
|
||||
// solution: x = [1; -1; 0] (verified: A*x = [2-1; 4-3; 8-7] = [1;1;1])
|
||||
mjtNum A[n*n] = {
|
||||
2, 1, 1,
|
||||
4, 3, 3,
|
||||
8, 7, 9,
|
||||
};
|
||||
int pivot[n];
|
||||
mjtNum b[n] = {1, 1, 1};
|
||||
mjtNum x[n];
|
||||
|
||||
EXPECT_EQ(mju_factorLU(A, n, pivot), 1);
|
||||
mju_solveLU(x, A, b, pivot, n);
|
||||
|
||||
mjtNum eps = MjTol(1e-14, 1e-6);
|
||||
EXPECT_NEAR(x[0], 1, eps);
|
||||
EXPECT_NEAR(x[1], -1, eps);
|
||||
EXPECT_NEAR(x[2], 0, eps);
|
||||
}
|
||||
|
||||
// random SPD matrices: compare LU solve against Cholesky solve
|
||||
TEST_F(DenseLUTest, RandomSPD) {
|
||||
std::mt19937_64 rng;
|
||||
rng.seed(7);
|
||||
std::normal_distribution<double> dist(0, 1);
|
||||
|
||||
for (int n : {4, 8, 16}) {
|
||||
vector<mjtNum> sqrtH(n * n);
|
||||
vector<mjtNum> A(n * n);
|
||||
vector<mjtNum> A_chol(n * n);
|
||||
vector<mjtNum> b(n);
|
||||
vector<mjtNum> x_lu(n);
|
||||
vector<mjtNum> x_chol(n);
|
||||
vector<int> pivot(n);
|
||||
|
||||
// generate random SPD matrix
|
||||
for (int i = 0; i < n * n; i++) sqrtH[i] = dist(rng);
|
||||
mju_mulMatTMat(A.data(), sqrtH.data(), sqrtH.data(), n, n, n);
|
||||
|
||||
// add diagonal regularizer
|
||||
for (int i = 0; i < n; i++) A[i*n+i] += n;
|
||||
|
||||
// generate random rhs
|
||||
for (int i = 0; i < n; i++) b[i] = dist(rng);
|
||||
|
||||
// solve with Cholesky
|
||||
mju_copy(A_chol.data(), A.data(), n * n);
|
||||
int rank = mju_cholFactor(A_chol.data(), n, 0);
|
||||
EXPECT_EQ(rank, n);
|
||||
mju_cholSolve(x_chol.data(), A_chol.data(), b.data(), n);
|
||||
|
||||
// solve with LU
|
||||
int ok = mju_factorLU(A.data(), n, pivot.data());
|
||||
EXPECT_EQ(ok, 1);
|
||||
mju_solveLU(x_lu.data(), A.data(), b.data(), pivot.data(), n);
|
||||
|
||||
// compare
|
||||
mjtNum eps = MjTol(1e-15, 1e-7);
|
||||
EXPECT_THAT(AsVector(x_lu.data(), n),
|
||||
Pointwise(MjNear(eps, eps),
|
||||
AsVector(x_chol.data(), n)));
|
||||
}
|
||||
}
|
||||
|
||||
// random non-symmetric matrices: verify A*x == b
|
||||
TEST_F(DenseLUTest, RandomGeneral) {
|
||||
std::mt19937_64 rng;
|
||||
rng.seed(42);
|
||||
std::normal_distribution<double> dist(0, 1);
|
||||
|
||||
for (int n : {3, 5, 10, 20}) {
|
||||
vector<mjtNum> A(n * n);
|
||||
vector<mjtNum> A_orig(n * n);
|
||||
vector<mjtNum> b(n);
|
||||
vector<mjtNum> x(n);
|
||||
vector<mjtNum> Ax(n);
|
||||
vector<int> pivot(n);
|
||||
|
||||
// random non-symmetric matrix with diagonal dominance
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = 0; j < n; j++) {
|
||||
A[i*n+j] = dist(rng);
|
||||
}
|
||||
A[i*n+i] += 2 * n;
|
||||
}
|
||||
mju_copy(A_orig.data(), A.data(), n * n);
|
||||
|
||||
// random rhs
|
||||
for (int i = 0; i < n; i++) b[i] = dist(rng);
|
||||
|
||||
// factor and solve
|
||||
int ok = mju_factorLU(A.data(), n, pivot.data());
|
||||
EXPECT_EQ(ok, 1);
|
||||
mju_solveLU(x.data(), A.data(), b.data(), pivot.data(), n);
|
||||
|
||||
// verify: A_orig * x == b
|
||||
mju_mulMatVec(Ax.data(), A_orig.data(), x.data(), n, n);
|
||||
|
||||
mjtNum eps = MjTol(1e-14, 1e-5);
|
||||
EXPECT_THAT(AsVector(Ax.data(), n),
|
||||
Pointwise(MjNear(eps, eps), AsVector(b.data(), n)));
|
||||
}
|
||||
}
|
||||
|
||||
// near-singular matrix returns 0
|
||||
TEST_F(DenseLUTest, Singular) {
|
||||
constexpr int n = 3;
|
||||
// all zeros: maximally singular
|
||||
mjtNum A[n*n] = {0};
|
||||
int pivot[n];
|
||||
|
||||
EXPECT_EQ(mju_factorLU(A, n, pivot), 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
// Tests for engine/engine_util_sparse.c
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "src/engine/engine_util_sparse.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
@@ -360,6 +361,81 @@ TEST_F(EngineUtilSparseTest, MjuCompressSparse) {
|
||||
EXPECT_EQ(AsVector(dense, 6), AsVector(dense_expected_minval1, 6));
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSym2Dense) {
|
||||
// lower-triangular CSR for a 3x3 symmetric matrix:
|
||||
// 1 2 0
|
||||
// 2 3 4
|
||||
// 0 4 5
|
||||
// stored as lower triangle:
|
||||
// row 0: [1] (col 0)
|
||||
// row 1: [2, 3] (cols 0, 1)
|
||||
// row 2: [4, 5] (cols 1, 2)
|
||||
mjtNum mat[] = {1, 2, 3, 4, 5};
|
||||
int rownnz[] = {1, 2, 2};
|
||||
int rowadr[] = {0, 1, 3};
|
||||
int colind[] = {0, 0, 1, 1, 2};
|
||||
|
||||
mjtNum dense[9];
|
||||
mju_sym2dense(dense, mat, 3, rownnz, rowadr, colind);
|
||||
|
||||
mjtNum expected[] = {1, 2, 0, 2, 3, 4, 0, 4, 5};
|
||||
EXPECT_EQ(AsVector(dense, 9), AsVector(expected, 9));
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSym2DenseWithUpper) {
|
||||
mjtNum mat[] = {1, 999, 2, 3, 4, 5};
|
||||
int rownnz[] = {2, 2, 2};
|
||||
int rowadr[] = {0, 2, 4};
|
||||
int colind[] = {0, 1, 0, 1, 1, 2};
|
||||
|
||||
mjtNum dense[9];
|
||||
mju_sym2dense(dense, mat, 3, rownnz, rowadr, colind);
|
||||
|
||||
mjtNum expected[] = {1, 2, 0,
|
||||
2, 3, 4,
|
||||
0, 4, 5};
|
||||
EXPECT_EQ(AsVector(dense, 9), AsVector(expected, 9));
|
||||
}
|
||||
|
||||
// helper: run split-col approach and return dense result
|
||||
static void SqrMatTDSplitCol(
|
||||
std::vector<mjtNum>& dense_result, int nr, int nc,
|
||||
const mjtNum* mat, const int* rownnz, const int* rowadr, const int* colind,
|
||||
const mjtNum* matT, const int* rownnzT, const int* rowadrT,
|
||||
const int* colindT, const int* rowsuperT, const mjtNum* diag,
|
||||
int* out_diagind, mjData* d) {
|
||||
// count mode
|
||||
std::vector<int> H_rownnz(nc, 0);
|
||||
std::vector<int> H_rowadr(nc, 0);
|
||||
int nnz = mju_sqrMatTDSparseSymbolic(
|
||||
H_rownnz.data(), H_rowadr.data(), nullptr,
|
||||
out_diagind, nr, nc, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, d);
|
||||
|
||||
// fill mode
|
||||
std::vector<int> H_colind(nnz);
|
||||
mju_sqrMatTDSparseSymbolic(
|
||||
H_rownnz.data(), H_rowadr.data(), H_colind.data(),
|
||||
out_diagind, nr, nc, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, d);
|
||||
|
||||
// numeric phase
|
||||
std::vector<mjtNum> H(nnz, 0);
|
||||
mju_sqrMatTDSparseNumeric(
|
||||
H.data(), nc, H_rownnz.data(), H_rowadr.data(),
|
||||
H_colind.data(), out_diagind, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, rowsuperT, diag, d);
|
||||
|
||||
// densify
|
||||
dense_result.assign(nc * nc, 0);
|
||||
for (int r = 0; r < nc; r++) {
|
||||
for (int j = 0; j < H_rownnz[r]; j++) {
|
||||
int c = H_colind[H_rowadr[r] + j];
|
||||
dense_result[r*nc + c] = H[H_rowadr[r] + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse1) {
|
||||
// 0 0 0
|
||||
// M = 0 0 0
|
||||
@@ -378,29 +454,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse1) {
|
||||
int rownnzT[] = {3, 3, 3};
|
||||
int rowadrT[] = {0, 3, 6};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
|
||||
diagindH, data);
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -424,27 +484,12 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseLower) {
|
||||
int rownnzT[] = {3, 3, 3};
|
||||
int rowadrT[] = {0, 3, 6};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
|
||||
nullptr, data);
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 0);
|
||||
EXPECT_THAT(rownnzH, ElementsAre(1, 2, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 1, 3));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, nullptr);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(12, 0, 0, 0, 6, 0, 12, 3, 14));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 0, 0, 0, 1, 0, 0, 1, 2));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(1, 2, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(12, 0, 0, 0, 6, 0, 12, 3, 14));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -468,31 +513,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse2) {
|
||||
int rownnzT[] = {3, 3, 3};
|
||||
int rowadrT[] = {0, 3, 6};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
|
||||
diagindH, data);
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(12, 0, 12, 0, 6, 3, 12, 3, 14));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(diagindH, ElementsAre(0, 4, 8));
|
||||
EXPECT_THAT(dense, ElementsAre(12, 0, 12, 0, 6, 3, 12, 3, 14));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -516,31 +543,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3) {
|
||||
int rownnzT[] = {2, 2, 0};
|
||||
int rowadrT[] = {0, 2, 4};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {2, 3, 4};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 0));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 4));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(66, 4, 0, 4, 35, 0, 0, 0, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 0, 2, 0, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 1));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(66, 4, 0, 4, 35, 0, 0, 0, 0));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -564,32 +575,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3b) {
|
||||
int rownnzT[] = {2, 2, 1};
|
||||
int rowadrT[] = {0, 2, 4};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {1, 1, 1};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 3, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 5));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(26, 2, 0, 2, 13, 12, 12, 16, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 2, 1, 2, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 3, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(diagindH, ElementsAre(0, 4, 7));
|
||||
EXPECT_THAT(dense, ElementsAre(26, 2, 0, 2, 13, 12, 0, 12, 16));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -613,32 +607,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse4) {
|
||||
int rownnzT[] = {2, 0, 2};
|
||||
int rowadrT[] = {0, 2, 2};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {2, 3, 4};
|
||||
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, diag,
|
||||
diagindH, data);
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 0, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 2));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(66, 4, 0, 0, 0, 0, 4, 35, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 2, 0, 1, 0, 0, 0, 2, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(66, 0, 4, 0, 0, 0, 4, 0, 35));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -662,30 +639,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse5) {
|
||||
int rownnzT[] = {2, 1, 1};
|
||||
int rowadrT[] = {0, 2, 3};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
|
||||
diagindH, data);
|
||||
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 5));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(5, 6, 4, 6, 9, 0, 4, 16, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 0, 0, 2, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(5, 6, 4, 6, 9, 0, 4, 0, 16));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -709,30 +669,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse6) {
|
||||
int rownnzT[] = {1, 1, 2};
|
||||
int rowadrT[] = {0, 1, 2};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
|
||||
diagindH, data);
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 3));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(1, 2, 0, 4, 0, 0, 2, 13, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 2, 0, 1, 0, 0, 0, 2, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(diagindH, ElementsAre(0, 3, 7));
|
||||
EXPECT_THAT(dense, ElementsAre(1, 0, 2, 0, 4, 0, 2, 0, 13));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -756,31 +699,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse7) {
|
||||
int rownnzT[] = {2, 2};
|
||||
int rowadrT[] = {0, 2};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0};
|
||||
int rowadrH[] = {0, 0};
|
||||
int diagindH[] = {0, 0};
|
||||
|
||||
mjtNum diag[] = {2, 3, 4};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 2, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
int diagindH[2];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 2, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 2);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 2, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(66, 4, 4, 35));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 1));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2));
|
||||
EXPECT_THAT(dense, ElementsAre(66, 4, 4, 35));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -803,31 +730,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse8) {
|
||||
int rownnzT[] = {2, 1, 1};
|
||||
int rowadrT[] = {0, 2, 3};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {2, 3};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 2, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 5));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 2, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(14, 18, 8, 18, 27, 0, 8, 32, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 0, 0, 2, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(14, 18, 8, 18, 27, 0, 8, 0, 32));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -851,31 +762,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse9) {
|
||||
int rownnzT[] = {3, 3, 3};
|
||||
int rowadrT[] = {0, 3, 6};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {2, 3, 4};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, nullptr, data, 1);
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, nullptr, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
nullptr, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(69, 77, 80, 77, 99, 108, 80, 108, 120));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(69, 77, 80, 77, 99, 108, 80, 108, 120));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -900,31 +795,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse10) {
|
||||
int rowadrT[] = {0, 3, 6};
|
||||
int rowsuperT[] = {2, 1, 0};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {1, 2, 1};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
rowsuperT, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(18, 17, 14, 17, 23, 19, 14, 19, 18));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(18, 17, 14, 17, 23, 19, 14, 19, 18));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -949,31 +828,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse11) {
|
||||
int rowadrT[] = {0, 1, 3};
|
||||
int rowsuperT[] = {0, 1, 0};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {1, 1, 1};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
|
||||
int diagindH[3];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 3);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
rowsuperT, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(1, 1, 1, 1, 10, 10, 1, 10, 10));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
|
||||
EXPECT_THAT(dense, ElementsAre(1, 1, 1, 1, 10, 10, 1, 10, 10));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -998,33 +861,16 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse12) {
|
||||
int rowadrT[] = {0, 1, 2, 4};
|
||||
int rowsuperT[] = {1, 0, 1, 0};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {1, 1, 1};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 4, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
|
||||
int diagindH[4];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 4, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(4, 4, 4, 4));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 4, 8, 12));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 4);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 4, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
rowsuperT, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH,
|
||||
EXPECT_THAT(dense,
|
||||
ElementsAre(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10, 10, 1, 1, 10, 10));
|
||||
EXPECT_THAT(colindH,
|
||||
ElementsAre(0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(4, 4, 4, 4));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 4, 8, 12));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -1049,35 +895,16 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse13) {
|
||||
int rowadrT[] = {0, 3, 6, 6, 6};
|
||||
int rowsuperT[] = {1, 0, 2, 1, 0};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0, 0, 0};
|
||||
|
||||
mjtNum diag[] = {1, 1, 1};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 5, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
|
||||
int diagindH[5];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 3, 5, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 0, 0, 0));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 4, 4, 4));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 5);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 5, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
rowsuperT, data, diagindH);
|
||||
|
||||
EXPECT_THAT(matH, ElementsAre(3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0,
|
||||
3, 0, 0, 0, 0, 4, 0, 0, 0, 0));
|
||||
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 1, 1, 1));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 5, 10, 15, 20));
|
||||
EXPECT_THAT(dense, ElementsAre(3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
@@ -1100,40 +927,305 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse14) {
|
||||
int rowadrT[] = {0, 1, 2, 3, 4, 5, 6};
|
||||
int rowsuperT[] = {3, 2, 1, 0, 2, 1, 0};
|
||||
|
||||
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int rownnzH[] = {0, 0, 0, 0, 0, 0, 0};
|
||||
int rowadrH[] = {0, 0, 0, 0, 0, 0, 0};
|
||||
int diagindH[] = {0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
// test precount
|
||||
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 7, rownnz, rowadr, colind,
|
||||
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(7, 7, 7, 7, 7, 7, 7));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 7, 14, 21, 28, 35, 42));
|
||||
|
||||
// test computation
|
||||
mju_sqrMatTDUncompressedInit(rowadrH, 7);
|
||||
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 1, 7, rownnzH, rowadrH, colindH,
|
||||
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
|
||||
rowsuperT, data, diagindH);
|
||||
int diagindH[7];
|
||||
std::vector<mjtNum> dense;
|
||||
SqrMatTDSplitCol(dense, 1, 7, mat, rownnz, rowadr, colind,
|
||||
matT, rownnzT, rowadrT, colindT, rowsuperT, nullptr,
|
||||
diagindH, data);
|
||||
|
||||
EXPECT_THAT(
|
||||
matH, ElementsAre(1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2,
|
||||
2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 2, 2, 2,
|
||||
2, 4, 4, 4, 2, 2, 2, 2, 4, 4, 4));
|
||||
EXPECT_THAT(colindH,
|
||||
ElementsAre(0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3,
|
||||
4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0,
|
||||
1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6));
|
||||
dense, ElementsAre(1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1,
|
||||
1, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 4, 4,
|
||||
4, 2, 2, 2, 2, 4, 4, 4, 2, 2, 2, 2, 4, 4, 4));
|
||||
|
||||
EXPECT_THAT(rownnzH, ElementsAre(7, 7, 7, 7, 7, 7, 7));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(0, 7, 14, 21, 28, 35, 42));
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseSymbolic) {
|
||||
// Simple dense 2x2 matrix:
|
||||
// 1 2
|
||||
// M = 3 4
|
||||
//
|
||||
// M'M (lower triangle) should have 3 elements: (0,0), (1,0), (1,1)
|
||||
|
||||
mjModel* model = LoadModelFromString("<mujoco/>");
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
// M in CSR: row 0 has cols 0,1; row 1 has cols 0,1
|
||||
int colind[] = {0, 1, 0, 1};
|
||||
int rownnz[] = {2, 2};
|
||||
int rowadr[] = {0, 2};
|
||||
|
||||
// compute transpose using mju_transposeSparse
|
||||
mjtNum mat[] = {1, 2, 3, 4};
|
||||
mjtNum matT[4];
|
||||
int colindT[4];
|
||||
int rownnzT[2];
|
||||
int rowadrT[2];
|
||||
mju_transposeSparse(matT, mat, 2, 2, rownnzT, rowadrT, colindT, nullptr,
|
||||
rownnz, rowadr, colind);
|
||||
|
||||
// use old function as ground truth
|
||||
int rownnzH_expected[] = {0, 0};
|
||||
int rowadrH_expected[] = {0, 0};
|
||||
int nnz_expected = mju_sqrMatTDSparseCount(
|
||||
rownnzH_expected, rowadrH_expected, 2, rownnz, rowadr, colind, rownnzT,
|
||||
rowadrT, colindT, nullptr, data, /*flg_upper=*/0);
|
||||
|
||||
// verify: lower triangle should have 3 elements: (0,0), (1,0), (1,1)
|
||||
EXPECT_EQ(nnz_expected, 3);
|
||||
EXPECT_THAT(rownnzH_expected, ElementsAre(1, 2));
|
||||
EXPECT_THAT(rowadrH_expected, ElementsAre(0, 1));
|
||||
|
||||
// test count mode of new function
|
||||
int rownnzH[] = {0, 0};
|
||||
int rowadrH[] = {0, 0};
|
||||
|
||||
int nnz = mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, nullptr, nullptr,
|
||||
2, 2, rownnz, rowadr, colind, rownnzT,
|
||||
rowadrT, colindT, nullptr, data);
|
||||
|
||||
EXPECT_EQ(nnz, nnz_expected);
|
||||
EXPECT_THAT(rownnzH, ElementsAre(rownnzH_expected[0], rownnzH_expected[1]));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(rowadrH_expected[0], rowadrH_expected[1]));
|
||||
|
||||
// test fill mode
|
||||
std::vector<int> colindH(nnz, -1);
|
||||
|
||||
mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, colindH.data(), nullptr, 2, 2,
|
||||
rownnz, rowadr, colind, rownnzT, rowadrT,
|
||||
colindT, nullptr, data);
|
||||
|
||||
// verify: row 0 should have {0}, row 1 should have {0, 1}
|
||||
EXPECT_THAT(colindH, ElementsAre(0, 0, 1));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseSymbolicUpper) {
|
||||
// Test flg_upper=1: count both lower and upper triangle
|
||||
// Same matrix as previous test
|
||||
|
||||
mjModel* model = LoadModelFromString("<mujoco/>");
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
int colind[] = {0, 1, 0, 1};
|
||||
int rownnz[] = {2, 2};
|
||||
int rowadr[] = {0, 2};
|
||||
|
||||
mjtNum mat[] = {1, 2, 3, 4};
|
||||
mjtNum matT[4];
|
||||
int colindT[4];
|
||||
int rownnzT[2];
|
||||
int rowadrT[2];
|
||||
mju_transposeSparse(matT, mat, 2, 2, rownnzT, rowadrT, colindT, nullptr,
|
||||
rownnz, rowadr, colind);
|
||||
|
||||
// use old function as ground truth with flg_upper=1
|
||||
int rownnzH_expected[] = {0, 0};
|
||||
int rowadrH_expected[] = {0, 0};
|
||||
int nnz_expected = mju_sqrMatTDSparseCount(
|
||||
rownnzH_expected, rowadrH_expected, 2, rownnz, rowadr, colind, rownnzT,
|
||||
rowadrT, colindT, nullptr, data, /*flg_upper=*/1);
|
||||
|
||||
// test new function with diagind (upper triangle)
|
||||
int rownnzH[] = {0, 0};
|
||||
int rowadrH[] = {0, 0};
|
||||
int diagindH[] = {0, 0};
|
||||
int nnz = mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, nullptr, diagindH,
|
||||
2, 2, rownnz, rowadr, colind, rownnzT,
|
||||
rowadrT, colindT, nullptr, data);
|
||||
|
||||
EXPECT_EQ(nnz, nnz_expected);
|
||||
EXPECT_THAT(rownnzH, ElementsAre(rownnzH_expected[0], rownnzH_expected[1]));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(rowadrH_expected[0], rowadrH_expected[1]));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseSymbolicSupernode) {
|
||||
// Test supernode exploitation with a matrix that has supernodes
|
||||
// M has two rows with identical sparsity pattern
|
||||
|
||||
mjModel* model = LoadModelFromString("<mujoco/>");
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
// 3x2 matrix where rows 1 and 2 have same pattern
|
||||
// 1 0
|
||||
// M = 2 3
|
||||
// 4 5
|
||||
int colind[] = {0, 0, 1, 0, 1};
|
||||
int rownnz[] = {1, 2, 2};
|
||||
int rowadr[] = {0, 1, 3};
|
||||
|
||||
mjtNum mat[] = {1, 2, 3, 4, 5};
|
||||
mjtNum matT[5];
|
||||
int colindT[5];
|
||||
int rownnzT[2];
|
||||
int rowadrT[2];
|
||||
mju_transposeSparse(matT, mat, 3, 2, rownnzT, rowadrT, colindT, nullptr,
|
||||
rownnz, rowadr, colind);
|
||||
|
||||
// compute rowsuperT
|
||||
int rowsuperT[2];
|
||||
mju_superSparse(2, rowsuperT, rownnzT, rowadrT, colindT);
|
||||
|
||||
// use old function as ground truth
|
||||
int rownnzH_expected[] = {0, 0};
|
||||
int rowadrH_expected[] = {0, 0};
|
||||
int nnz_expected = mju_sqrMatTDSparseCount(
|
||||
rownnzH_expected, rowadrH_expected, 2, rownnz, rowadr, colind, rownnzT,
|
||||
rowadrT, colindT, rowsuperT, data, /*flg_upper=*/0);
|
||||
|
||||
// test new function with supernodes
|
||||
int rownnzH[] = {0, 0};
|
||||
int rowadrH[] = {0, 0};
|
||||
int nnz = mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, nullptr, nullptr,
|
||||
3, 2, rownnz, rowadr, colind, rownnzT,
|
||||
rowadrT, colindT, rowsuperT, data);
|
||||
|
||||
EXPECT_EQ(nnz, nnz_expected);
|
||||
EXPECT_THAT(rownnzH, ElementsAre(rownnzH_expected[0], rownnzH_expected[1]));
|
||||
EXPECT_THAT(rowadrH, ElementsAre(rowadrH_expected[0], rowadrH_expected[1]));
|
||||
|
||||
// test fill mode with supernodes
|
||||
std::vector<int> colindH(nnz, -1);
|
||||
mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, colindH.data(), nullptr, 3, 2,
|
||||
rownnz, rowadr, colind, rownnzT, rowadrT,
|
||||
colindT, rowsuperT, data);
|
||||
|
||||
// verify all filled
|
||||
for (int i = 0; i < nnz; i++) {
|
||||
EXPECT_GE(colindH[i], 0) << "colindH[" << i << "] not filled";
|
||||
}
|
||||
|
||||
// verify numeric phase with supernodes
|
||||
std::vector<mjtNum> resH(nnz);
|
||||
mjtNum diag[] = {1, 1, 1, 1, 1}; // dummy diagonal
|
||||
mju_sqrMatTDSparseNumeric(resH.data(), 2, rownnzH, rowadrH, colindH.data(),
|
||||
nullptr, mat, rownnz, rowadr, colind, matT, rownnzT,
|
||||
rowadrT, colindT, rowsuperT, diag, data);
|
||||
|
||||
// ground truth numeric
|
||||
std::vector<mjtNum> res_expected(4);
|
||||
std::vector<int> colindH_expected(4);
|
||||
int rownnzH_exp[] = {0, 0};
|
||||
int rowadrH_exp[] = {0, 2};
|
||||
mju_sqrMatTDSparse(res_expected.data(), mat, matT, diag, 3, 2, rownnzH_exp,
|
||||
rowadrH_exp, colindH_expected.data(), rownnz, rowadr,
|
||||
colind, nullptr, rownnzT, rowadrT, colindT, rowsuperT,
|
||||
data, nullptr);
|
||||
|
||||
// compare values (sparse result vs sparse ground truth)
|
||||
for (int r = 0; r < 2; r++) {
|
||||
for (int i = 0; i < rownnzH[r]; i++) {
|
||||
// find matching col in ground truth
|
||||
int c = colindH[rowadrH[r] + i];
|
||||
mjtNum val = resH[rowadrH[r] + i];
|
||||
|
||||
bool found = false;
|
||||
for (int j = 0; j < rownnzH_exp[r]; j++) {
|
||||
if (colindH_expected[rowadrH_exp[r] + j] == c) {
|
||||
EXPECT_NEAR(val, res_expected[rowadrH_exp[r] + j], 1e-14);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found) << "Column " << c
|
||||
<< " not found in ground truth for row " << r;
|
||||
}
|
||||
}
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseNumeric) {
|
||||
// Test numeric phase using symbolic phase + existing function as ground truth
|
||||
// 1 2
|
||||
// M = 3 4
|
||||
|
||||
mjModel* model = LoadModelFromString("<mujoco/>");
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
int colind[] = {0, 1, 0, 1};
|
||||
int rownnz[] = {2, 2};
|
||||
int rowadr[] = {0, 2};
|
||||
mjtNum mat[] = {1, 2, 3, 4};
|
||||
|
||||
// compute transpose
|
||||
mjtNum matT[4];
|
||||
int colindT[4];
|
||||
int rownnzT[2];
|
||||
int rowadrT[2];
|
||||
mju_transposeSparse(matT, mat, 2, 2, rownnzT, rowadrT, colindT, nullptr,
|
||||
rownnz, rowadr, colind);
|
||||
|
||||
// compute supernodes
|
||||
int rowsuperT[2];
|
||||
mju_superSparse(2, rowsuperT, rownnzT, rowadrT, colindT);
|
||||
|
||||
mjtNum diag[] = {2, 3}; // diagonal weighting matrix
|
||||
|
||||
// test both diagind cases: lower-only (diagind=NULL) and both triangles
|
||||
// (diagind!=NULL)
|
||||
for (int use_diagind = 0; use_diagind <= 1; use_diagind++) {
|
||||
// compute sparsity pattern using symbolic phase
|
||||
int rownnzH[] = {0, 0};
|
||||
int rowadrH[] = {0, 0};
|
||||
int diagindH[] = {0, 0};
|
||||
int nnz = mju_sqrMatTDSparseSymbolic(
|
||||
rownnzH, rowadrH, nullptr, use_diagind ? diagindH : nullptr, 2, 2,
|
||||
rownnz, rowadr, colind, rownnzT, rowadrT, colindT, nullptr, data);
|
||||
|
||||
std::vector<int> colindH(nnz);
|
||||
mju_sqrMatTDSparseSymbolic(
|
||||
rownnzH, rowadrH, colindH.data(), use_diagind ? diagindH : nullptr, 2,
|
||||
2, rownnz, rowadr, colind, rownnzT, rowadrT, colindT, nullptr, data);
|
||||
|
||||
// compute values using numeric phase
|
||||
std::vector<mjtNum> resH(nnz);
|
||||
mju_sqrMatTDSparseNumeric(resH.data(), 2, rownnzH, rowadrH,
|
||||
colindH.data(), use_diagind ? diagindH : nullptr,
|
||||
mat, rownnz, rowadr, colind, matT, rownnzT,
|
||||
rowadrT, colindT, rowsuperT, diag, data);
|
||||
|
||||
// compute ground truth using existing mju_sqrMatTDSparse
|
||||
// use uncompressed storage to give the old function enough room
|
||||
std::vector<mjtNum> res_expected(4); // 2x2 uncompressed
|
||||
std::vector<int> colindH_expected(4);
|
||||
int rownnzH_exp[] = {0, 0};
|
||||
int rowadrH_exp[] = {0, 2};
|
||||
int diagind_exp[] = {0, 0};
|
||||
mju_sqrMatTDSparse(res_expected.data(), mat, matT, diag, 2, 2, rownnzH_exp,
|
||||
rowadrH_exp, colindH_expected.data(), rownnz, rowadr,
|
||||
colind, nullptr, rownnzT, rowadrT, colindT, nullptr,
|
||||
data, use_diagind ? diagind_exp : nullptr);
|
||||
|
||||
// check that rownnz matches (nnz may differ due to compressed vs
|
||||
// uncompressed storage)
|
||||
EXPECT_EQ(rownnzH[0], rownnzH_exp[0])
|
||||
<< "rownnz[0] mismatch for use_diagind=" << use_diagind;
|
||||
EXPECT_EQ(rownnzH[1], rownnzH_exp[1])
|
||||
<< "rownnz[1] mismatch for use_diagind=" << use_diagind;
|
||||
|
||||
// compare column indices and values for each row
|
||||
for (int r = 0; r < 2; r++) {
|
||||
for (int j = 0; j < rownnzH[r]; j++) {
|
||||
int idx = rowadrH[r] + j;
|
||||
int idx_exp = rowadrH_exp[r] + j;
|
||||
EXPECT_EQ(colindH[idx], colindH_expected[idx_exp])
|
||||
<< "colind mismatch at row " << r << " pos " << j
|
||||
<< " for use_diagind=" << use_diagind;
|
||||
EXPECT_NEAR(resH[idx], res_expected[idx_exp], 1e-10)
|
||||
<< "value mismatch at row " << r << " pos " << j
|
||||
<< " for use_diagind=" << use_diagind;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
|
||||
@@ -101,7 +101,6 @@ TEST_F(MjvSceneTest, UpdateSceneGeomsExhausted) {
|
||||
mjv_updateScene(model, data, &opt_, &pert_, &cam_, mjCAT_ALL, &scn_);
|
||||
EXPECT_EQ(scn_.status, 1);
|
||||
EXPECT_EQ(scn_.ngeom, maxgeoms);
|
||||
EXPECT_EQ(data->warning[mjWARN_VGEOMFULL].number, 1);
|
||||
|
||||
mj_deleteData(data);
|
||||
FreeSceneObjects();
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<!-- This model leads to bad contacts from mjc_BoxBox -->
|
||||
|
||||
<default>
|
||||
<geom rgba="1 1 1 1" margin="1e-3" gap="1e-3"/>
|
||||
<geom rgba="1 1 1 1" gap="1e-3"/>
|
||||
<site type="sphere" rgba="0 0 0 0" size="0.001"/>
|
||||
</default>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<body pos="0 0 .3">
|
||||
<freejoint/>
|
||||
<geom type="sphere" size=".05" rgba=".5 .5 .5 .5" margin="0.1" gap="0.1"/>
|
||||
<geom type="sphere" size=".05" rgba=".5 .5 .5 .5" gap="0.1"/>
|
||||
</body>
|
||||
|
||||
<body pos="-.5 0 .3">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<mujoco>
|
||||
<option>
|
||||
<flag contact="disable"/>
|
||||
</option>
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 -1 1"/>
|
||||
|
||||
<!-- body 1: pendulum on world hinge, horizontal pointing right -->
|
||||
<body name="upper" pos="0 0 1">
|
||||
<joint name="hinge1" type="hinge" axis="0 1 0"/>
|
||||
<geom type="capsule" size="0.03" fromto="0 0 0 0.5 0 0"/>
|
||||
<site name="tip1" pos="0.5 0 0"/>
|
||||
</body>
|
||||
|
||||
<!-- body 2: planar free body (slide x, slide z, hinge y) -->
|
||||
<body name="lower" pos="0.5 0 1" euler="0 -90 0">
|
||||
<joint type="slide" axis="1 0 0"/>
|
||||
<joint type="slide" axis="0 0 1"/>
|
||||
<joint type="hinge" axis="0 1 0"/>
|
||||
<geom type="capsule" size="0.04" fromto="0 0 0 0.4 0 0"/>
|
||||
<site name="tip2" pos="0 0 0"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<equality>
|
||||
<connect body1="upper" body2="lower" anchor="0.5 0 0"/>
|
||||
</equality>
|
||||
</mujoco>
|
||||
@@ -0,0 +1,29 @@
|
||||
<mujoco>
|
||||
<option integrator="implicit">
|
||||
<flag contact="disable"/>
|
||||
</option>
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 -1 1"/>
|
||||
|
||||
<!-- body 1: pendulum on world ball joint, tilted -->
|
||||
<body name="upper">
|
||||
<joint type="ball"/>
|
||||
<geom type="box" size="0.03" fromto="0 0 0 0.5 0 0"/>
|
||||
<geom type="box" size="0.03" fromto="0.5 0 0 0.5 0.3 0"/>
|
||||
<site name="tip1" pos="0.5 0.3 0"/>
|
||||
</body>
|
||||
|
||||
<!-- body 2: free body, positioned at tip of upper -->
|
||||
<body name="lower" pos="0.5 0.3 0">
|
||||
<freejoint/>
|
||||
<geom type="box" size="0.04" fromto="0 0 0 0.5 0 0"/>
|
||||
<geom type="box" size="0.04" fromto="0.5 0 0 0.5 0.3 0"/>
|
||||
<site name="tip2" pos="0 0 0"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<equality>
|
||||
<connect site1="tip1" site2="tip2"/>
|
||||
</equality>
|
||||
</mujoco>
|
||||
@@ -0,0 +1,29 @@
|
||||
<mujoco>
|
||||
<option integrator="implicit">
|
||||
<flag contact="disable"/>
|
||||
</option>
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 -1 1"/>
|
||||
|
||||
<!-- body 1: pendulum on world ball joint, tilted -->
|
||||
<body name="upper">
|
||||
<joint type="ball"/>
|
||||
<geom type="box" size="0.03" fromto="0 0 0 0.5 0 0"/>
|
||||
<geom type="box" size="0.03" fromto="0.5 0 0 0.5 0.3 0"/>
|
||||
<site name="tip1" pos="0.5 0.3 0"/>
|
||||
</body>
|
||||
|
||||
<!-- body 2: free body, positioned at tip of upper -->
|
||||
<body name="lower" pos="0.5 0.3 0">
|
||||
<freejoint/>
|
||||
<geom type="box" size="0.04" fromto="0 0 0 0.5 0 0"/>
|
||||
<geom type="box" size="0.04" fromto="0.5 0 0 0.5 0.3 0"/>
|
||||
<site name="tip2" pos="0 0 0"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<equality>
|
||||
<weld site1="tip1" site2="tip2" solimp="0 0.96 0.01" torquescale="0.1"/>
|
||||
</equality>
|
||||
</mujoco>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="motor1" pos="0 0.1 0">
|
||||
<joint name="slide1" type="slide" axis="0 0 1"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
<body name="motor2" pos="0 0.2 0">
|
||||
<joint name="slide2" type="slide" axis="0 0 1"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
<body name="motor3" pos="0 0.3 0">
|
||||
<joint name="slide3" type="slide" axis="0 0 1"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
<body name="motor4" pos="0 0.4 0">
|
||||
<joint name="joint4"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
<body name="motor5" pos="0 0.5 0">
|
||||
<joint name="slide5" type="slide" axis="0 0 1"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
<body name="motor6" pos="0 0.6 0">
|
||||
<joint name="slide6" type="slide" axis="0 0 1"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
<body name="motor7" pos="0 0.7 0">
|
||||
<joint name="slide7" type="slide" axis="0 0 1"/>
|
||||
<geom size=".03"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<actuator>
|
||||
<!-- DC motor back-EMF stateless damping -->
|
||||
<dcmotor name="dc_bias" joint="slide1" motorconst="2.0" resistance="0.5"/>
|
||||
|
||||
<!-- DC motor velocity controller damping -->
|
||||
<dcmotor name="dc_vel" joint="slide2" motorconst="1.0" resistance="1.0" input="velocity" controller="0 5"/>
|
||||
|
||||
<!-- DC motor position controller damping -->
|
||||
<dcmotor name="dc_pos" joint="slide3" motorconst="1.0" resistance="1.0" input="position" controller="10 0 5"/>
|
||||
|
||||
<!-- DC motor with LuGre friction (sigma1 micro-damping) -->
|
||||
<dcmotor name="dc_lugre" joint="joint4" motorconst="0.05" resistance="2.0"
|
||||
damping="0.001" lugre="1e4 100 0.005 0.008 0.1"/>
|
||||
|
||||
<!-- Stateful current, voltage mode (back-EMF only through act_dot) -->
|
||||
<dcmotor name="dc_stateful_v" joint="slide5"
|
||||
motorconst="2.0" resistance="0.5" inductance="0 0.001"/>
|
||||
|
||||
<!-- Stateful current, position mode (controller + back-EMF through act_dot) -->
|
||||
<dcmotor name="dc_stateful_pos" joint="slide6"
|
||||
motorconst="1.0" resistance="1.0" inductance="0 0.001"
|
||||
input="position" controller="10 0 5"/>
|
||||
|
||||
<!-- Stateful current, velocity mode -->
|
||||
<dcmotor name="dc_stateful_vel" joint="slide7"
|
||||
motorconst="1.0" resistance="1.0" inductance="0 0.001"
|
||||
input="velocity" controller="5 0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
+1
-1
@@ -43,7 +43,7 @@
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 0 3"/>
|
||||
<geom name="floor" type="plane" size="4 4 .1" margin="0.01" gap="0.005"/>
|
||||
<geom name="floor" type="plane" size="4 4 .1" margin="0.005" gap="0.005"/>
|
||||
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05" rgba="0 0 1 1"/>
|
||||
<body name="head" pos="0 0 .7" gravcomp="0.5">
|
||||
<geom type="ellipsoid" size=".2 .2 .4" density="200" material="grid"/>
|
||||
|
||||
@@ -172,6 +172,8 @@ TEST_F(MjcPhysicsSceneTest, TestDefaults) {
|
||||
mjDSBL_EULERDAMP);
|
||||
EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(AutoResetFlag,
|
||||
mjDSBL_AUTORESET);
|
||||
EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(MultiCCDFlag,
|
||||
mjDSBL_MULTICCD);
|
||||
|
||||
EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(OverrideFlag,
|
||||
mjENBL_OVERRIDE);
|
||||
@@ -179,8 +181,6 @@ TEST_F(MjcPhysicsSceneTest, TestDefaults) {
|
||||
EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(FwdinvFlag, mjENBL_FWDINV);
|
||||
EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(InvDiscreteFlag,
|
||||
mjENBL_INVDISCRETE);
|
||||
EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(MultiCCDFlag,
|
||||
mjENBL_MULTICCD);
|
||||
|
||||
mj_deleteModel(default_model);
|
||||
mj_deleteSpec(empty_spec);
|
||||
|
||||
+10
-3
@@ -251,10 +251,17 @@ mjtNum CompareModel(const mjModel* m1, const mjModel* m2,
|
||||
|
||||
// compare arrays, apart from bvh-related ones (which includes flex_vert0), as
|
||||
// those are sensitive to numerical differences when meshes are perfectly
|
||||
// symmetric.
|
||||
// symmetric. Also skip flex fields derived from node local positions and
|
||||
// cell geometry that are not fully serialized to XML.
|
||||
#define X(type, name, nr, nc) \
|
||||
if (strncmp(#name, "bvh_", 4) && strncmp(#name, "flex_vert0", 4) && \
|
||||
strncmp(#name, "mesh_poly", 4)) { \
|
||||
if (strncmp(#name, "bvh_", 4) && \
|
||||
strncmp(#name, "flex_vert", 9) && \
|
||||
strncmp(#name, "mesh_poly", 9) && \
|
||||
strcmp(#name, "flex_centered") && \
|
||||
strcmp(#name, "flex_size") && \
|
||||
strcmp(#name, "flexedge_length0") && \
|
||||
strcmp(#name, "flexedge_invweight0") && \
|
||||
strncmp(#name, "flex_node", 9)) { \
|
||||
for (int r = 0; r < m1->nr; r++) { \
|
||||
for (int c = 0; c < nc; c++) { \
|
||||
dif = Compare(m1->name[r * nc + c], m2->name[r * nc + c]); \
|
||||
|
||||
@@ -16,5 +16,4 @@ if(MUJOCO_BUILD_EXAMPLES)
|
||||
include(ShellTests)
|
||||
|
||||
add_mujoco_shell_test(compile_test compile)
|
||||
add_mujoco_shell_test(testspeed_test testspeed)
|
||||
endif()
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 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.
|
||||
|
||||
MODEL_DIRS=(
|
||||
"${CMAKE_SOURCE_DIR}/model"
|
||||
"${CMAKE_SOURCE_DIR}/test"
|
||||
)
|
||||
|
||||
die() { echo "$*" 1>&2 ; exit 1; }
|
||||
|
||||
test_model() {
|
||||
local EXPECTED_STR='Simulation time'
|
||||
local model="$1"
|
||||
echo "Testing $model" >&2
|
||||
|
||||
local iterations=10
|
||||
# for particularly slow models, only run 2 steps under ASAN, or skip.
|
||||
if [[ ${TESTSPEED_ASAN:-0} != 0 ]]; then
|
||||
if [[ "$model" == */humanoid/100_humanoids.xml ||
|
||||
"$model" == */composite/particle.xml ||
|
||||
"$model" == */replicate/bunnies.xml ||
|
||||
"$model" == */replicate/leaves.xml ||
|
||||
"$model" == */replicate/particle.xml ||
|
||||
"$model" == */perf/*
|
||||
]]; then
|
||||
# these tests can take several minutes under ASAN
|
||||
return 0
|
||||
fi
|
||||
if [[ "$model" == */benchmark/testdata/humanoid200.xml ||
|
||||
"$model" == */engine/testdata/collision_convex/stacked_boxes.xml ||
|
||||
"$model" == */user/testdata/shark_22_ascii_fTetWild.xml ||
|
||||
"$model" == */user/testdata/shark_22_binary_fTetWild.xml
|
||||
]]; then
|
||||
iterations=2
|
||||
fi
|
||||
fi
|
||||
|
||||
# run testspeed, writing its output to stderr.
|
||||
# die if testspeed returns a failure code, or if it doesn't have the string
|
||||
# "Simulation time" in the output.
|
||||
("$TARGET_BINARY" "$model" "$iterations" || die "testspeed failed") \
|
||||
| tee >(cat 1>&2) | grep -q "$EXPECTED_STR"
|
||||
|
||||
if [ "$?" != 0 ]; then
|
||||
die "Expected string not found in output ($EXPECTED_STR)."
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "$TARGET_BINARY" ]; then
|
||||
die "Expecting environment variable TARGET_BINARY."
|
||||
fi
|
||||
|
||||
if [ -z "$MUJOCO_DLL_DIR" ]; then
|
||||
# Extend PATH to include the directory containing the mujoco DLL.
|
||||
# This is needed on Windows.
|
||||
PATH=$PATH:$MUJOCO_DLL_DIR
|
||||
fi
|
||||
|
||||
shopt -s globstar
|
||||
for model_dir in ${MODEL_DIRS[@]}; do
|
||||
echo "Looking in $model_dir"
|
||||
for model in $model_dir/**/*.xml; do
|
||||
if [[ $(basename $model) == malformed* ]]; then
|
||||
echo "Skipping $model" >&2
|
||||
continue
|
||||
fi
|
||||
if [[ $(basename $model) == *_fail.xml ]]; then
|
||||
echo "Skipping $model" >&2
|
||||
continue
|
||||
fi
|
||||
if grep -q "plugin" $model; then
|
||||
continue
|
||||
fi
|
||||
test_model "$model"
|
||||
done
|
||||
done
|
||||
|
||||
cd $CURRENT_DIR
|
||||
echo "PASS"
|
||||
Vendored
+4
@@ -1,4 +1,8 @@
|
||||
<mujoco>
|
||||
<option>
|
||||
<flag multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<visual>
|
||||
<global elevation="-20"/>
|
||||
</visual>
|
||||
|
||||
Vendored
+3
-1
@@ -1,5 +1,7 @@
|
||||
<mujoco>
|
||||
<option timestep="0.01"/>
|
||||
<option timestep="0.01">
|
||||
<flag multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<worldbody>
|
||||
<body name="body">
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
<mujoco model="flex">
|
||||
<option timestep="0.01" integrator="implicitfast">
|
||||
<flag island="enable"/>
|
||||
<flag island="enable" multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<asset>
|
||||
|
||||
Vendored
+4
@@ -2,6 +2,10 @@
|
||||
The frustum should match exactly the fron face of the box. -->
|
||||
|
||||
<mujoco>
|
||||
<option>
|
||||
<flag multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<visual>
|
||||
<map znear="0.01"/>
|
||||
<rgba frustum="1. 1. 0 0.2"/>
|
||||
|
||||
Vendored
+3
-1
@@ -22,7 +22,9 @@
|
||||
Simple hammock, implemented as a 2D grid composite, pinned at the corners.
|
||||
-->
|
||||
|
||||
<option timestep="0.001" solver="CG" iterations="30" tolerance="1e-6"/>
|
||||
<option timestep="0.001" solver="CG" iterations="30" tolerance="1e-6">
|
||||
<flag multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<size memory="20M"/>
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
||||
<mujoco>
|
||||
<option jacobian="dense" density="1.225" viscosity="1.8e-5" wind="0 0 1">
|
||||
<flag fwdinv="enable" energy="enable" island="enable"/>
|
||||
<flag fwdinv="enable" energy="enable" island="enable" multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<asset>
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 0 3"/>
|
||||
<geom name="floor" type="plane" size="4 4 .1" margin="0.01" gap="0.005"/>
|
||||
<geom name="floor" type="plane" size="4 4 .1" margin="0.005" gap="0.005"/>
|
||||
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05" rgba="0 0 1 1"/>
|
||||
<body name="head" pos="0 0 .7" gravcomp="0.5">
|
||||
<geom type="ellipsoid" size=".2 .2 .4" density="200" material="grid"/>
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
<mujoco>
|
||||
<option>
|
||||
<flag island="enable"/>
|
||||
<flag island="enable" multiccd="enable"/>
|
||||
</option>
|
||||
|
||||
<default>
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
|
||||
mujoco_test(
|
||||
user_model_test
|
||||
PROPERTIES
|
||||
ENVIRONMENT
|
||||
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
|
||||
ADDITIONAL_LINK_LIBRARIES absl::str_format
|
||||
)
|
||||
|
||||
@@ -31,16 +28,10 @@ mujoco_test(
|
||||
|
||||
mujoco_test(
|
||||
user_flex_test
|
||||
PROPERTIES
|
||||
ENVIRONMENT
|
||||
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
|
||||
)
|
||||
|
||||
mujoco_test(
|
||||
user_mesh_test
|
||||
PROPERTIES
|
||||
ENVIRONMENT
|
||||
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
|
||||
ADDITIONAL_LINK_LIBRARIES absl::str_format
|
||||
)
|
||||
|
||||
@@ -49,3 +40,5 @@ mujoco_test(user_composite_test)
|
||||
mujoco_test(user_resource_test)
|
||||
|
||||
mujoco_test(user_vfs_test)
|
||||
|
||||
mujoco_test(user_util_test)
|
||||
|
||||
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture file="test-pattern.png" type="2d"/>
|
||||
<texture name="white" type="2d" builtin="flat" rgb1="1 1 1" width="2" height="2"/>
|
||||
<texture name="grid" type="2d" builtin="checker" rgb1="1 1 1" rgb2="0.9 0.9 0.9" width="64" height="64"/>
|
||||
|
||||
<material name="pattern" texture="test-pattern"/>
|
||||
<material name="floor" reflectance="0.5" texture="grid" texuniform="true"/>
|
||||
<material name="metal" metallic="1.0" roughness="0.0">
|
||||
<layer role="rgb" texture="white"/>
|
||||
<layer role="metallic" texture="white"/>
|
||||
<layer role="roughness" texture="white"/>
|
||||
</material>
|
||||
|
||||
<mesh name="icosahedron"
|
||||
vertex="0 1 1.618
|
||||
0 -1 1.618
|
||||
0 1 -1.618
|
||||
0 -1 -1.618
|
||||
1 1.618 0
|
||||
-1 1.618 0
|
||||
1 -1.618 0
|
||||
-1 -1.618 0
|
||||
1.618 0 1
|
||||
1.618 0 -1
|
||||
-1.618 0 1
|
||||
-1.618 0 -1"/>
|
||||
<hfield name="hfield" nrow="3" ncol="3" size="1 1 0.1 0.1"
|
||||
elevation="1 0 1
|
||||
0 1 0
|
||||
1 0 1"/>
|
||||
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom name="ground" type="plane" size="12 8 0.1" pos="0 0 -1" material="floor"/>
|
||||
|
||||
<geom type="box" rgba="0.3 0.6 0.9 1.0" size="1 1 1" pos="-9 0 0"/>
|
||||
<geom type="sphere" rgba="0.3 0.6 0.9 1.0" size="1" pos="-6 0 0"/>
|
||||
<geom type="ellipsoid" rgba="0.3 0.6 0.9 1.0" size="1 2 1" pos="0 0 0"/>
|
||||
<geom type="capsule" rgba="0.3 0.6 0.9 1.0" size="1 1" pos="-3 0 1"/>
|
||||
<geom type="cylinder" rgba="0.3 0.6 0.9 1.0" size="1 1" pos="3 0 0"/>
|
||||
<geom type="hfield" hfield="hfield" rgba="0.3 0.6 0.9 1.0" pos="6 0 -0.5"/>
|
||||
<geom type="sdf" mesh="icosahedron" rgba="0.3 0.6 0.9 1.0" pos="9 0 0.809"/>
|
||||
|
||||
<geom type="box" material="pattern" size="1 1 1" pos="-9 5 0"/>
|
||||
<geom type="sphere" material="pattern" size="1" pos="-6 5 0"/>
|
||||
<geom type="ellipsoid" material="pattern" size="1 2 1" pos="0 5 0"/>
|
||||
<geom type="capsule" material="pattern" size="1 1" pos="-3 5 1"/>
|
||||
<geom type="cylinder" material="pattern" size="1 1" pos="3 5 0"/>
|
||||
<geom type="hfield" hfield="hfield" material="pattern" pos="6 5 -0.5"/>
|
||||
<geom type="sdf" mesh="icosahedron" material="pattern" pos="9 5 0.809"/>
|
||||
|
||||
<geom type="box" material="metal" size="1 1 1" pos="-9 -5 0"/>
|
||||
<geom type="sphere" material="metal" size="1" pos="-6 -5 0"/>
|
||||
<geom type="ellipsoid" material="metal" size="1 2 1" pos="0 -5 0"/>
|
||||
<geom type="capsule" material="metal" size="1 1" pos="-3 -5 1"/>
|
||||
<geom type="cylinder" material="metal" size="1 1" pos="3 -5 0"/>
|
||||
<geom type="hfield" hfield="hfield" material="metal" pos="6 -5 -0.5"/>
|
||||
<geom type="sdf" mesh="icosahedron" material="metal" pos="9 -5 0.809"/>
|
||||
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -19,6 +19,7 @@
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem> // NOLINT
|
||||
#include <functional>
|
||||
#include <map>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include "src/cc/array_safety.h"
|
||||
#include <mujoco/mujoco.h>
|
||||
#include <mujoco/mjspec.h>
|
||||
#include <mujoco/mjplugin.h>
|
||||
#include "src/xml/xml_api.h"
|
||||
#include "src/xml/xml_numeric_format.h"
|
||||
#include "test/fixture.h"
|
||||
@@ -173,6 +175,166 @@ TEST_F(MujocoTest, TreeTraversal) {
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, AttachAndChildDeletion) {
|
||||
mjSpec* child_spec = mj_makeSpec();
|
||||
mjsBody* child_world = mjs_findBody(child_spec, "world");
|
||||
mjsBody* child_body = mjs_addBody(child_world, 0);
|
||||
mjsJoint* freejoint = mjs_addJoint(child_body, 0);
|
||||
freejoint->type = mjJNT_FREE;
|
||||
mjs_setName(freejoint->element, "child_freejoint");
|
||||
|
||||
mjSpec* parent_spec = mj_makeSpec();
|
||||
mjsBody* parent_world = mjs_findBody(parent_spec, "world");
|
||||
mjsBody* parent_body = mjs_addBody(parent_world, 0);
|
||||
|
||||
// Attach child spec to parent_body
|
||||
mjsElement* attached =
|
||||
mjs_attach(parent_body->element, child_spec->element, "pre_", "");
|
||||
ASSERT_THAT(attached, NotNull());
|
||||
|
||||
// Delete freejoint from child_spec, should fail because it is attached
|
||||
int result = mjs_delete(child_spec, freejoint->element);
|
||||
EXPECT_EQ(result, -1);
|
||||
|
||||
// The freejoint should still be in parent_spec because deletion failed
|
||||
mjsElement* found_joint =
|
||||
mjs_findElement(parent_spec, mjOBJ_JOINT, "pre_child_freejoint");
|
||||
EXPECT_THAT(found_joint, NotNull());
|
||||
|
||||
mj_deleteSpec(child_spec);
|
||||
mj_deleteSpec(parent_spec);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, OriginSpecInvariantToAttachment) {
|
||||
mjSpec* child_spec = mj_makeSpec();
|
||||
mjsBody* child_world = mjs_findBody(child_spec, "world");
|
||||
mjsBody* child_body = mjs_addBody(child_world, 0);
|
||||
mjsJoint* freejoint = mjs_addJoint(child_body, 0);
|
||||
freejoint->type = mjJNT_FREE;
|
||||
mjs_setName(freejoint->element, "child_freejoint");
|
||||
|
||||
mjSpec* parent_spec = mj_makeSpec();
|
||||
mjsBody* parent_world = mjs_findBody(parent_spec, "world");
|
||||
mjsBody* parent_body = mjs_addBody(parent_world, 0);
|
||||
mjs_setName(parent_body->element, "parent_body");
|
||||
|
||||
// Attach child spec to parent_body
|
||||
mjsElement* attached =
|
||||
mjs_attach(parent_body->element, child_spec->element, "pre_", "");
|
||||
ASSERT_THAT(attached, NotNull());
|
||||
|
||||
// The freejoint should still be in parent_spec because deletion failed
|
||||
mjsElement* child_spec_joint =
|
||||
mjs_findElement(parent_spec, mjOBJ_JOINT, "pre_child_freejoint");
|
||||
EXPECT_EQ(mjs_getSpec(child_spec_joint), parent_spec);
|
||||
EXPECT_EQ(mjs_getOriginSpec(child_spec_joint), child_spec);
|
||||
|
||||
mjsElement* parent_spec_body =
|
||||
mjs_findElement(parent_spec, mjOBJ_BODY, "parent_body");
|
||||
EXPECT_EQ(mjs_getOriginSpec(parent_spec_body), parent_spec);
|
||||
|
||||
|
||||
mj_deleteSpec(child_spec);
|
||||
mj_deleteSpec(parent_spec);
|
||||
}
|
||||
|
||||
int open_mock(mjResource* resource) {
|
||||
static const char parent_xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="parent_body"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
resource->data = mju_malloc(sizeof(parent_xml));
|
||||
std::strcpy((char*)resource->data, parent_xml);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int read_mock(mjResource* resource, const void** buffer) {
|
||||
*buffer = resource->data;
|
||||
return std::strlen((const char*)resource->data);
|
||||
}
|
||||
|
||||
void close_mock(mjResource* resource) {
|
||||
mju_free(resource->data);
|
||||
resource->data = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, AttachedSpecDoesNotInheritURI) {
|
||||
// This test checks that when we attach a child spec to a parent spec that was
|
||||
// loaded from a resource provider, the child spec does not inherit the
|
||||
// resource URI from the parent. This allows the child spec to specify assets
|
||||
// relative to its model file or in the VFS.
|
||||
mjpResourceProvider provider = {
|
||||
.prefix = "fakeprovider",
|
||||
.open = open_mock,
|
||||
.read = read_mock,
|
||||
.close = close_mock,
|
||||
};
|
||||
|
||||
mjp_registerResourceProvider(&provider);
|
||||
|
||||
std::array<char, 1024> err;
|
||||
mjSpec* parent_spec =
|
||||
mj_parseXML("fakeprovider:parent.xml", nullptr, err.data(), err.size());
|
||||
mjs_setString(parent_spec->modelname, "parent");
|
||||
ASSERT_THAT(parent_spec, NotNull()) << err.data();
|
||||
|
||||
// Create child spec
|
||||
static constexpr char child_xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="child_body">
|
||||
<geom type="mesh" mesh="asset"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<asset>
|
||||
<mesh name="asset" file="asset.obj"/>
|
||||
</asset>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
// Setup VFS with asset
|
||||
mjVFS vfs;
|
||||
mj_defaultVFS(&vfs);
|
||||
static constexpr char asset_data[] = R"(
|
||||
v 0 0 0
|
||||
v 1 0 0
|
||||
v 0 1 0
|
||||
v 0 0 1
|
||||
f 1 2 3
|
||||
f 1 2 4
|
||||
f 2 3 4
|
||||
f 3 1 4
|
||||
)";
|
||||
mj_addBufferVFS(&vfs, "asset.obj", asset_data, sizeof(asset_data));
|
||||
|
||||
mjSpec* child_spec =
|
||||
mj_parseXMLString(child_xml, &vfs, err.data(), err.size());
|
||||
mjs_setString(child_spec->modelname, "child");
|
||||
ASSERT_THAT(child_spec, NotNull()) << err.data();
|
||||
|
||||
// Attach child spec to parent spec's world body
|
||||
mjsBody* world = mjs_findBody(parent_spec, "world");
|
||||
ASSERT_THAT(world, NotNull());
|
||||
|
||||
mjsElement* attached =
|
||||
mjs_attach(world->element, child_spec->element, "", "");
|
||||
ASSERT_THAT(attached, NotNull());
|
||||
|
||||
mjModel* model = mj_compile(parent_spec, &vfs);
|
||||
mj_deleteVFS(&vfs);
|
||||
|
||||
EXPECT_THAT(model, NotNull()) << mjs_getError(parent_spec);
|
||||
|
||||
if (model) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
mj_deleteSpec(parent_spec);
|
||||
mj_deleteSpec(child_spec);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, ActivatePlugin) {
|
||||
mjSpec* spec = mj_makeSpec();
|
||||
mjs_activatePlugin(spec, "mujoco.elasticity.cable");
|
||||
@@ -239,6 +401,96 @@ TEST_F(MujocoTest, DeletePlugin) {
|
||||
mj_deleteModel(newmodel);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, SetToDCMotorNullable) {
|
||||
mjSpec* spec = mj_makeSpec();
|
||||
mjsActuator* actuator = mjs_addActuator(spec, 0);
|
||||
|
||||
double motorconst[2] = {0.05, 0.05};
|
||||
double resistance = 2.0;
|
||||
|
||||
const char* err = mjs_setToDCMotor(actuator, motorconst, resistance,
|
||||
nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr,
|
||||
nullptr, 0);
|
||||
EXPECT_STREQ(err, "");
|
||||
EXPECT_EQ(actuator->gainprm[0], 2.0);
|
||||
EXPECT_EQ(actuator->gainprm[1], 0.05);
|
||||
EXPECT_EQ(actuator->gainprm[4], 0);
|
||||
EXPECT_EQ(actuator->gainprm[5], 0);
|
||||
EXPECT_EQ(actuator->gainprm[6], 0);
|
||||
EXPECT_EQ(actuator->dynprm[7], 0);
|
||||
EXPECT_EQ(actuator->dynprm[8], 0);
|
||||
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, SetToDCMotorDeriveKe) {
|
||||
mjSpec* spec = mj_makeSpec();
|
||||
mjsActuator* actuator = mjs_addActuator(spec, 0);
|
||||
|
||||
double resistance = 2.0;
|
||||
double nominal[3] = {12.0, 0, 100.0}; // vn=12, omega0=100
|
||||
|
||||
const char* err = mjs_setToDCMotor(actuator, nullptr, resistance,
|
||||
nominal, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr,
|
||||
nullptr, 0);
|
||||
EXPECT_STREQ(err, "");
|
||||
EXPECT_EQ(actuator->gainprm[0], 2.0);
|
||||
EXPECT_NEAR(actuator->gainprm[1], 0.12, 1e-5);
|
||||
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, SetToDCMotorFull) {
|
||||
mjSpec* spec = mj_makeSpec();
|
||||
mjsActuator* actuator = mjs_addActuator(spec, 0);
|
||||
|
||||
double motorconst[2] = {0.05, 0.05};
|
||||
double resistance = 2.0;
|
||||
double saturation[3] = {1.0, 2.0, 3.0};
|
||||
double controller[6] = {10.0, 20.0, 30.0, 40.0, 50.0, 60.0};
|
||||
|
||||
const char* err = mjs_setToDCMotor(actuator, motorconst, resistance,
|
||||
nullptr, saturation, nullptr,
|
||||
nullptr, controller, nullptr,
|
||||
nullptr, 0);
|
||||
EXPECT_STREQ(err, "");
|
||||
EXPECT_EQ(actuator->gainprm[0], 2.0); // resistance
|
||||
EXPECT_EQ(actuator->gainprm[1], 0.05); // K
|
||||
EXPECT_EQ(actuator->gainprm[4], 10.0); // kp
|
||||
EXPECT_EQ(actuator->gainprm[5], 20.0); // ki
|
||||
EXPECT_EQ(actuator->gainprm[6], 30.0); // kd
|
||||
EXPECT_EQ(actuator->dynprm[7], 40.0); // slewmax
|
||||
EXPECT_EQ(actuator->dynprm[8], 50.0); // Imax
|
||||
EXPECT_EQ(actuator->gainprm[7], 60.0); // Vmax
|
||||
EXPECT_EQ(actuator->dynprm[1], 3.0); // (di/dt)_max
|
||||
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
|
||||
TEST_F(MujocoTest, SetToDCMotorLuGre) {
|
||||
mjSpec* spec = mj_makeSpec();
|
||||
mjsActuator* actuator = mjs_addActuator(spec, 0);
|
||||
|
||||
double motorconst[2] = {0.05, 0.05};
|
||||
double resistance = 2.0;
|
||||
double lugre[5] = {100.0, 1.0, 0.5, 0.7, 10.0};
|
||||
|
||||
const char* err = mjs_setToDCMotor(actuator, motorconst, resistance,
|
||||
nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr,
|
||||
lugre, 0);
|
||||
EXPECT_STREQ(err, "");
|
||||
EXPECT_EQ(actuator->dynprm[5], 100.0); // stiffness
|
||||
EXPECT_EQ(actuator->dynprm[6], 1.0); // damping
|
||||
EXPECT_EQ(actuator->biasprm[3], 0.5); // coulomb
|
||||
EXPECT_EQ(actuator->biasprm[4], 0.7); // static
|
||||
EXPECT_EQ(actuator->biasprm[5], 10.0); // stribeck
|
||||
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
|
||||
static constexpr char xml_plugin_1[] = R"(
|
||||
<mujoco model="MuJoCo Model">
|
||||
<worldbody>
|
||||
|
||||
+398
-6
@@ -79,6 +79,27 @@ TEST_F(UserFlexTest, CountTooSmall) {
|
||||
EXPECT_THAT(error.data(), HasSubstr("Count too small"));
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, CellnumZeroInterpolated) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="b0"/>
|
||||
<body name="b1"/>
|
||||
<body name="b2"/>
|
||||
<body name="b3"/>
|
||||
</worldbody>
|
||||
<deformable>
|
||||
<flex name="test" cellcount="2 2 0" dof="trilinear"
|
||||
dim="3" body="b0 b1 b2 b3" element="0 1 2 3"/>
|
||||
</deformable>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(m, IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("cellcount cannot be 0"));
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, SpacingGreaterThanGeometry) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
@@ -371,9 +392,9 @@ TEST_F(UserFlexTest, TrilinearInterpolation) {
|
||||
|
||||
EXPECT_EQ(m1->nflexvert, m2->nflexvert);
|
||||
for (int i = 0; i < 3*m1->nflexvert; ++i) {
|
||||
EXPECT_EQ(m1->flex_vert[i], d2->flexvert_xpos[i]);
|
||||
EXPECT_EQ(m1->flex_vert0[i], m2->flex_vert0[i]);
|
||||
EXPECT_EQ(d1->flexvert_xpos[i], d2->flexvert_xpos[i]);
|
||||
EXPECT_NEAR(m1->flex_vert[i], d2->flexvert_xpos[i], 1e-7);
|
||||
EXPECT_NEAR(m1->flex_vert0[i], m2->flex_vert0[i], 1e-7);
|
||||
EXPECT_NEAR(d1->flexvert_xpos[i], d2->flexvert_xpos[i], 1e-7);
|
||||
}
|
||||
|
||||
EXPECT_EQ(m1->nM, m2->nM);
|
||||
@@ -447,7 +468,7 @@ TEST_F(UserFlexTest, StiffnessMatrix) {
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
EXPECT_NE(m->flex_stiffness[0], 0);
|
||||
EXPECT_NE(m->flex_stiffness[m->flex_stiffnessadr[0]], 0);
|
||||
EXPECT_EQ(m->nflexnode, 8);
|
||||
|
||||
// constants are in the kernel
|
||||
@@ -456,7 +477,8 @@ TEST_F(UserFlexTest, StiffnessMatrix) {
|
||||
zeros[i] = 0;
|
||||
ones[i] = 1;
|
||||
}
|
||||
mju_mulMatVec(res, m->flex_stiffness, ones, 3*m->nflexnode, 3*m->nflexnode);
|
||||
mju_mulMatVec(res, m->flex_stiffness + m->flex_stiffnessadr[0], ones,
|
||||
3 * m->nflexnode, 3 * m->nflexnode);
|
||||
EXPECT_THAT(res, Pointwise(MjNear(1e-8, 1e-4), zeros));
|
||||
|
||||
mj_deleteModel(m);
|
||||
@@ -496,7 +518,8 @@ TEST_F(UserFlexTest, StiffnessCacheDiffersByGeometry) {
|
||||
|
||||
// Same number of nodes but different stiffness due to different geometry
|
||||
EXPECT_EQ(m_small->nflexnode, m_large->nflexnode);
|
||||
EXPECT_NE(m_small->flex_stiffness[0], m_large->flex_stiffness[0]);
|
||||
EXPECT_NE(m_small->flex_stiffness[m_small->flex_stiffnessadr[0]],
|
||||
m_large->flex_stiffness[m_large->flex_stiffnessadr[0]]);
|
||||
|
||||
mj_deleteModel(m_small);
|
||||
mj_deleteModel(m_large);
|
||||
@@ -1008,5 +1031,374 @@ TEST_F(UserFlexTest, FlexNoConstraintsWarning) {
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, EmptyCellNodePinning) {
|
||||
// A 2x2x2 grid with a box mesh that fills all cells.
|
||||
// No nodes should be pinned.
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="test" type="box" spacing=".1 .1 .1" dim="3"
|
||||
dof="trilinear" mass="1" cellcount="2 2 2">
|
||||
<contact selfcollide="none"/>
|
||||
<elasticity young="1"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
|
||||
// A 2x2x2 grid with trilinear order has (2+1)^3 = 27 node positions.
|
||||
int nadr = m->flex_nodeadr[0];
|
||||
int nnode = m->flex_nodenum[0];
|
||||
EXPECT_EQ(nnode, 27);
|
||||
|
||||
// All cells are occupied by the box, so no node should be pinned.
|
||||
int pinned = 0;
|
||||
for (int n = nadr; n < nadr + nnode; n++) {
|
||||
int bid = m->flex_nodebodyid[n];
|
||||
if (m->body_jntnum[bid] == 0) {
|
||||
pinned++;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(pinned, 0);
|
||||
|
||||
// Verify simulation works
|
||||
mjData* d = mj_makeData(m);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, EmptyCellNodePinningMesh) {
|
||||
// Load bunny_multicell.xml which has a 3x3x3 grid.
|
||||
// The bunny mesh only occupies some cells, so many nodes should be pinned.
|
||||
const std::string xml_path =
|
||||
GetModelPath("flex/bunny_multicell.xml");
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = mj_loadXML(xml_path.c_str(), 0, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
|
||||
// 3x3x3 grid, order=1: (3+1)^3 = 64 node positions
|
||||
int nadr = m->flex_nodeadr[0];
|
||||
int nnode = m->flex_nodenum[0];
|
||||
EXPECT_EQ(nnode, 64);
|
||||
|
||||
// Count pinned nodes (no joints)
|
||||
int pinned = 0;
|
||||
int free_nodes = 0;
|
||||
for (int n = nadr; n < nadr + nnode; n++) {
|
||||
int bid = m->flex_nodebodyid[n];
|
||||
if (m->body_jntnum[bid] == 0) {
|
||||
pinned++;
|
||||
} else {
|
||||
free_nodes++;
|
||||
}
|
||||
}
|
||||
|
||||
// At least some nodes should be pinned since the bunny doesn't fill all cells
|
||||
EXPECT_GT(pinned, 0) << "Expected some nodes to be pinned from empty cells";
|
||||
EXPECT_GT(free_nodes, 0) << "Expected some nodes to remain free";
|
||||
EXPECT_EQ(pinned + free_nodes, nnode);
|
||||
|
||||
// Verify the model can simulate
|
||||
mjData* d = mj_makeData(m);
|
||||
mj_forward(m, d);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
mj_deleteData(d);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, EmptyCellNodePinningQuadratic) {
|
||||
// Regression test for ci_min calculation with order=2.
|
||||
// A 2x1x1 quadratic grid has nodes at gi=0..4 (5 nodes per axis).
|
||||
// We place mesh vertices only in cell 0 (x in [0, 0.5]), so cell 1 is empty.
|
||||
//
|
||||
// Node gi=3 belongs only to cell 1 (1*2 <= 3 <= 2*2).
|
||||
// With the old formula (gi-order)/order = (3-2)/2 = 0, it would also check
|
||||
// cell 0 (non-empty), incorrectly marking gi=3 as non-pinned.
|
||||
// Single hex element at x=[0,0.3], well inside cell 0 of a 3x1x1 grid.
|
||||
// Anchor vertex at x=1.0 extends the bounding box to [0,1]^3.
|
||||
// The 3x1x1 quadratic grid splits at x=0.33, 0.67.
|
||||
// Cell 0 has vertices, cells 1 and 2 are empty.
|
||||
// Interior nodes for cells 1,2 should be pinned to the parent body.
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="parent">
|
||||
<freejoint/>
|
||||
<inertial mass="0.01" pos="0 0 0"
|
||||
diaginertia="0.001 0.001 0.001"/>
|
||||
<flexcomp name="test" type="direct" dim="3"
|
||||
dof="quadratic" mass="1" cellcount="3 1 1"
|
||||
point="0.0 0.0 0.0 0.3 0.0 0.0
|
||||
0.0 1.0 0.0 0.3 1.0 0.0
|
||||
0.0 0.0 1.0 0.3 0.0 1.0
|
||||
0.0 1.0 1.0 0.3 1.0 1.0
|
||||
1.0 0.5 0.5"
|
||||
element="0 1 3 2 4 5 7 6">
|
||||
<contact selfcollide="none"/>
|
||||
<elasticity young="1"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
|
||||
// 3x1x1 quadratic grid: (3*2+1) * (1*2+1) * (1*2+1) = 7*3*3 = 63 nodes
|
||||
int nadr = m->flex_nodeadr[0];
|
||||
int nnode = m->flex_nodenum[0];
|
||||
EXPECT_EQ(nnode, 63);
|
||||
|
||||
// Count pinned nodes: pinned nodes are assigned to the parent body.
|
||||
int parent_bid = mj_name2id(m, mjOBJ_BODY, "parent");
|
||||
ASSERT_GT(parent_bid, 0);
|
||||
int pinned = 0;
|
||||
for (int n = nadr; n < nadr + nnode; n++) {
|
||||
if (m->flex_nodebodyid[n] == parent_bid) {
|
||||
pinned++;
|
||||
}
|
||||
}
|
||||
|
||||
// Cells 1 and 2 are empty, so nodes exclusively in those cells are pinned.
|
||||
// Nodes at gi=3..6 (with any gj, gk) are only in cells 1 and/or 2.
|
||||
// That's 4 * 3 * 3 = 36 nodes.
|
||||
EXPECT_EQ(pinned, 36);
|
||||
|
||||
mj_deleteData(mj_makeData(m));
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, EmptyCellDetectsElements) {
|
||||
// A cube surface mesh (dim=2, 12 triangles) spanning [0,1]^3.
|
||||
// With cellcount="6 6 6" (216 cells), only 8 corner cells contain
|
||||
// mesh vertices.
|
||||
//
|
||||
// Bug: MarkEmptyCells only checked vertices, so 208/216 cells are
|
||||
// marked empty, causing most interior nodes to be incorrectly pinned.
|
||||
// Fix: check element AABBs to correctly identify occupied cells.
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="parent">
|
||||
<freejoint/>
|
||||
<inertial mass="0.01" pos="0.5 0.5 0.5"
|
||||
diaginertia="0.001 0.001 0.001"/>
|
||||
<flexcomp name="test" type="direct" dim="2"
|
||||
dof="trilinear" mass="1" cellcount="6 6 6"
|
||||
point="0 0 0 1 0 0 1 1 0 0 1 0
|
||||
0 0 1 1 0 1 1 1 1 0 1 1"
|
||||
element="0 1 2 0 2 3 4 6 5 4 7 6
|
||||
0 5 1 0 4 5 2 7 3 2 6 7
|
||||
0 3 7 0 7 4 1 5 6 1 6 2">
|
||||
<contact selfcollide="none"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
|
||||
// 6x6x6 trilinear grid: (6+1)^3 = 343 nodes
|
||||
int nadr = m->flex_nodeadr[0];
|
||||
int nnode = m->flex_nodenum[0];
|
||||
ASSERT_EQ(nnode, 343);
|
||||
|
||||
// Count pinned nodes: those assigned to the parent body.
|
||||
int parent_bid = mj_name2id(m, mjOBJ_BODY, "parent");
|
||||
ASSERT_GT(parent_bid, 0);
|
||||
int pinned = 0;
|
||||
for (int n = nadr; n < nadr + nnode; n++) {
|
||||
if (m->flex_nodebodyid[n] == parent_bid) {
|
||||
pinned++;
|
||||
}
|
||||
}
|
||||
|
||||
// The cube surface fills the entire bounding box. The element-AABB
|
||||
// marks all boundary cells as surface cells (152/216). The interior
|
||||
// flood-fill finds no exterior seeds (all boundary cells are surface),
|
||||
// so the remaining 64 cells are classified as interior (non-empty).
|
||||
// No cells are empty → 0 nodes pinned.
|
||||
EXPECT_EQ(pinned, 0);
|
||||
|
||||
mj_deleteData(mj_makeData(m));
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, TotalMassTrilinear) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="test" type="grid" count="2 2 2" spacing="1 1 1"
|
||||
dim="3" dof="trilinear" mass="1.5">
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
|
||||
double total_mass = 0;
|
||||
for (int i = 1; i < m->nbody; ++i) {
|
||||
total_mass += m->body_mass[i];
|
||||
}
|
||||
|
||||
EXPECT_NEAR(total_mass, 1.5, 1e-5);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, TotalMassQuadratic) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="test" type="grid" count="3 2 2" spacing="1 1 1"
|
||||
dim="3" dof="quadratic" mass="2.0">
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
|
||||
double total_mass = 0;
|
||||
for (int i = 1; i < m->nbody; ++i) {
|
||||
total_mass += m->body_mass[i];
|
||||
}
|
||||
|
||||
EXPECT_NEAR(total_mass, 2.0, 1e-5);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, Dof2d) {
|
||||
// 3x3 grid with dof="2d": 9 vertices, 2 DOFs each -> nv = 18
|
||||
static constexpr char xml_2d[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="test" type="grid" count="3 3 1" spacing=".1 .1 .1"
|
||||
dim="2" radius=".01" dof="2d">
|
||||
<edge equality="true"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
// same model with dof="full" for comparison: 9 vertices, 3 DOFs each -> nv = 27
|
||||
static constexpr char xml_full[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="test" type="grid" count="3 3 1" spacing=".1 .1 .1"
|
||||
dim="2" radius=".01">
|
||||
<edge equality="true"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
std::array<char, 1024> error;
|
||||
|
||||
// load 2d model
|
||||
mjModel* m_2d = LoadModelFromString(xml_2d, error.data(), error.size());
|
||||
ASSERT_THAT(m_2d, NotNull()) << error.data();
|
||||
mjData* d_2d = mj_makeData(m_2d);
|
||||
|
||||
// load full model
|
||||
mjModel* m_full = LoadModelFromString(xml_full, error.data(), error.size());
|
||||
ASSERT_THAT(m_full, NotNull()) << error.data();
|
||||
mjData* d_full = mj_makeData(m_full);
|
||||
|
||||
// verify DOF counts
|
||||
EXPECT_EQ(m_2d->nv, 18); // 9 vertices * 2 DOFs
|
||||
EXPECT_EQ(m_full->nv, 27); // 9 vertices * 3 DOFs
|
||||
|
||||
// same number of vertices and elements
|
||||
EXPECT_EQ(m_2d->nflexvert, m_full->nflexvert);
|
||||
EXPECT_EQ(m_2d->nflexelem, m_full->nflexelem);
|
||||
|
||||
// each body has 2 DOFs in 2d mode, 3 in full mode
|
||||
for (int i = 1; i < m_2d->nbody; i++) {
|
||||
EXPECT_EQ(m_2d->body_dofnum[i], 2) << "body " << i;
|
||||
}
|
||||
for (int i = 1; i < m_full->nbody; i++) {
|
||||
EXPECT_EQ(m_full->body_dofnum[i], 3) << "body " << i;
|
||||
}
|
||||
|
||||
// simulate a few steps to make sure nothing crashes
|
||||
for (int i = 0; i < 10; i++) {
|
||||
mj_step(m_2d, d_2d);
|
||||
mj_step(m_full, d_full);
|
||||
}
|
||||
|
||||
mj_deleteModel(m_2d);
|
||||
mj_deleteModel(m_full);
|
||||
mj_deleteData(d_2d);
|
||||
mj_deleteData(d_full);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, Vert0RotationInvariant) {
|
||||
// unrotated trilinear grid
|
||||
static constexpr char xml_unrotated[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="parent">
|
||||
<flexcomp name="test" type="grid" count="2 2 2" spacing="1 1 1"
|
||||
dim="3" dof="trilinear">
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
// same grid rotated 45 degrees around Z via parent body quaternion
|
||||
static constexpr char xml_rotated[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body name="parent" quat="0.9238795 0 0 0.3826834">
|
||||
<flexcomp name="test" type="grid" count="2 2 2" spacing="1 1 1"
|
||||
dim="3" dof="trilinear">
|
||||
<contact selfcollide="none" internal="false"/>
|
||||
</flexcomp>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m1 = LoadModelFromString(xml_unrotated, error.data(), error.size());
|
||||
ASSERT_THAT(m1, NotNull()) << error.data();
|
||||
|
||||
mjModel* m2 = LoadModelFromString(xml_rotated, error.data(), error.size());
|
||||
ASSERT_THAT(m2, NotNull()) << error.data();
|
||||
|
||||
// same number of vertices
|
||||
ASSERT_EQ(m1->nflexvert, m2->nflexvert);
|
||||
|
||||
// vert0 must be identical regardless of rotation
|
||||
for (int i = 0; i < 3 * m1->nflexvert; ++i) {
|
||||
EXPECT_NEAR(m1->flex_vert0[i], m2->flex_vert0[i], 1e-10)
|
||||
<< "vert0 mismatch at index " << i;
|
||||
}
|
||||
|
||||
mj_deleteModel(m1);
|
||||
mj_deleteModel(m2);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
|
||||
+129
-1
@@ -713,7 +713,7 @@ TEST_F(MjCMeshTest, AreaTooSmall) {
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, testing::IsNull());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("mesh surface area is too small"));
|
||||
}
|
||||
|
||||
@@ -819,6 +819,73 @@ TEST_F(MjCMeshTest, VolumeSmallAllowedShell) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, Flex2DElasticityRequiresPositiveThickness) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="f" type="grid" count="3 3 1" spacing="1 1 1" dim="2" dof="2d">
|
||||
<elasticity young="1" thickness="0" elastic2d="bend"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, testing::IsNull());
|
||||
EXPECT_THAT(error.data(),
|
||||
HasSubstr("2d elasticity requires positive thickness"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, InterpolatedFlexSupportsBendElasticityWithWarning) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="f" type="grid" count="3 3 2" spacing="1 1 1" dim="3" dof="trilinear">
|
||||
<contact selfcollide="none"/>
|
||||
<elasticity young="1" thickness="1" elastic2d="bend"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, testing::NotNull()) << error.data();
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, InterpolatedFlexSupportsBothElasticityWithWarning) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="f" type="grid" count="3 3 2" spacing="1 1 1" dim="3" dof="trilinear">
|
||||
<contact selfcollide="none"/>
|
||||
<elasticity young="1" thickness="1" elastic2d="both"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, testing::NotNull()) << error.data();
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, Flex2DElasticityRequires2DFlex) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="f" type="grid" count="3 3 3" spacing="1 1 1" dim="3" dof="2d">
|
||||
<elasticity young="1" thickness="1" elastic2d="bend"/>
|
||||
</flexcomp>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, testing::IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("2d elasticity requires 2d flex"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, VolumeNegativeThrowsError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
@@ -1318,6 +1385,63 @@ TEST_F(MjCMeshTest, QhullCache) {
|
||||
mj_deleteVFS(&vfs);
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, ColocatedMeshError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="example_mesh"
|
||||
vertex="0 0 0 0 0 0 0 0 0 0 0 0"
|
||||
face="0 1 2 1 3 2"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="example_mesh"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("colocated"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, CollinearMeshError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="example_mesh"
|
||||
vertex="0 0 0 1 0 0 2 0 0 3 0 0"
|
||||
face="0 1 2 1 3 2"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="example_mesh"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("collinear"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, CoplanarMeshError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="flat_quad"
|
||||
vertex="-5 -5 0 5 -5 0 -5 5 0 5 5 0"
|
||||
face="0 1 2 1 3 2"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="flat_quad"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("coplanar"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, LoadSkin) {
|
||||
const std::string xml_path = GetTestDataFilePath(kCubeSkinPath);
|
||||
std::array<char, 1024> error;
|
||||
@@ -1377,6 +1501,8 @@ TEST_F(MjCMeshTest, OctreeIsBalanced) {
|
||||
mjSpec* spec = mj_parseXML(xml_path.c_str(), 0, error.data(), error.size());
|
||||
mjsGeom* geom = mjs_asGeom(mjs_firstElement(spec, mjOBJ_GEOM));
|
||||
geom->type = mjGEOM_SDF;
|
||||
mjsMesh* mesh = mjs_asMesh(mjs_firstElement(spec, mjOBJ_MESH));
|
||||
mesh->octree_maxdepth = 5;
|
||||
mjModel* model = mj_compile(spec, 0);
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_GT(model->mesh_octnum[0], 0);
|
||||
@@ -1440,6 +1566,8 @@ TEST_F(MjCMeshTest, OctreeHangingNodeInterpolation) {
|
||||
mjSpec* spec = mj_parseXML(xml_path.c_str(), 0, error.data(), error.size());
|
||||
mjsGeom* geom = mjs_asGeom(mjs_firstElement(spec, mjOBJ_GEOM));
|
||||
geom->type = mjGEOM_SDF;
|
||||
mjsMesh* mesh = mjs_asMesh(mjs_firstElement(spec, mjOBJ_MESH));
|
||||
mesh->octree_maxdepth = 5;
|
||||
mjModel* model = mj_compile(spec, 0);
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_GT(model->mesh_octnum[0], 0);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "src/user/user_util.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -180,5 +182,142 @@ TEST_F(UserUtilTest, VectorToStringEmpty) {
|
||||
EXPECT_EQ(VectorToString(v), "");
|
||||
}
|
||||
|
||||
// utility: modified Gram-Schmidt to orthogonalize columns of Q (n x n)
|
||||
static void gramSchmidt(double* Q, int n) {
|
||||
for (int j = 0; j < n; j++) {
|
||||
// subtract projections onto previous columns
|
||||
for (int k = 0; k < j; k++) {
|
||||
double dot = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
dot += Q[i * n + j] * Q[i * n + k];
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
Q[i * n + j] -= dot * Q[i * n + k];
|
||||
}
|
||||
}
|
||||
// normalize
|
||||
double norm = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
norm += Q[i * n + j] * Q[i * n + j];
|
||||
}
|
||||
norm = std::sqrt(norm);
|
||||
for (int i = 0; i < n; i++) {
|
||||
Q[i * n + j] /= norm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// utility: compose SPD matrix A = Q * diag(eigvals) * Q^T
|
||||
static void composeMatrix(double* A, const double* Q,
|
||||
const double* eigvals, int n) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = 0; j <= i; j++) {
|
||||
double sum = 0;
|
||||
for (int k = 0; k < n; k++) {
|
||||
sum += Q[i * n + k] * eigvals[k] * Q[j * n + k];
|
||||
}
|
||||
A[i * n + j] = sum;
|
||||
A[j * n + i] = sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(UserUtilTest, EigendecomposeConvergence) {
|
||||
// seeded RNG for reproducibility
|
||||
std::mt19937_64 rng;
|
||||
rng.seed(42);
|
||||
std::normal_distribution<double> dist(0, 1);
|
||||
|
||||
// sweep over matrix sizes used by flex stiffness
|
||||
// order=1: 8 nodes * 3 dof = 24
|
||||
// order=2: 27 nodes * 3 dof = 81
|
||||
for (int n : {24, 81}) {
|
||||
int total_sweeps = 0;
|
||||
int max_sweeps = 0;
|
||||
int count = 0;
|
||||
|
||||
// generate random orthogonal matrix Q via Gram-Schmidt
|
||||
std::vector<double> Q(n * n);
|
||||
for (int i = 0; i < n * n; i++) {
|
||||
Q[i] = dist(rng);
|
||||
}
|
||||
gramSchmidt(Q.data(), n);
|
||||
|
||||
// sweep eigenvalue spectra of varying difficulty
|
||||
// well-separated, clustered, wide condition number
|
||||
for (double condition : {1e1, 1e3, 1e6}) {
|
||||
for (double cluster : {0.0, 0.5, 0.9}) {
|
||||
// construct eigenvalues
|
||||
std::vector<double> eigvals(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
// base: logarithmically spaced from 1 to condition
|
||||
double t = (double)i / (n - 1);
|
||||
double base = std::exp(t * std::log(condition));
|
||||
|
||||
// cluster: push eigenvalues toward geometric mean
|
||||
double mean = std::sqrt(condition);
|
||||
eigvals[i] = (1 - cluster) * base + cluster * mean;
|
||||
}
|
||||
|
||||
// compose A = Q * diag(eigvals) * Q^T
|
||||
std::vector<double> A(n * n);
|
||||
composeMatrix(A.data(), Q.data(), eigvals.data(), n);
|
||||
|
||||
// save copy for verification
|
||||
std::vector<double> A_copy(A);
|
||||
|
||||
// decompose
|
||||
std::vector<double> found_eigval(n);
|
||||
std::vector<double> found_eigvec(n * n);
|
||||
int sweeps = mjuu_eigendecompose(
|
||||
A.data(), found_eigval.data(),
|
||||
found_eigvec.data(), n);
|
||||
|
||||
total_sweeps += sweeps;
|
||||
if (sweeps > max_sweeps) max_sweeps = sweeps;
|
||||
count++;
|
||||
|
||||
// verify convergence
|
||||
EXPECT_LT(sweeps, 200)
|
||||
<< "n=" << n
|
||||
<< " condition=" << condition
|
||||
<< " cluster=" << cluster;
|
||||
|
||||
// verify A*v = lambda*v for each eigenpair
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int r = 0; r < n; r++) {
|
||||
double Av = 0;
|
||||
for (int c = 0; c < n; c++) {
|
||||
Av += A_copy[r * n + c] * found_eigvec[c * n + i];
|
||||
}
|
||||
double lv = found_eigval[i] * found_eigvec[r * n + i];
|
||||
EXPECT_NEAR(Av, lv,
|
||||
1e-6 * std::abs(found_eigval[i]))
|
||||
<< "n=" << n << " condition=" << condition
|
||||
<< " cluster=" << cluster
|
||||
<< " eigpair=" << i << " row=" << r;
|
||||
}
|
||||
}
|
||||
|
||||
// verify all eigenvalues are positive
|
||||
for (int i = 0; i < n; i++) {
|
||||
EXPECT_GT(found_eigval[i], 0)
|
||||
<< "n=" << n << " eigenvalue " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double mean_sweeps = (double)total_sweeps / count;
|
||||
|
||||
// assert reasonable average convergence
|
||||
EXPECT_LE(mean_sweeps, 20.0)
|
||||
<< "n=" << n << ": mean sweeps too high";
|
||||
|
||||
// assert max sweeps within budget
|
||||
EXPECT_LT(max_sweeps, 200)
|
||||
<< "n=" << n << ": max sweeps exceeded 200";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -224,6 +224,38 @@ TEST_F(UserVfsTest, DeleteFileRepeat) {
|
||||
mj_deleteVFS(&vfs);
|
||||
}
|
||||
|
||||
TEST_F(UserVfsTest, ContainsBuffer) {
|
||||
mjVFS vfs;
|
||||
mj_defaultVFS(&vfs);
|
||||
std::string buffer = "<mujoco/>";
|
||||
const void* ptr = static_cast<const void*>(buffer.c_str());
|
||||
mj_addBufferVFS(&vfs, "model", ptr, buffer.size());
|
||||
|
||||
EXPECT_TRUE(mj_containsBufferVFS(&vfs, "model"));
|
||||
EXPECT_FALSE(mj_containsBufferVFS(&vfs, "nonexistent"));
|
||||
EXPECT_FALSE(mj_containsBufferVFS(&vfs, "Model"));
|
||||
|
||||
mj_deleteVFS(&vfs);
|
||||
}
|
||||
|
||||
TEST_F(UserVfsTest, ContainsFile) {
|
||||
mjVFS vfs;
|
||||
mj_defaultVFS(&vfs);
|
||||
|
||||
constexpr char path[] = "engine/testdata/actuation/";
|
||||
const std::string dir = GetTestDataFilePath(path);
|
||||
std::string file = "activation.xml";
|
||||
mj_addFileVFS(&vfs, dir.c_str(), file.c_str());
|
||||
|
||||
EXPECT_TRUE(mj_containsFileVFS(&vfs, dir.c_str(), file.c_str()));
|
||||
EXPECT_TRUE(mj_containsFileVFS(&vfs, nullptr, (dir + file).c_str()));
|
||||
EXPECT_TRUE(mj_containsFileVFS(&vfs, nullptr, "Activation.xml"));
|
||||
EXPECT_TRUE(mj_containsFileVFS(&vfs, "some/dir/", "activation.xml"));
|
||||
EXPECT_FALSE(mj_containsFileVFS(&vfs, nullptr, "nonexistent.xml"));
|
||||
|
||||
mj_deleteVFS(&vfs);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(UserVfsTest, AddBuffer) {
|
||||
mjVFS vfs;
|
||||
|
||||
@@ -16,9 +16,6 @@ mujoco_test(xml_api_test)
|
||||
|
||||
mujoco_test(
|
||||
xml_native_reader_test
|
||||
PROPERTIES
|
||||
ENVIRONMENT
|
||||
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
|
||||
)
|
||||
|
||||
mujoco_test(xml_utils_test)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
mujoco_test(mjz_api_test)
|
||||
|
||||
mujoco_test(
|
||||
mjz_decoder_test
|
||||
)
|
||||
@@ -3003,6 +3003,426 @@ TEST_F(ActuatorParseTest, AdhesionInheritsFromGeneral) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorBasicParsing) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0" damping="1 2 3" armature="0.1"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_dyntype[0], mjDYN_DCMOTOR);
|
||||
EXPECT_EQ(model->actuator_gaintype[0], mjGAIN_DCMOTOR);
|
||||
EXPECT_EQ(model->actuator_biastype[0], mjBIAS_DCMOTOR);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 2.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], 0.05);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_damping[0], 1.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dampingpoly[0], 2.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dampingpoly[1], 3.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_armature[0], 0.1);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorNominalDerivation) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<!-- no damping: Ke = vn/omega0 -->
|
||||
<dcmotor joint="jnt" nominal="12 0.6 600"/>
|
||||
<!-- B > 0, R given: quadratic -->
|
||||
<dcmotor joint="jnt" nominal="12 0 600" resistance="0.4" damping="0.0001"/>
|
||||
<!-- B > 0, R from nominal: linear -->
|
||||
<dcmotor joint="jnt" nominal="12 0.6 600" damping="0.0001"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
|
||||
// actuator 0: B = 0, Ke = vn/omega0
|
||||
{
|
||||
double K = 12.0 / 600.0;
|
||||
double R = K * 12.0 / 0.6;
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0*mjNGAIN + 0], R);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0*mjNGAIN + 1], K);
|
||||
}
|
||||
|
||||
// actuator 1: B > 0, R given, quadratic Ke^2*omega0 - Ke*vn + R*B*omega0 = 0
|
||||
{
|
||||
double B = 0.0001, R = 0.4, vn = 12.0, omega0 = 600.0;
|
||||
double disc = vn*vn - 4*R*B*omega0*omega0;
|
||||
double Ke = (vn + sqrt(disc)) / (2*omega0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1*mjNGAIN + 0], R);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1*mjNGAIN + 1], Ke);
|
||||
}
|
||||
|
||||
// actuator 2: B > 0, R from nominal, Ke = vn/omega0 - vn*B/tau0
|
||||
{
|
||||
double B = 0.0001, vn = 12.0, tau0 = 0.6, omega0 = 600.0;
|
||||
double Ke = vn / omega0 - vn*B / tau0;
|
||||
double R = Ke * vn / tau0;
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[2*mjNGAIN + 0], R);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[2*mjNGAIN + 1], Ke);
|
||||
}
|
||||
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorSaturation) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
||||
saturation="1.5 0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_forcelimited[0], 1);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_forcerange[0], -1.5);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_forcerange[1], 1.5);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorInheritedDefaults) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<default>
|
||||
<dcmotor motorconst="1.0" resistance="1.0" controller="2.0 0.5 0.1 10.0 5.0 12.0"
|
||||
saturation="0 0 0" inductance="0 0.01" input="velocity"/>
|
||||
</default>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
|
||||
// check motorconst and resistance are overridden by instance
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], 0.05);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 2.0);
|
||||
|
||||
// check controller gains (kp, ki, kd) in gainprm[4:6]
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[4], 2.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[5], 0.5);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[6], 0.1);
|
||||
|
||||
// check controller limits (slewmax, Imax) in dynprm[7,8]
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[7], 10.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[8], 5.0);
|
||||
|
||||
// check Vmax in gainprm[7]
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[7], 12.0);
|
||||
|
||||
// check input mode in gainprm[8]
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[8], 2.0);
|
||||
|
||||
// check inductance (te) in dynprm[0]
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[0], 0.01);
|
||||
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorControllerFull) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
||||
controller="1.0 2.0 3.0 4.0 5.0 6.0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[4], 1.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[5], 2.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[6], 3.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[7], 4.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[8], 5.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[7], 6.0);
|
||||
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorLuGreRemapping) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0" damping="0.01"
|
||||
lugre="100 1 0.5 0.7 10"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[5], 100);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[6], 1);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_damping[0], 0.01);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_biasprm[3], 0.5);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_biasprm[4], 0.7);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_biasprm[5], 10);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorLuGreInheritedDefaults) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<default>
|
||||
<dcmotor motorconst="0.05" resistance="2.0" damping="0.01" lugre="100 1 0.5 0.7 10"/>
|
||||
</default>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_MJTNUM_EQ(model->actuator_damping[0], 0.01);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorActdimStateless) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_actnum[0], 0);
|
||||
EXPECT_EQ(model->actuator_actadr[0], -1);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorActdimCurrentOnly) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
||||
inductance="0.001 0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_actnum[0], 1);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[0], 0.001 / 2.0);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorActdimThermalOnly) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
||||
thermal="10 5 0 0 0 25"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_actnum[0], 1);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[2], 10);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[3], 5);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[4], 25);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorActdimLuGreOnly) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
||||
lugre="100 1 0.5 0.7 10"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_actnum[0], 1);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_dynprm[5], 100);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorActdimAllThree) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
||||
inductance="0.001 0"
|
||||
thermal="10 5 0 0 0 25"
|
||||
lugre="100 1 0.5 0.7 10"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_EQ(model->actuator_actnum[0], 3);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorMissingKError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" resistance="2.0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error.data(), HasSubstr("motor constant K must be positive"));
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorDefaultsPropagate) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<default>
|
||||
<dcmotor motorconst="0.03" resistance="1.5"/>
|
||||
</default>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 1.5);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], 0.03);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, DCMotorMotorconstGeometricMean) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body>
|
||||
<geom size="1"/>
|
||||
<joint name="jnt"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
<actuator>
|
||||
<dcmotor joint="jnt" motorconst="0.03 0.05" resistance="2.0"/>
|
||||
<dcmotor joint="jnt" motorconst="0.03" resistance="2.0"/>
|
||||
</actuator>
|
||||
</mujoco>
|
||||
)";
|
||||
std::array<char, 1024> error;
|
||||
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
|
||||
ASSERT_THAT(model, NotNull()) << error.data();
|
||||
double K = std::sqrt(0.03 * 0.05);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 2.0);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], K);
|
||||
EXPECT_MJTNUM_EQ(model->actuator_gainprm[mjNGAIN + 1], 0.03);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(ActuatorParseTest, ActdimDefaultsPropagate) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
|
||||
@@ -1404,7 +1404,10 @@ std::vector<std::string> GetWriteReadTestModels() {
|
||||
absl::StrContains(xml, "hfield_xml") ||
|
||||
absl::StrContains(xml, "fromto_convex") ||
|
||||
absl::StrContains(xml, "cube_skin") ||
|
||||
absl::StrContains(xml, "cube_3x3x3")) {
|
||||
absl::StrContains(xml, "cube_3x3x3") ||
|
||||
// exclude files that fail since we do not save pinned flex nodes
|
||||
absl::StrContains(xml, "gripper_trilinear") ||
|
||||
absl::StrContains(xml, "strain")) {
|
||||
continue;
|
||||
}
|
||||
models.push_back(xml);
|
||||
|
||||
Reference in New Issue
Block a user