Add manual test for CG converence
Also add MJTOL_SCALE to fixture to allow tests to be run with zero tolerance. This is useful when assesing the impact of code changes (A/B comparison of failure values) PiperOrigin-RevId: 924219083 Change-Id: Ifdd09ac850904ca8dd79179930ce738a4b37d284
This commit is contained in:
committed by
Copybara-Service
parent
50e823e91c
commit
4358a102cd
@@ -26,9 +26,8 @@
|
||||
namespace mujoco {
|
||||
namespace {
|
||||
|
||||
using ::testing::NotNull;
|
||||
using ::testing::Pointwise;
|
||||
using ::std::max;
|
||||
using ::testing::NotNull;
|
||||
|
||||
using SolverTest = MujocoTest;
|
||||
|
||||
@@ -60,16 +59,14 @@ TEST_F(SolverTest, IslandsEquivalent) {
|
||||
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.
|
||||
mjtNum rtol[kNumTol] = {
|
||||
MjTol(6e-2, 7.2e-1),
|
||||
MjTol(6e-3, 7.2e-2),
|
||||
MjTol(6e-4, 7.2e-3)
|
||||
MjTol(1e-1, 2),
|
||||
MjTol(3e-2, 1),
|
||||
MjTol(1.5e-4, 3.6)
|
||||
};
|
||||
|
||||
for (int i = 0; i < kNumTol; ++i) {
|
||||
@@ -85,6 +82,14 @@ TEST_F(SolverTest, IslandsEquivalent) {
|
||||
model->opt.disableflags &= ~mjDSBL_WARMSTART;
|
||||
}
|
||||
|
||||
mjtNum max_ratio = 0;
|
||||
mjtNum worst_diff = 0;
|
||||
mjtNum worst_scale = 1.0;
|
||||
mjtNum worst_expected = 0;
|
||||
mjtNum worst_actual = 0;
|
||||
std::string worst_time = "";
|
||||
int worst_dof = -1;
|
||||
|
||||
while (data_noisland->time < .1) {
|
||||
mj_getState(model, data_noisland, state, mjSTATE_INTEGRATION);
|
||||
mj_setState(model, data_island, state, mjSTATE_INTEGRATION);
|
||||
@@ -95,22 +100,38 @@ TEST_F(SolverTest, IslandsEquivalent) {
|
||||
model->opt.disableflags |= mjDSBL_ISLAND; // disable islands
|
||||
mj_forward(model, data_noisland);
|
||||
|
||||
auto time = std::to_string(data_noisland->time);
|
||||
for (int j = 0; j < nv; j++) {
|
||||
// increase tolerance for large elements
|
||||
mjtNum diff = std::abs(data_noisland->qacc[j] - data_island->qacc[j]);
|
||||
mjtNum scale = 0.5 * max(static_cast<mjtNum>(2.0),
|
||||
std::abs(data_noisland->qacc[j]) +
|
||||
std::abs(data_island->qacc[j]));
|
||||
EXPECT_NEAR(data_noisland->qacc[j], data_island->qacc[j],
|
||||
MjTol(scale * rtol[i], 500 * scale * rtol[i]))
|
||||
<< "time: " << time << '\n'
|
||||
<< "dof: " << j << '\n'
|
||||
<< "maxiter: " << maxiter[i] << '\n'
|
||||
<< "rtol: " << scale * rtol[i];
|
||||
std::abs(data_island->qacc[j]));
|
||||
mjtNum ratio = diff / scale;
|
||||
if (ratio > max_ratio) {
|
||||
max_ratio = ratio;
|
||||
worst_diff = diff;
|
||||
worst_scale = scale;
|
||||
worst_expected = data_island->qacc[j];
|
||||
worst_actual = data_noisland->qacc[j];
|
||||
worst_time = std::to_string(data_noisland->time);
|
||||
worst_dof = j;
|
||||
}
|
||||
}
|
||||
|
||||
mj_step(model, data_noisland);
|
||||
}
|
||||
|
||||
// Assert once per condition with the worst offender.
|
||||
// rtol[i] is already scaled by MjTolScale() at initialization.
|
||||
mjtNum allowed_tol = worst_scale * rtol[i];
|
||||
EXPECT_NEAR(worst_actual, worst_expected, allowed_tol)
|
||||
<< "Worst offender info:\n"
|
||||
<< "time: " << worst_time << '\n'
|
||||
<< "dof: " << worst_dof << '\n'
|
||||
<< "maxiter: " << maxiter[i] << '\n'
|
||||
<< "coldstart: " << coldstart << '\n'
|
||||
<< "rtol: " << worst_scale * rtol[i] << '\n'
|
||||
<< "actual diff: " << worst_diff << " (allowed: " << allowed_tol
|
||||
<< ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,16 +182,36 @@ TEST_F(SolverTest, IslandsEquivalentForward) {
|
||||
model->opt.disableflags &= ~mjDSBL_ISLAND; // enable islands
|
||||
mj_forward(model, data_island);
|
||||
|
||||
mjtNum max_diff = 0;
|
||||
mjtNum worst_expected = 0;
|
||||
mjtNum worst_actual = 0;
|
||||
int worst_idx = -1;
|
||||
mjtNum scale = 0.5 * (mju_norm(data_noisland->qacc, nv) +
|
||||
mju_norm(data_island->qacc, nv));
|
||||
mjtNum tol = scale * (solver == mjSOL_CG ? 1e-6 : 1e-8);
|
||||
EXPECT_THAT(AsVector(data_island->qacc, nv),
|
||||
Pointwise(MjNear(scale * tol, 500 * scale * tol),
|
||||
AsVector(data_noisland->qacc, nv)))
|
||||
mjtNum rtol = solver == mjSOL_CG ? MjTol(1e-6, 1e-2)
|
||||
: MjTol(1e-13, 1e-3);
|
||||
mjtNum worst_allowed = scale * rtol;
|
||||
|
||||
for (int j = 0; j < nv; j++) {
|
||||
mjtNum diff =
|
||||
std::abs(data_island->qacc[j] - data_noisland->qacc[j]);
|
||||
if (diff > max_diff) {
|
||||
max_diff = diff;
|
||||
worst_expected = data_noisland->qacc[j];
|
||||
worst_actual = data_island->qacc[j];
|
||||
worst_idx = j;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_NEAR(worst_actual, worst_expected, worst_allowed)
|
||||
<< "Worst offender in IslandsEquivalentForward:\n"
|
||||
<< "idx: " << worst_idx << '\n'
|
||||
<< "warmstart: " << warmstart << '\n'
|
||||
<< "jacobian: " << (jacobian ? "sparse" : "dense") << '\n'
|
||||
<< "solver: " << (solver == mjSOL_CG ? "CG" : "Newton") << '\n'
|
||||
<< "cone: " << (cone == 1 ? "elliptic" : "pyramidal");
|
||||
<< "cone: " << (cone == 1 ? "elliptic" : "pyramidal") << '\n'
|
||||
<< "actual diff: " << max_diff << " (allowed: " << worst_allowed
|
||||
<< ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,15 +224,14 @@ TEST_F(SolverTest, IslandsEquivalentForward) {
|
||||
|
||||
TEST_F(SolverTest, SolversEquivalent) {
|
||||
struct SolverTolerances {
|
||||
double newton;
|
||||
double cg;
|
||||
double pgs_pyramidal;
|
||||
double pgs_elliptic;
|
||||
mjtNum newton;
|
||||
mjtNum cg;
|
||||
mjtNum pgs_pyramidal;
|
||||
mjtNum pgs_elliptic;
|
||||
};
|
||||
|
||||
// Base relative tolerances are factor of 10 above failure thresholds
|
||||
// on Linux, clang, x86-64 (i.e., test just passes with tol_multiplier = 1)
|
||||
// TODO: Get float32 tolerances
|
||||
// Relative tolerances: 10x above failure thresholds on Linux, clang, x86-64.
|
||||
// MjTol(f64, f32) selects the appropriate tolerance for the current build.
|
||||
const struct {
|
||||
const char* path;
|
||||
SolverTolerances tolerances;
|
||||
@@ -199,18 +239,18 @@ TEST_F(SolverTest, SolversEquivalent) {
|
||||
{.path = kModelPath,
|
||||
.tolerances =
|
||||
{
|
||||
.newton = 1e-15,
|
||||
.cg = 1e-7,
|
||||
.pgs_pyramidal = 1e-14,
|
||||
.pgs_elliptic = 1e-3,
|
||||
.newton = MjTol(1e-13, 1e-5),
|
||||
.cg = MjTol(1e-5, 1e-2),
|
||||
.pgs_pyramidal = MjTol(1e-12, 1e-5),
|
||||
.pgs_elliptic = MjTol(1e-3, 1e-2),
|
||||
}},
|
||||
{.path = kHumanoidPath,
|
||||
.tolerances =
|
||||
{
|
||||
.newton = 1e-15,
|
||||
.cg = 1e-7,
|
||||
.pgs_pyramidal = 1e-6,
|
||||
.pgs_elliptic = 1e-9,
|
||||
.newton = MjTol(1e-13, 1e-5),
|
||||
.cg = MjTol(1e-5, 1e-2),
|
||||
.pgs_pyramidal = MjTol(1e-5, 1e-5),
|
||||
.pgs_elliptic = MjTol(1e-8, 1e-4),
|
||||
}},
|
||||
};
|
||||
|
||||
@@ -241,7 +281,7 @@ TEST_F(SolverTest, SolversEquivalent) {
|
||||
mjtNum scale = mju_norm(data_truth->qfrc_constraint, nv);
|
||||
|
||||
for (mjtSolver solver : {mjSOL_NEWTON, mjSOL_CG, mjSOL_PGS}) {
|
||||
double rtol;
|
||||
mjtNum rtol;
|
||||
switch (solver) {
|
||||
case mjSOL_NEWTON:
|
||||
rtol = config.tolerances.newton;
|
||||
@@ -255,9 +295,7 @@ TEST_F(SolverTest, SolversEquivalent) {
|
||||
break;
|
||||
}
|
||||
|
||||
// increase base tolerance to avoid test flakiness
|
||||
double tol_multiplier = 1e2;
|
||||
double tolerance = scale * rtol * tol_multiplier;
|
||||
mjtNum tolerance = scale * rtol;
|
||||
|
||||
for (mjtJacobian jacobian : {mjJAC_DENSE, mjJAC_SPARSE}) {
|
||||
model->opt.solver = solver;
|
||||
@@ -274,15 +312,31 @@ TEST_F(SolverTest, SolversEquivalent) {
|
||||
const char* jacobian_str =
|
||||
(jacobian == mjJAC_DENSE ? "dense" : "sparse");
|
||||
|
||||
EXPECT_THAT(AsVector(data->qfrc_constraint, nv),
|
||||
Pointwise(MjNear(tolerance,
|
||||
max(1e-1, 1000 * tolerance)),
|
||||
AsVector(data_truth->qfrc_constraint, nv)))
|
||||
mjtNum max_diff = 0;
|
||||
mjtNum worst_expected = 0;
|
||||
mjtNum worst_actual = 0;
|
||||
int worst_idx = -1;
|
||||
|
||||
for (int j = 0; j < nv; j++) {
|
||||
mjtNum diff = std::abs(data->qfrc_constraint[j] -
|
||||
data_truth->qfrc_constraint[j]);
|
||||
if (diff > max_diff) {
|
||||
max_diff = diff;
|
||||
worst_expected = data_truth->qfrc_constraint[j];
|
||||
worst_actual = data->qfrc_constraint[j];
|
||||
worst_idx = j;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_NEAR(worst_actual, worst_expected, tolerance)
|
||||
<< "Worst offender in SolversEquivalent:\n"
|
||||
<< "idx: " << worst_idx << '\n'
|
||||
<< "model: " << config.path << "\n"
|
||||
<< "cone: " << cone_str << "\n"
|
||||
<< "solver: " << solver_str << "\n"
|
||||
<< "jacobian: " << jacobian_str << "\n"
|
||||
<< "tolerance: " << tolerance;
|
||||
<< "actual diff: " << max_diff << " (allowed: " << tolerance
|
||||
<< ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user