Remove midpoint integration, superseded by free-body gyroscopic derivatives.
The gyroscopic (bias) derivatives applied to standalone free bodies by the implicitfast integrator provide comparable stability for spinning bodies, with none of midpoint's restrictions: they apply under contacts, fluid forces and constraints, and preserve the linear force-velocity relation required by discrete-time inverse dynamics. The invdiscrete flag reverts to its original single meaning and no longer affects forward dynamics. Restore implicitfast coverage in the DiscreteInverseMatch test, removed when midpoint made discrete inverse dynamics untestable. Add implicit gyroscopic (bias) derivatives for free bodies in implicitfast. The implicitfast integrator drops the RNE (bias) derivative to stay on the symmetric Cholesky path, so fast-spinning free bodies integrate gyroscopic forces explicitly and can gain energy. Symmetrizing the gyroscopic Jacobian is not an option: its stabilizing content is the antisymmetric part, and adding only the symmetric part is destabilizing. Instead, exploit the fact that for a standalone free body the 6x6 block of M - h*D is decoupled from the rest of the system (qDeriv sparsity is tree-local): after the global solve, rebuild the block with the exact bias derivative in closed form (mjd_freeBias_vel) and re-solve it with dense unsymmetric LU, overwriting the block's rows of qacc. For lone spinning bodies this makes implicitfast match implicit to rounding, at ~150ns per eligible body: cheaper than the midpoint machinery it will replace. Eligibility is structural only; contacts, fluid and constraints need no gating. The same block is mirrored in discrete inverse dynamics (mj_discreteAcc), making invdiscrete exact for spinning free bodies. PiperOrigin-RevId: 948472495 Change-Id: I813ef3d98c7b399881bc8603b9f9208cfb02eb58
This commit is contained in:
committed by
Copybara-Service
parent
b2106db52f
commit
f0fa3d8260
@@ -147,6 +147,84 @@ TEST_F(DerivativeTest, SmoothDvel) {
|
||||
}
|
||||
}
|
||||
|
||||
// mjd_freeBias_vel: 6x6 bias-derivative block for a standalone free body
|
||||
// validated against mjd_rne_vel and against finite-differenced mj_rne
|
||||
TEST_F(DerivativeTest, FreeBiasVel) {
|
||||
// free body with offset CoM, rotated inertia, non-identity orientation
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<body pos="0.1 -0.2 0.3" euler="20 -30 40">
|
||||
<freejoint/>
|
||||
<geom type="box" size=".1 .2 .3" mass="2" pos=".04 -.02 .03" euler="10 20 30"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
mjData* d = data.get();
|
||||
|
||||
// set fast, fully populated velocity
|
||||
mjtNum qvel[6] = {0.4, -0.3, 0.2, 5, -3, 2};
|
||||
mju_copy(d->qvel, qvel, 6);
|
||||
mj_forward(m, d);
|
||||
|
||||
// analytic block
|
||||
mjtNum B[36];
|
||||
mjd_freeBias_vel(m, d, /*jnt=*/0, B);
|
||||
|
||||
// linear columns are zero by construction
|
||||
for (int r = 0; r < 6; r++) {
|
||||
for (int c = 0; c < 3; c++) {
|
||||
EXPECT_EQ(B[6 * r + c], 0);
|
||||
}
|
||||
}
|
||||
|
||||
// compare with mjd_rne_vel: B == -(qDeriv(flg_bias=1) - qDeriv(flg_bias=0))
|
||||
mju_zero(d->qDeriv, m->nD);
|
||||
mjd_smooth_vel(m, d, /*flg_bias=*/1);
|
||||
vector<mjtNum> qDeriv_bias = AsVector(d->qDeriv, m->nD);
|
||||
mju_zero(d->qDeriv, m->nD);
|
||||
mjd_smooth_vel(m, d, /*flg_bias=*/0);
|
||||
for (int r = 0; r < 6; r++) {
|
||||
int rowadr = m->D_rowadr[r];
|
||||
ASSERT_EQ(m->D_rownnz[r], 6);
|
||||
for (int k = 0; k < 6; k++) {
|
||||
int c = m->D_colind[rowadr + k];
|
||||
mjtNum rne_val = -(qDeriv_bias[rowadr + k] - d->qDeriv[rowadr + k]);
|
||||
EXPECT_NEAR(B[6 * r + c], rne_val, MjTol(1e-14, 1e-6))
|
||||
<< "mismatch at (" << r << ", " << c << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// compare with central finite differences of mj_rne
|
||||
mjtNum eps = MjTol(1e-6, 1e-3);
|
||||
for (int c = 0; c < 6; c++) {
|
||||
mjtNum bias_plus[6], bias_minus[6];
|
||||
|
||||
d->qvel[c] = qvel[c] + eps;
|
||||
mj_comVel(m, d);
|
||||
mj_rne(m, d, /*flg_acc=*/0, bias_plus);
|
||||
|
||||
d->qvel[c] = qvel[c] - eps;
|
||||
mj_comVel(m, d);
|
||||
mj_rne(m, d, /*flg_acc=*/0, bias_minus);
|
||||
|
||||
d->qvel[c] = qvel[c];
|
||||
|
||||
for (int r = 0; r < 6; r++) {
|
||||
mjtNum fd = (bias_plus[r] - bias_minus[r]) / (2 * eps);
|
||||
EXPECT_NEAR(B[6 * r + c], fd, MjTol(1e-7, 1e-2))
|
||||
<< "FD mismatch at (" << r << ", " << c << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// disabled actuators do not contribute to d_qfrc_actuator/d_qvel
|
||||
TEST_F(DerivativeTest, DisabledActuators) {
|
||||
// model with only a position actuator
|
||||
|
||||
+230
-365
@@ -464,112 +464,149 @@ TEST_F(ImplicitIntegratorTest, EnergyConservation) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// Energy and angmom conservation for free body with implicitfast (IMR)
|
||||
TEST_F(ImplicitIntegratorTest, ConservationMidpoint) {
|
||||
// aligned: CoM at joint origin
|
||||
static constexpr char xml1[] = R"(
|
||||
// free-body local solve: implicitfast matches implicit exactly for a standalone
|
||||
// free body
|
||||
TEST_F(ImplicitIntegratorTest, FreeBodyMatchesImplicit) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.01">
|
||||
<flag energy="enable" gravity="disable"/>
|
||||
</option>
|
||||
<option timestep="0.005"/>
|
||||
<worldbody>
|
||||
<body>
|
||||
<freejoint/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1" euler="10 20 30"/>
|
||||
<body pos="0.1 -0.2 0.5" euler="20 -30 40">
|
||||
<joint type="free" damping="0.1"/>
|
||||
<geom type="box" size=".1 .2 .3" mass="2" pos=".04 -.02 .03" euler="10 20 30"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
// auto-aligned: CoM at joint origin
|
||||
static constexpr char xml2[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.01">
|
||||
<flag energy="enable" gravity="disable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<body>
|
||||
<freejoint align="true"/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1" euler="10 20 30" pos=".03 .02 .01"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr d1 = MakeData(model);
|
||||
MjDataPtr d2 = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
|
||||
// non-aligned: CoM offset from joint origin
|
||||
static constexpr char xml3[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.01">
|
||||
<flag energy="enable" gravity="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>
|
||||
)";
|
||||
int xml_idx = 1;
|
||||
for (auto xml : {xml1, xml2, xml3}) {
|
||||
SCOPED_TRACE(testing::Message() << "XML case " << xml_idx++);
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
// tumbling initial velocity
|
||||
mj_resetData(m, d1.get());
|
||||
d1->qvel[3] = 5;
|
||||
d1->qvel[4] = -3;
|
||||
d1->qvel[5] = 2;
|
||||
|
||||
const int nstep = 500;
|
||||
mjtNum energy_drift[2], angmom_drift[2]; // [0]=midpoint, [1]=rk4
|
||||
// step both integrators from identical states, re-synchronizing each step
|
||||
// to avoid chaotic divergence of tumbling trajectories
|
||||
int nstate = mj_stateSize(m, mjSTATE_INTEGRATION);
|
||||
std::vector<mjtNum> state(nstate);
|
||||
mjtNum tol = MjTol(1e-14, 1e-6);
|
||||
for (int i = 0; i < 50; i++) {
|
||||
mj_getState(m, d1.get(), state.data(), mjSTATE_INTEGRATION);
|
||||
mj_setState(m, d2.get(), state.data(), mjSTATE_INTEGRATION);
|
||||
|
||||
for (int integrator : {mjINT_IMPLICITFAST, mjINT_RK4}) {
|
||||
int idx = (integrator == mjINT_IMPLICITFAST) ? 0 : 1;
|
||||
model->opt.integrator = integrator;
|
||||
m->opt.integrator = mjINT_IMPLICITFAST;
|
||||
mj_step(m, d1.get());
|
||||
m->opt.integrator = mjINT_IMPLICIT;
|
||||
mj_step(m, d2.get());
|
||||
|
||||
// reset
|
||||
mj_resetData(model.get(), data.get());
|
||||
data->qvel[3] = 1.0;
|
||||
data->qvel[4] = 2.0;
|
||||
data->qvel[5] = 3.0;
|
||||
mj_forward(model.get(), data.get());
|
||||
mjtNum initial_energy = data->energy[1];
|
||||
mjtNum initial_angmom[3];
|
||||
mj_subtreeVel(model.get(), data.get());
|
||||
mju_copy3(initial_angmom, data->subtree_angmom);
|
||||
|
||||
for (int i = 0; i < nstep; i++) {
|
||||
mj_step(model.get(), data.get());
|
||||
}
|
||||
|
||||
energy_drift[idx] = fabs(data->energy[1] - initial_energy);
|
||||
mj_subtreeVel(model.get(), data.get());
|
||||
mjtNum angmom_err[3];
|
||||
mju_sub3(angmom_err, data->subtree_angmom, initial_angmom);
|
||||
angmom_drift[idx] = mju_norm3(angmom_err);
|
||||
for (int k = 0; k < m->nv; k++) {
|
||||
EXPECT_NEAR(d1->qvel[k], d2->qvel[k], tol)
|
||||
<< "step " << i << " dof " << k;
|
||||
}
|
||||
|
||||
// midpoint should conserve energy better than RK4 (double only)
|
||||
#ifndef mjUSESINGLE
|
||||
EXPECT_LT(energy_drift[0], energy_drift[1]);
|
||||
#endif
|
||||
|
||||
// both should conserve angular momentum well
|
||||
EXPECT_LT(angmom_drift[0], MjTol(1e-3, 1e-2));
|
||||
EXPECT_LT(angmom_drift[1], MjTol(1e-3, 1e-2));
|
||||
}
|
||||
}
|
||||
|
||||
// verify second-order convergence of midpoint integration
|
||||
TEST_F(ImplicitIntegratorTest, MidpointConvergenceOrder) {
|
||||
// aligned: CoM at joint origin
|
||||
static constexpr char xml1[] = R"(
|
||||
// free-body local solve: spinning free bodies do not gain energy in vacuum
|
||||
TEST_F(ImplicitIntegratorTest, FreeBodyGyroStable) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast">
|
||||
<flag gravity="disable"/>
|
||||
<option integrator="implicitfast" timestep="0.005">
|
||||
<flag energy="enable" gravity="disable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<body>
|
||||
<freejoint/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1" euler="10 20 30"/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
mjData* d = data.get();
|
||||
|
||||
// middle-axis tumble and fast principal-axis spin
|
||||
static constexpr mjtNum qvel0[2][3] = {{0.05, 5, 0.05}, {20, 0.05, 0.05}};
|
||||
|
||||
for (int c = 0; c < 2; c++) {
|
||||
SCOPED_TRACE(testing::Message() << "velocity case " << c);
|
||||
mj_resetData(m, d);
|
||||
mju_copy3(d->qvel + 3, qvel0[c]);
|
||||
mj_forward(m, d);
|
||||
mjtNum initial_energy = d->energy[1];
|
||||
|
||||
// 100 simulated seconds
|
||||
for (int i = 0; i < 20000; i++) {
|
||||
mj_step(m, d);
|
||||
ASSERT_LT(d->energy[1], 1.01 * initial_energy)
|
||||
<< "energy gain at step " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// free-body local solve: applies to bodies in contact
|
||||
TEST_F(ImplicitIntegratorTest, FreeBodyGyroStableContact) {
|
||||
// spinning ellipsoid on an inclined plane, as in gyroscopic.xml
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.002"/>
|
||||
<worldbody>
|
||||
<geom type="plane" size="5 5 .1" euler="0 15 0"/>
|
||||
<body pos="0 0 .2">
|
||||
<freejoint/>
|
||||
<geom type="ellipsoid" size=".05 .1 .15" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
mjData* d = data.get();
|
||||
|
||||
mj_resetData(m, d);
|
||||
d->qvel[3] = 30;
|
||||
mjtNum initial_speed = mju_norm(d->qvel, m->nv);
|
||||
|
||||
int ncon_total = 0;
|
||||
for (int i = 0; i < 5000; i++) {
|
||||
mj_step(m, d);
|
||||
ncon_total += d->ncon;
|
||||
ASSERT_LT(mju_norm(d->qvel, m->nv), 2 * initial_speed)
|
||||
<< "speed gain at step " << i;
|
||||
}
|
||||
|
||||
// the body was in contact while spinning
|
||||
EXPECT_GT(ncon_total, 1000);
|
||||
}
|
||||
|
||||
// free-body local solve: energy of a tumbling free body never increases and is
|
||||
// only mildly damped; angular momentum drift is bounded
|
||||
TEST_F(ImplicitIntegratorTest, FreeBodyConservation) {
|
||||
// aligned: CoM at joint origin
|
||||
static constexpr char xml1[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.01">
|
||||
<flag energy="enable" gravity="disable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<body>
|
||||
<freejoint/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
@@ -578,14 +615,13 @@ TEST_F(ImplicitIntegratorTest, MidpointConvergenceOrder) {
|
||||
// non-aligned: CoM offset from joint origin
|
||||
static constexpr char xml2[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast">
|
||||
<flag gravity="disable"/>
|
||||
<option integrator="implicitfast" timestep="0.01">
|
||||
<flag energy="enable" gravity="disable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<body>
|
||||
<freejoint/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1" euler="10 20 30"
|
||||
pos=".05 .03 .02"/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1" euler="10 20 30" pos=".03 .02 .01"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
@@ -597,302 +633,131 @@ TEST_F(ImplicitIntegratorTest, MidpointConvergenceOrder) {
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
mjData* d = data.get();
|
||||
|
||||
mjtNum T = 1.0;
|
||||
mjtNum h_coarse = 0.02;
|
||||
mjtNum quat_coarse[4], quat_fine[4], quat_ref[4];
|
||||
mj_resetData(m, d);
|
||||
d->qvel[3] = 1.0;
|
||||
d->qvel[4] = 2.0;
|
||||
d->qvel[5] = 3.0;
|
||||
mj_forward(m, d);
|
||||
mjtNum initial_energy = d->energy[1];
|
||||
mjtNum initial_angmom[3];
|
||||
mj_subtreeVel(m, d);
|
||||
mju_copy3(initial_angmom, d->subtree_angmom);
|
||||
|
||||
auto run = [&](mjtNum h, mjtNum quat_out[4]) {
|
||||
model->opt.timestep = h;
|
||||
MjDataPtr data = MakeData(model);
|
||||
for (int i = 0; i < 500; i++) {
|
||||
mj_step(m, d);
|
||||
|
||||
data->qvel[3] = 1.0;
|
||||
data->qvel[4] = 2.0;
|
||||
data->qvel[5] = 3.0;
|
||||
|
||||
int nstep = (int)(T / h + 0.5);
|
||||
for (int i = 0; i < nstep; i++) {
|
||||
mj_step(model.get(), data.get());
|
||||
}
|
||||
|
||||
mju_copy4(quat_out, data->qpos + 3);
|
||||
};
|
||||
|
||||
run(h_coarse, quat_coarse);
|
||||
run(h_coarse / 2, quat_fine);
|
||||
run(h_coarse / 16, quat_ref);
|
||||
|
||||
// quaternion distance: ||quat - quat_ref|| (handles sign ambiguity)
|
||||
auto quat_dist = [](const mjtNum a[4], const mjtNum b[4]) -> mjtNum {
|
||||
mjtNum pos = 0, neg = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
pos += (a[i] - b[i]) * (a[i] - b[i]);
|
||||
neg += (a[i] + b[i]) * (a[i] + b[i]);
|
||||
}
|
||||
return mju_sqrt(mju_min(pos, neg));
|
||||
};
|
||||
|
||||
mjtNum err_coarse = quat_dist(quat_coarse, quat_ref);
|
||||
mjtNum err_fine = quat_dist(quat_fine, quat_ref);
|
||||
|
||||
// second-order: error ratio should be ~4 when halving timestep
|
||||
mjtNum ratio = err_coarse / err_fine;
|
||||
EXPECT_GT(ratio, 3.5);
|
||||
EXPECT_LT(ratio, 4.5);
|
||||
}
|
||||
}
|
||||
|
||||
// verify that Newton iteration in mj_midpoint converges quickly (aligned case)
|
||||
TEST_F(ImplicitIntegratorTest, MidpointNewtonConvergence) {
|
||||
// inertia ratios: symmetric, mildly asymmetric, extremely asymmetric
|
||||
mjtNum inertias[][3] = {
|
||||
{1.0, 1.0, 1.0},
|
||||
{1.0, 2.0, 3.0},
|
||||
{0.01, 1.0, 100.0},
|
||||
{1.0, 1.0, 1000.0},
|
||||
};
|
||||
|
||||
mjtNum timesteps[] = {0.001, 0.01, 0.1};
|
||||
|
||||
mjtNum velocities[][3] = {
|
||||
{1.0, 2.0, 3.0},
|
||||
{100.0, 0.0, 0.0},
|
||||
{10.0, 10.0, 10.0},
|
||||
{0.01, 0.01, 100.0},
|
||||
};
|
||||
|
||||
mjtNum q_identity[4] = {1, 0, 0, 0};
|
||||
mjtNum torques[][3] = {
|
||||
{0, 0, 0},
|
||||
{10.0, 20.0, 30.0},
|
||||
{100.0, 0.0, 0.0},
|
||||
{0.0, 0.0, 100.0},
|
||||
};
|
||||
|
||||
int max_iter = 0;
|
||||
int total_iter = 0;
|
||||
int ncases = 0;
|
||||
|
||||
for (auto& I : inertias) {
|
||||
for (mjtNum h : timesteps) {
|
||||
for (auto& w : velocities) {
|
||||
for (auto& tau : torques) {
|
||||
mjtNum vel[6] = {0, 0, 0, w[0], w[1], w[2]};
|
||||
mjtNum tau_ext[6] = {0, 0, 0, tau[0], tau[1], tau[2]};
|
||||
mjtNum v_new[6];
|
||||
mjtNum ipos[3] = {0, 0, 0};
|
||||
int niter = mj_midpoint(1.0, I, ipos, q_identity, q_identity, vel,
|
||||
tau_ext, NULL, h, v_new);
|
||||
EXPECT_LT(niter, 10)
|
||||
<< "Failed for I=(" << I[0] << "," << I[1] << "," << I[2] << ")"
|
||||
<< " h=" << h << " w=(" << w[0] << "," << w[1] << "," << w[2]
|
||||
<< ")"
|
||||
<< " tau=(" << tau[0] << "," << tau[1] << "," << tau[2] << ")";
|
||||
max_iter = std::max(max_iter, niter);
|
||||
total_iter += niter;
|
||||
ncases++;
|
||||
}
|
||||
}
|
||||
// energy never increases (small tolerance for rounding)
|
||||
ASSERT_LT(d->energy[1], initial_energy * (1 + MjTol(1e-9, 1e-4)))
|
||||
<< "energy gain at step " << i;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_LE(max_iter, 4);
|
||||
EXPECT_LT((mjtNum)total_iter / ncases, 2.0);
|
||||
// implicit damping of tumbling is mild: measured E_end/E0 = 0.93
|
||||
EXPECT_GT(d->energy[1], 0.7 * initial_energy);
|
||||
|
||||
// angular momentum drift is bounded: measured 5e-3
|
||||
mj_subtreeVel(m, d);
|
||||
mjtNum angmom_err[3];
|
||||
mju_sub3(angmom_err, d->subtree_angmom, initial_angmom);
|
||||
EXPECT_LT(mju_norm3(angmom_err), 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// verify that Newton iteration in mj_midpoint converges quickly (non-aligned)
|
||||
TEST_F(ImplicitIntegratorTest, MidpointFullNewtonConvergence) {
|
||||
mjtNum masses[] = {0.1, 1.0, 10.0};
|
||||
|
||||
mjtNum inertias[][3] = {
|
||||
{1.0, 1.0, 1.0},
|
||||
{1.0, 2.0, 3.0},
|
||||
{0.01, 1.0, 100.0},
|
||||
};
|
||||
|
||||
mjtNum offsets[][3] = {
|
||||
{0.1, 0.0, 0.0},
|
||||
{0.05, 0.03, 0.02},
|
||||
{0.0, 0.0, 0.5},
|
||||
};
|
||||
|
||||
mjtNum timesteps[] = {0.001, 0.01, 0.1};
|
||||
|
||||
mjtNum velocities[][6] = {
|
||||
{1.0, 0.0, 0.0, 1.0, 2.0, 3.0},
|
||||
{0.0, 0.0, 0.0, 10.0, 10.0, 10.0},
|
||||
{5.0, 5.0, 5.0, 0.01, 0.01, 100.0},
|
||||
};
|
||||
|
||||
mjtNum q_identity[4] = {1, 0, 0, 0};
|
||||
mjtNum forces[][6] = {
|
||||
{0, 0, 0, 0, 0, 0},
|
||||
{10.0, 20.0, 30.0, 1.0, 2.0, 3.0},
|
||||
};
|
||||
|
||||
int max_iter = 0;
|
||||
int total_iter = 0;
|
||||
int ncases = 0;
|
||||
|
||||
for (mjtNum mass : masses) {
|
||||
for (auto& I : inertias) {
|
||||
for (auto& r : offsets) {
|
||||
for (mjtNum h : timesteps) {
|
||||
for (auto& vel : velocities) {
|
||||
for (auto& frc : forces) {
|
||||
mjtNum v_new[6];
|
||||
int niter = mj_midpoint(mass, I, r, q_identity, q_identity, vel,
|
||||
frc, NULL, h, v_new);
|
||||
EXPECT_LT(niter, 10)
|
||||
<< "Failed for mass=" << mass << " I=(" << I[0] << "," << I[1]
|
||||
<< "," << I[2] << ")"
|
||||
<< " r=(" << r[0] << "," << r[1] << "," << r[2] << ")"
|
||||
<< " h=" << h;
|
||||
max_iter = std::max(max_iter, niter);
|
||||
total_iter += niter;
|
||||
ncases++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_LE(max_iter, 6);
|
||||
EXPECT_LT((mjtNum)total_iter / ncases, 3.0);
|
||||
}
|
||||
|
||||
// verify midpoint eligibility: compare with/without invdiscrete
|
||||
// if trajectories differ, midpoint was applied
|
||||
// if trajectories match, midpoint was skipped
|
||||
TEST_F(ImplicitIntegratorTest, MidpointEligibility) {
|
||||
// free body with asymmetric inertia, optionally near a plane
|
||||
// gyroscopic instability: Euler gains energy where implicitfast does not
|
||||
TEST_F(ImplicitIntegratorTest, FreeBodyEulerGainsImplicitfastDissipates) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.01">
|
||||
<flag energy="enable"/>
|
||||
<option timestep="0.01">
|
||||
<flag energy="enable" gravity="disable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<geom type="plane" size="5 5 0.1"/>
|
||||
<body name="free" pos="0 0 2">
|
||||
<body>
|
||||
<freejoint/>
|
||||
<geom type="ellipsoid" size="0.3 0.2 0.1" mass="1"/>
|
||||
<geom type="box" size=".1 .2 .3" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(m.get(), NotNull()) << error;
|
||||
MjDataPtr d1 = MakeData(m);
|
||||
MjDataPtr d2 = MakeData(m);
|
||||
int nsteps = 50;
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
mjData* d = data.get();
|
||||
|
||||
auto spin_and_compare = [&](const char* label, bool expect_midpoint) {
|
||||
mj_resetData(m.get(), d1.get());
|
||||
mj_resetData(m.get(), d2.get());
|
||||
d1->qvel[3] = d2->qvel[3] = 5;
|
||||
d1->qvel[4] = d2->qvel[4] = 3;
|
||||
d1->qvel[5] = d2->qvel[5] = 1;
|
||||
|
||||
// d1: midpoint enabled (default)
|
||||
m->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
for (int i = 0; i < nsteps; i++) mj_step(m.get(), d1.get());
|
||||
|
||||
// d2: midpoint disabled
|
||||
m->opt.enableflags |= mjENBL_INVDISCRETE;
|
||||
mj_resetData(m.get(), d2.get());
|
||||
d2->qvel[3] = 5;
|
||||
d2->qvel[4] = 3;
|
||||
d2->qvel[5] = 1;
|
||||
for (int i = 0; i < nsteps; i++) mj_step(m.get(), d2.get());
|
||||
m->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
|
||||
// compare angular velocities
|
||||
mjtNum diff = 0;
|
||||
for (int k = 3; k < 6; k++) {
|
||||
mjtNum d = d1->qvel[k] - d2->qvel[k];
|
||||
diff += d * d;
|
||||
mjtNum energy_end[2];
|
||||
for (int integrator : {mjINT_EULER, mjINT_IMPLICITFAST}) {
|
||||
m->opt.integrator = integrator;
|
||||
mj_resetData(m, d);
|
||||
d->qvel[3] = 1.0;
|
||||
d->qvel[4] = 2.0;
|
||||
d->qvel[5] = 3.0;
|
||||
mj_forward(m, d);
|
||||
mjtNum initial_energy = d->energy[1];
|
||||
for (int i = 0; i < 500; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
energy_end[integrator == mjINT_IMPLICITFAST] =
|
||||
d->energy[1] / initial_energy;
|
||||
}
|
||||
|
||||
// Euler gains energy (measured: 1.09), implicitfast does not
|
||||
EXPECT_GT(energy_end[0], 1.01);
|
||||
EXPECT_LT(energy_end[1], 1.0);
|
||||
}
|
||||
|
||||
// the invdiscrete flag has no effect on forward dynamics
|
||||
TEST_F(ImplicitIntegratorTest, InvdiscreteForwardNoop) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option timestep="0.005"/>
|
||||
<worldbody>
|
||||
<geom type="plane" size="2 2 .1"/>
|
||||
<body pos="0 0 .3">
|
||||
<joint type="free" damping="0.1"/>
|
||||
<geom type="box" size=".1 .2 .3" mass="2" pos=".03 .02 .01"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr d1 = MakeData(model);
|
||||
MjDataPtr d2 = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
|
||||
for (int integrator : {mjINT_IMPLICITFAST, mjINT_IMPLICIT}) {
|
||||
m->opt.integrator = integrator;
|
||||
|
||||
mj_resetData(m, d1.get());
|
||||
d1->qvel[3] = 5;
|
||||
d1->qvel[5] = 2;
|
||||
mj_resetData(m, d2.get());
|
||||
d2->qvel[3] = 5;
|
||||
d2->qvel[5] = 2;
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
m->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
mj_step(m, d1.get());
|
||||
m->opt.enableflags |= mjENBL_INVDISCRETE;
|
||||
mj_step(m, d2.get());
|
||||
}
|
||||
m->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
|
||||
// trajectories are bit-identical
|
||||
for (int k = 0; k < m->nq; k++) {
|
||||
EXPECT_EQ(d1->qpos[k], d2->qpos[k]) << "qpos " << k;
|
||||
}
|
||||
for (int k = 0; k < m->nv; k++) {
|
||||
EXPECT_EQ(d1->qvel[k], d2->qvel[k]) << "qvel " << k;
|
||||
}
|
||||
if (expect_midpoint) {
|
||||
EXPECT_GT(diff, 1e-6) << label << ": expected midpoint to be applied";
|
||||
} else {
|
||||
EXPECT_LT(diff, 1e-20) << label << ": expected midpoint to be skipped";
|
||||
}
|
||||
};
|
||||
|
||||
// case 1: free body in vacuum, implicitfast -> midpoint applied
|
||||
m->opt.integrator = mjINT_IMPLICITFAST;
|
||||
m->opt.density = 0;
|
||||
m->opt.viscosity = 0;
|
||||
spin_and_compare("vacuum+implicitfast", true);
|
||||
|
||||
// case 2: implicit integrator -> midpoint NOT applied
|
||||
m->opt.integrator = mjINT_IMPLICIT;
|
||||
spin_and_compare("vacuum+implicit", false);
|
||||
|
||||
// case 3: fluid (nonzero density) -> midpoint NOT applied
|
||||
m->opt.integrator = mjINT_IMPLICITFAST;
|
||||
m->opt.density = 1.2;
|
||||
spin_and_compare("fluid+implicitfast", false);
|
||||
m->opt.density = 0;
|
||||
|
||||
// case 4: fluid (nonzero viscosity) -> midpoint NOT applied
|
||||
m->opt.viscosity = 0.001;
|
||||
spin_and_compare("viscosity+implicitfast", false);
|
||||
m->opt.viscosity = 0;
|
||||
|
||||
// case 5: body with active contacts -> midpoint NOT applied
|
||||
// test both island-enabled and island-disabled branches
|
||||
for (int disable_island = 0; disable_island < 2; disable_island++) {
|
||||
m->opt.integrator = mjINT_IMPLICITFAST;
|
||||
if (disable_island) {
|
||||
m->opt.disableflags |= mjDSBL_ISLAND;
|
||||
} else {
|
||||
m->opt.disableflags &= ~mjDSBL_ISLAND;
|
||||
}
|
||||
|
||||
mj_resetData(m.get(), d1.get());
|
||||
mj_resetData(m.get(), d2.get());
|
||||
d1->qpos[2] = d2->qpos[2] = 0.05;
|
||||
d1->qvel[3] = d2->qvel[3] = 5;
|
||||
d1->qvel[4] = d2->qvel[4] = 3;
|
||||
d1->qvel[5] = d2->qvel[5] = 1;
|
||||
|
||||
// verify contacts are active
|
||||
mj_forward(m.get(), d1.get());
|
||||
ASSERT_GT(d1->ncon, 0) << "body should be in contact with the plane";
|
||||
|
||||
// single step with midpoint enabled
|
||||
mj_resetData(m.get(), d1.get());
|
||||
d1->qpos[2] = 0.05;
|
||||
d1->qvel[3] = 5;
|
||||
d1->qvel[4] = 3;
|
||||
d1->qvel[5] = 1;
|
||||
m->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
mj_step(m.get(), d1.get());
|
||||
|
||||
// single step with midpoint disabled
|
||||
mj_resetData(m.get(), d2.get());
|
||||
d2->qpos[2] = 0.05;
|
||||
d2->qvel[3] = 5;
|
||||
d2->qvel[4] = 3;
|
||||
d2->qvel[5] = 1;
|
||||
m->opt.enableflags |= mjENBL_INVDISCRETE;
|
||||
mj_step(m.get(), d2.get());
|
||||
m->opt.enableflags &= ~mjENBL_INVDISCRETE;
|
||||
|
||||
mjtNum diff = 0;
|
||||
for (int k = 0; k < m->nv; k++) {
|
||||
mjtNum d = d1->qvel[k] - d2->qvel[k];
|
||||
diff += d * d;
|
||||
}
|
||||
EXPECT_LT(diff, 1e-20) << "contact (island "
|
||||
<< (disable_island ? "disabled" : "enabled")
|
||||
<< "): expected midpoint to be skipped";
|
||||
}
|
||||
m->opt.disableflags &= ~mjDSBL_ISLAND;
|
||||
}
|
||||
|
||||
// model with degenerate translational inertia
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "src/engine/engine_inverse.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
@@ -100,7 +101,7 @@ 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}) {
|
||||
for (auto integrator : {mjINT_EULER, mjINT_IMPLICIT, mjINT_IMPLICITFAST}) {
|
||||
model->opt.integrator = integrator;
|
||||
for (bool invdiscrete : {false, true}) {
|
||||
// set/unset mjENBL_INVDISCRETE flag (affects both forward and inverse)
|
||||
@@ -154,5 +155,67 @@ TEST_F(InverseTest, DiscreteInverseMatch) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// discrete-time inverse dynamics for a spinning free body under implicitfast:
|
||||
// exercises the local unsymmetric block (bias derivative) in mj_discreteAcc
|
||||
TEST_F(InverseTest, DiscreteInverseFreeBody) {
|
||||
// spinning box resting on a plane: standalone free body with active contacts
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" timestep="0.002">
|
||||
<flag invdiscrete="enable"/>
|
||||
</option>
|
||||
<worldbody>
|
||||
<geom type="plane" size="2 2 .1" friction="0.2"/>
|
||||
<body pos="0 0 .1">
|
||||
<joint type="free" damping="0.01"/>
|
||||
<geom type="box" size=".2 .15 .1" mass="2" pos=".02 -.01 .03" friction="0.2"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
|
||||
ASSERT_THAT(model.get(), NotNull()) << error;
|
||||
MjDataPtr data = MakeData(model);
|
||||
mjModel* m = model.get();
|
||||
mjData* d = data.get();
|
||||
int nv = m->nv;
|
||||
|
||||
// spin about the vertical, small tumble components
|
||||
mj_resetData(m, d);
|
||||
d->qvel[3] = 0.5;
|
||||
d->qvel[4] = -0.3;
|
||||
d->qvel[5] = 20;
|
||||
|
||||
// settle into persistent contact while still spinning
|
||||
for (int i = 0; i < kSteps; i++) {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
// save state, step, compute finite-differenced acceleration
|
||||
int nstate = mj_stateSize(m, mjSTATE_INTEGRATION);
|
||||
std::vector<mjtNum> state(nstate), qvel_next(nv), qacc_fd(nv);
|
||||
mj_getState(m, d, state.data(), mjSTATE_INTEGRATION);
|
||||
mj_step(m, d);
|
||||
mju_copy(qvel_next.data(), d->qvel, nv);
|
||||
mj_setState(m, d, state.data(), mjSTATE_INTEGRATION);
|
||||
mju_sub(qacc_fd.data(), qvel_next.data(), d->qvel, nv);
|
||||
mju_scl(qacc_fd.data(), qacc_fd.data(), 1 / m->opt.timestep, nv);
|
||||
|
||||
// forward, overwrite qacc with finite-differenced acceleration, compare
|
||||
mj_forward(m, d);
|
||||
ASSERT_GT(d->ncon, 0) << "body should be in contact";
|
||||
ASSERT_GT(mju_abs(d->qvel[5]), 1) << "body should still be spinning";
|
||||
mju_copy(d->qacc, qacc_fd.data(), nv);
|
||||
mj_compareFwdInv(m, d);
|
||||
|
||||
// measured residuals: ~6e-12 double, ~1.5e-2 single (float solver
|
||||
// convergence)
|
||||
mjtNum epsilon = MjTol(1e-10, 0.05);
|
||||
EXPECT_LT(d->solver_fwdinv[0], epsilon);
|
||||
EXPECT_LT(d->solver_fwdinv[1], epsilon);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
@@ -501,10 +501,10 @@ 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
|
||||
// Test that the free-body implicit (gyroscopic) solve doesn't break the sleep
|
||||
// qvel=0 invariant. A standalone free body with high viscosity should
|
||||
// eventually go to sleep, and after sleeping, qvel/qacc must be exactly zero.
|
||||
TEST_F(SleepTest, MidpointSleepZeroVelocity) {
|
||||
TEST_F(SleepTest, FreeBodySleepZeroVelocity) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<option integrator="implicitfast" viscosity="10"
|
||||
|
||||
Reference in New Issue
Block a user