Skip to content

Commit 35c0631

Browse files
yuvaltassacopybara-github
authored andcommitted
Clamp and decouple a Hessian pivot lost to rounding instead of aborting Newton.
H = M + J'*D*J >= M is SPD in exact arithmetic, so FactorizeHessian treated a rank-deficient sparse factor as a programming error. It is not: the reverse-order sparse factorization can lose a pivot to cancellation when the inertia is too ill-conditioned for float32 mjtNum. Both tests fail under float32 before the fix. PiperOrigin-RevId: 981785664 Change-Id: I382e7cfc2e4e0dfa8e0f8c0bbe3a1063b5bde32c
1 parent 74e466e commit 35c0631

5 files changed

Lines changed: 109 additions & 17 deletions

File tree

‎doc/changelog.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Engine
2222
- The :ref:`mjWARN_INERTIA <mjtWarning>` warning is now also raised by the modified-inertia factorizations of the
2323
:at:`implicitfast` and damped-:at:`Euler` integrators (previously silent) and of the :at:`implicit` integrator
2424
(previously a fatal error).
25+
- The sparse Newton solver no longer aborts with a "rank-deficient sparse Hessian" error when rounding loses a pivot of
26+
its Hessian, as can happen in single precision with ill-conditioned inertia. The pivot is now clamped and its row
27+
decoupled, as in the dense factorization.
2528

2629
Bug fixes
2730
^^^^^^^^^

‎src/engine/engine_solver.c‎

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2503,16 +2503,11 @@ static void FactorizeHessian(mjData* d, mjPrimalContext* ctx, int flg_recompute)
25032503
}
25042504

25052505
// numeric sparse factorization: L = chol(H) using pre-computed sparsity pattern
2506-
int rank = mju_cholFactorNumeric(
2506+
mju_cholFactorNumeric(
25072507
ctx->L, nv, mjMINVAL,
25082508
ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind,
25092509
ctx->LT_rownnz, ctx->LT_rowadr, ctx->LT_colind, ctx->LT_map,
25102510
ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, ctx->cholscratch);
2511-
2512-
// rank-deficient; SHOULD NOT OCCUR
2513-
if (rank != nv) {
2514-
mjERROR("rank-deficient sparse Hessian");
2515-
}
25162511
}
25172512

25182513
// dense
@@ -2607,13 +2602,11 @@ static void HessianConeFolded(mjData* d, mjPrimalContext* ctx) {
26072602
mju_addToMatSparse(Hcone, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, nv,
26082603
ctx->S_val, ctx->S_rownnz, ctx->S_rowadr, ctx->S_colind);
26092604
}
2610-
if (mju_cholFactorNumeric(ctx->Lcone, nv, mjMINVAL,
2611-
ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind,
2612-
ctx->LT_rownnz, ctx->LT_rowadr, ctx->LT_colind, ctx->LT_map,
2613-
Hcone, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind,
2614-
ctx->cholscratch) != nv) {
2615-
mjERROR("rank-deficient cone Hessian");
2616-
}
2605+
mju_cholFactorNumeric(ctx->Lcone, nv, mjMINVAL,
2606+
ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind,
2607+
ctx->LT_rownnz, ctx->LT_rowadr, ctx->LT_colind, ctx->LT_map,
2608+
Hcone, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind,
2609+
ctx->cholscratch);
26172610
}
26182611

26192612

‎src/engine/engine_util_solve.c‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,20 +351,25 @@ int mju_cholFactorNumeric(mjtNum* restrict L, int n, mjtNum mindiag,
351351

352352
// factor row r diagonal, handle rank-deficient case
353353
mjtNum diag = dense[r];
354-
if (diag < mindiag) {
354+
int deficient = diag < mindiag;
355+
if (deficient) {
355356
diag = mindiag;
356357
rank--;
357358
}
358359

359-
// scale off-diagonals
360+
// scale off-diagonals; if deficient clear them, decoupling the row as mju_cholFactor does
360361
mjtNum L_rr = mju_sqrt(diag);
361362
mjtNum L_rr_inv = 1.0 / L_rr;
362363
int L_adr = L_rowadr[r];
363364
int L_nnz = L_rownnz[r];
364365
const int* colptr = L_colind + L_adr;
365366
mjtNum* Lptr = L + L_adr;
366-
for (int i = 0; i < L_nnz - 1; i++) {
367-
Lptr[i] = dense[colptr[i]] * L_rr_inv;
367+
if (deficient) {
368+
mju_zero(Lptr, L_nnz - 1);
369+
} else {
370+
for (int i = 0; i < L_nnz - 1; i++) {
371+
Lptr[i] = dense[colptr[i]] * L_rr_inv;
372+
}
368373
}
369374

370375
// store diagonal

‎test/engine/engine_solver_test.cc‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,5 +765,50 @@ TEST_F(SolverTest, ConeFoldEquivalent) {
765765
}
766766
}
767767

768+
// A chain of thin capsules whose tip is connected to a slider: the connect
769+
// fills H = M + J'*D*J, and once the chain sags, the twist pivots of the
770+
// reverse factorization (the capsules' tiny axial inertia) are five orders of
771+
// magnitude below their diagonals. In single precision rounding loses one:
772+
// Newton must clamp and decouple it and continue, not abort or diverge.
773+
TEST_F(SolverTest, NewtonHessianSurvivesLostPivot) {
774+
constexpr char xml[] = R"(
775+
<mujoco>
776+
<worldbody>
777+
<composite type="cable" curve="s" count="41 1 1" size="1" initial="none">
778+
<joint kind="main" damping=".015"/>
779+
<geom type="capsule" size=".005" contype="0" conaffinity="0"/>
780+
</composite>
781+
<body name="slider" pos="1 0 0">
782+
<joint type="slide" axis="1 0 0" damping=".1"/>
783+
<geom size=".01" contype="0" conaffinity="0"/>
784+
</body>
785+
</worldbody>
786+
<equality>
787+
<connect body1="B_last" body2="slider" anchor=".025 0 0"/>
788+
</equality>
789+
</mujoco>
790+
)";
791+
792+
char error[1024];
793+
MjModelPtr model = LoadModelFromString(xml, error, sizeof(error));
794+
ASSERT_THAT(model.get(), NotNull()) << error;
795+
model->opt.solver = mjSOL_NEWTON;
796+
797+
for (mjtJacobian jacobian : {mjJAC_DENSE, mjJAC_SPARSE}) {
798+
for (int disableflags : {0, (int)mjDSBL_ISLAND}) {
799+
model->opt.jacobian = jacobian;
800+
model->opt.disableflags = disableflags;
801+
MjDataPtr data = MakeData(model);
802+
803+
// bounded by step count: a divergence resets mjData and rewinds the clock
804+
for (int step = 0; step < 200; step++) {
805+
mj_step(model.get(), data.get());
806+
}
807+
EXPECT_EQ(data->warning[mjWARN_BADQACC].number, 0)
808+
<< "jacobian " << jacobian << ", disableflags " << disableflags;
809+
}
810+
}
811+
}
812+
768813
} // namespace
769814
} // namespace mujoco

‎test/engine/engine_util_solve_test.cc‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,52 @@ TEST_F(EngineUtilSolveTest, CholFactorSymbolicNumeric) {
11011101
}
11021102
}
11031103

1104+
// A pivot lost to rounding leaves its row's coupling in place. The clamp must
1105+
// decouple the row, as mju_cholFactor does: dividing the coupling by
1106+
// sqrt(mindiag) would drive every later pivot of the factorization negative.
1107+
TEST_F(EngineUtilSolveTest, CholFactorNumericClampDecouples) {
1108+
MjModelPtr model = LoadModelFromString("<mujoco/>");
1109+
MjDataPtr d = MakeData(model);
1110+
1111+
// reverse elimination: row 2 has pivot 1, leaving row 1 with pivot 0 and
1112+
// coupling 0.5 to row 0, whose pivot is 1 once row 1 is decoupled
1113+
constexpr int n = 3;
1114+
mjtNum H[n * n] = {1, .5, 0, .5, 1, 1, 0, 1, 1};
1115+
mjtNum sparseH[n * n];
1116+
int H_rownnz[n], H_rowadr[n], H_colind[n * n];
1117+
mju_dense2sparse(sparseH, H, n, n, H_rownnz, H_rowadr, H_colind, n * n);
1118+
1119+
// symbolic factorization
1120+
int L_rownnz[n], L_rowadr[n], LT_rownnz[n], LT_rowadr[n];
1121+
int nnz = mju_cholFactorSymbolic(nullptr, L_rownnz, L_rowadr, nullptr,
1122+
LT_rownnz, LT_rowadr, nullptr, H_rownnz,
1123+
H_rowadr, H_colind, n, d.get());
1124+
int L_colind[n * n], LT_colind[n * n], LT_map[n * n];
1125+
mju_cholFactorSymbolic(L_colind, L_rownnz, L_rowadr, LT_colind, LT_rownnz,
1126+
LT_rowadr, LT_map, H_rownnz, H_rowadr, H_colind, n,
1127+
d.get());
1128+
1129+
// numeric factorization: only row 1 is clamped
1130+
mjtNum L[n * n], scratch[n];
1131+
int rank = mju_cholFactorNumeric(
1132+
L, n, mjMINVAL, L_rownnz, L_rowadr, L_colind, LT_rownnz, LT_rowadr,
1133+
LT_colind, LT_map, sparseH, H_rownnz, H_rowadr, H_colind, scratch);
1134+
EXPECT_EQ(rank, n - 1);
1135+
1136+
// row 1 is decoupled: clamped diagonal, zero off-diagonals
1137+
int adr1 = L_rowadr[1], nnz1 = L_rownnz[1];
1138+
EXPECT_EQ(L[adr1 + nnz1 - 1], mju_sqrt(mjMINVAL));
1139+
for (int i = 0; i < nnz1 - 1; i++) {
1140+
EXPECT_EQ(L[adr1 + i], 0);
1141+
}
1142+
1143+
// row 0 keeps its pivot, every entry is bounded
1144+
EXPECT_EQ(L[L_rowadr[0] + L_rownnz[0] - 1], 1);
1145+
for (int i = 0; i < nnz; i++) {
1146+
EXPECT_LE(mju_abs(L[i]), 1) << "entry " << i;
1147+
}
1148+
}
1149+
11041150
// ----------------------------- dense LU --------------------------------------
11051151

11061152
using DenseLUTest = MujocoTest;

0 commit comments

Comments
 (0)
⌂ 换网址