DGMRES: guard against Arnoldi happy breakdown and singular Hessenberg libeigen/eigen!2454 Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
diff --git a/unsupported/Eigen/src/IterativeSolvers/DGMRES.h b/unsupported/Eigen/src/IterativeSolvers/DGMRES.h index b251ff6..25f16be 100644 --- a/unsupported/Eigen/src/IterativeSolvers/DGMRES.h +++ b/unsupported/Eigen/src/IterativeSolvers/DGMRES.h
@@ -316,18 +316,31 @@ m_H(i, it) = coef; m_Hes(i, it) = coef; } - // Normalize the vector + // Normalize the vector. coef == 0 is an Arnoldi happy breakdown: the new + // direction lies in span(V[0..it]), so skip the division (which would + // poison m_V.col(it+1) with NaN) and fall through to the termination + // check below. coef = tv1.norm(); - m_V.col(it + 1) = tv1 / coef; + const bool happy_breakdown = numext::is_exactly_zero(coef); + if (!happy_breakdown) { + m_V.col(it + 1) = tv1 / coef; + } m_H(it + 1, it) = coef; // m_Hes(it+1,it) = coef; - // FIXME: Check for happy breakdown. - // Update Hessenberg matrix with Givens rotations for (Index i = 1; i <= it; ++i) { m_H.col(it).applyOnTheLeft(i - 1, i, gr[i - 1].adjoint()); } + + // If the rotated diagonal is also zero, the reduced triangular system + // becomes singular and the back-substitution below would produce Inf/NaN. + // Stop with NumericalIssue instead of polluting x. + if (happy_breakdown && numext::is_exactly_zero(m_H(it, it))) { + m_info = NumericalIssue; + break; + } + // Compute the new plane rotation gr[it].makeGivens(m_H(it, it), m_H(it + 1, it)); // Apply the new rotation @@ -339,15 +352,15 @@ it++; nbIts++; - if (m_error < m_tolerance) { - // The method has converged + if (m_error < m_tolerance || happy_breakdown) { + // Happy breakdown: residual on the current subspace is exactly zero, so + // the it-dim triangular system yields the exact solution. m_info = Success; break; } } - // Compute the new coefficients by solving the least square problem - // FIXME: Check first if the matrix is singular (zero diagonal). + // Compute the new coefficients by solving the least square problem. DenseVector nrs(m_restart); nrs = m_H.topLeftCorner(it, it).template triangularView<Upper>().solve(g.head(it));
diff --git a/unsupported/test/dgmres.cpp b/unsupported/test/dgmres.cpp index 940800c..04a4414 100644 --- a/unsupported/test/dgmres.cpp +++ b/unsupported/test/dgmres.cpp
@@ -24,7 +24,46 @@ // CALL_SUBTEST( check_sparse_square_solving(dgmres_colmajor_ssor) ); } +// Regression: Arnoldi breakdown used to divide by zero (producing NaN in the +// Krylov basis) and solve a singular triangular system, silently returning +// Inf with info() == Success. Exercise both the pathological (rank-deficient +// pivot) and benign (exact Krylov subspace) breakdown paths. +template <typename T> +void test_dgmres_breakdown_T() { + typedef SparseMatrix<T> Mat; + typedef Matrix<T, 2, 1> Vec; + + // Nilpotent A with singular Hessenberg pivot on the first step. + Mat A(2, 2); + A.insert(0, 1) = T(1); + A.makeCompressed(); + Vec b; + b << T(1), T(0); + + DGMRES<Mat, IdentityPreconditioner> solver; + solver.compute(A); + Vec x = solver.solve(b); + VERIFY(x.allFinite()); + VERIFY(solver.info() != Success); + + // Diagonal A with b in an eigenspace: Arnoldi converges after one step. + Mat D(2, 2); + D.insert(0, 0) = T(2); + D.insert(1, 1) = T(2); + D.makeCompressed(); + Vec d; + d << T(2), T(2); + + DGMRES<Mat, DiagonalPreconditioner<T> > solver2; + solver2.compute(D); + Vec y = solver2.solve(d); + VERIFY_IS_EQUAL(solver2.info(), Success); + VERIFY_IS_APPROX(y, (Vec() << T(1), T(1)).finished()); +} + EIGEN_DECLARE_TEST(dgmres) { CALL_SUBTEST_1(test_dgmres_T<double>()); CALL_SUBTEST_2(test_dgmres_T<std::complex<double> >()); + CALL_SUBTEST_3(test_dgmres_breakdown_T<double>()); + CALL_SUBTEST_4(test_dgmres_breakdown_T<std::complex<double> >()); }