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:
Yuval Tassa
2026-07-15 12:07:10 -07:00
committed by Copybara-Service
parent b2106db52f
commit f0fa3d8260
12 changed files with 631 additions and 840 deletions
+64 -1
View File
@@ -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