Skip to content

Commit 30577b9

Browse files
committed
fix: normalize w/p to unit S-norm before building small subspace
The [w,p] block of the Gram matrix M shrinks as residuals converge, making M nearly singular and causing sygvd to produce garbage eigenvectors. Scaling w and p to unit S-norm keeps M well-conditioned (diagonal ~1) without changing the subspace — Ritz values are identical and Ritz vector coefficients cancel in update_one_block. This enables the full 3-block [psi,w,p] subspace (use_p=true) by addressing the fundamental ill-conditioning that the p-bad Krylov fallback alone could not handle.
1 parent 765064a commit 30577b9

1 file changed

Lines changed: 42 additions & 9 deletions

File tree

source/source_hsolver/diago_ppcg.cpp

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,41 @@ void DiagoPPCG<T, Device>::build_small_subspace(
517517
copy_cols(hp_.data(), cols, hp_l);
518518
}
519519

520+
// ---------------------------------------------------------------------------
521+
// Normalize w and p columns to unit S-norm for numerical stability.
522+
//
523+
// The [w, p] block of the Gram matrix M has entries O(||w||²) which
524+
// become tiny when residuals are small, making M nearly singular and
525+
// causing sygvd to produce garbage eigenvectors.
526+
//
527+
// Scaling to unit S-norm keeps M well-conditioned (diagonal ~1) without
528+
// changing the subspace. The Ritz values are identical and the Ritz
529+
// vector coefficients in update_one_block automatically compensate.
530+
// ---------------------------------------------------------------------------
531+
auto scale_to_unit_snorm = [this](std::vector<T>& x, std::vector<T>& sx,
532+
std::vector<T>& hx, int lcols) {
533+
for (int j = 0; j < lcols; ++j) {
534+
Real sn2 = 0;
535+
for (int ig = 0; ig < n_dim_; ++ig)
536+
sn2 += std::real(std::conj(x[idx(ig, j, ld_psi_)])
537+
* sx[idx(ig, j, ld_psi_)]);
538+
Real sn = std::sqrt(std::max(sn2, static_cast<Real>(1e-30)));
539+
// Only scale if the norm is non-negligible; a near-zero
540+
// column is a converged band whose contribution is harmless.
541+
if (sn > static_cast<Real>(1e-15)) {
542+
Real inv = static_cast<Real>(1) / sn;
543+
for (int ig = 0; ig < n_dim_; ++ig) {
544+
x[ idx(ig, j, ld_psi_)] *= inv;
545+
sx[idx(ig, j, ld_psi_)] *= inv;
546+
hx[idx(ig, j, ld_psi_)] *= inv;
547+
}
548+
}
549+
}
550+
};
551+
scale_to_unit_snorm(w_l, sw_l, hw_l, l);
552+
if (use_p)
553+
scale_to_unit_snorm(p_l, sp_l, hp_l, l);
554+
520555
auto fill_sym = [&](const std::vector<T>& a, const std::vector<T>& b,
521556
int r0, int c0, std::vector<Real>& mat)
522557
{
@@ -1168,15 +1203,13 @@ double DiagoPPCG<T, Device>::diag(const HPsiFunc& hpsi_func,
11681203

11691204
avg_iter += static_cast<double>(nact) / static_cast<double>(ncol);
11701205

1171-
// Use the 2-block [psi, w] subspace (preconditioned Davidson).
1172-
// The 3-block [psi, w, p] subspace can become ill-conditioned
1173-
// when residuals are small: the [w, p] block of the Gram matrix
1174-
// shrinks, making M nearly singular and causing sygvd to produce
1175-
// garbage eigenvectors. The p-bad detection + H·w Krylov fallback
1176-
// (below, currently disabled) addresses p~w collinearity but not
1177-
// the small-residual ill-conditioning. Without p the method
1178-
// converges robustly with slightly more iterations.
1179-
const bool use_p = false;
1206+
// Use the 3-block [psi, w, p] subspace.
1207+
// w and p are normalized to unit S-norm before building the
1208+
// Gram matrix (see build_small_subspace), which keeps M
1209+
// well-conditioned even when residuals are small. The p-bad
1210+
// detection + H·w Krylov fallback handles the remaining
1211+
// ill-conditioning: p nearly collinear with w.
1212+
const bool use_p = true;
11801213
if (use_p)
11811214
{
11821215
apply_s_current(p_.data(), sp_.data(), ncol);

0 commit comments

Comments
 (0)