Finally Block Harmonic(?) KS working

This commit is contained in:
Chulwoo Jung
2026-04-14 14:40:55 -04:00
parent aecc50869c
commit a696953485
3 changed files with 59 additions and 185 deletions
@@ -34,65 +34,44 @@ See the full license in the file "LICENSE" in the top level distribution directo
NAMESPACE_BEGIN(Grid);
/**
* Block harmonic restarted Krylov-Schur eigensolver.
* Block shift-targeted Krylov-Schur eigensolver.
*
* Harmonic Ritz values
* --------------------
* Standard Ritz values of A in a Krylov space K_m minimise the residual
* in a Galerkin sense; they are good approximations to eigenvalues at the
* *exterior* of the spectrum. For eigenvalues *near* a target shift σ
* (e.g. the smallest eigenvalues when σ=0) harmonic Ritz values are
* better-suited: they are obtained by a Petrov-Galerkin condition that
* requires the residual to be orthogonal to (A-σI)K_m instead of K_m.
*
* Given the block Arnoldi factorisation
* Algorithm
* ---------
* Uses a block Arnoldi factorisation:
*
* A V = V H + F B^dag (1)
*
* with V orthonormal (Nm columns), H the Nm² block
* upper-Hessenberg Rayleigh quotient, F the Nblock residual vectors and B
* the Nm×Nblock coupling matrix, the harmonic Rayleigh quotient
* relative to shift σ is
*
* Hhat = H + (H - σI)^{-H} B B^H (2)
*
* Derivation: the harmonic Ritz condition (A-σI)Vy ⊥ (A-σI)V leads to
*
* [ (H-σI)^H (H-σI) + B B^H ] y = μ (H-σI)^H y
*
* Left-multiplying by (H-σI)^{-H} and setting θ = μ + σ gives the
* standard eigenvalue problem Hhat y = θ y with Hhat as in (2).
*
* The harmonic Ritz values θ_j are eigenvalues of Hhat; among these,
* the ones closest to σ (smallest |θ_j - σ|) are the best approximations
* to the eigenvalues of A near σ.
* with V orthonormal (Nm columns), H the Nm×Nm block upper-Hessenberg
* Rayleigh quotient, F the Nblock residual vectors and B the Nm×Nblock
* coupling matrix.
*
* Thick restart
* -------------
* The Schur decomposition Hhat = Q^dag S Q is computed and the
* leading Nk*Nblock Schur values (sorted by the RitzFilter) are kept.
* The same unitary rotation Q is applied to both the Krylov basis and
* to the *original* Rayleigh quotient H (not Hhat) for the restart:
* To target eigenvalues near shift σ, the Schur decomposition is computed
* for the shifted Rayleigh quotient:
*
* V_new = V Q^dag [first Nk*Nblock columns]
* H_new = Q H Q^dag [truncated Nk*Nblock × Nk*Nblock]
* B_new = Q B [truncated Nk*Nblock × Nblock]
* (H - σI) = Q^dag S Q (2)
*
* Block Arnoldi then resumes from block Nk, restoring H to full size
* as new columns are appended.
* Sorting the Schur values of (H - σI) by smallest |S(i,i)| = |λ - σ| and
* retaining the leading Nk is equivalent to selecting the Ritz values of H
* closest to σ. Since Q diagonalises H - σI (and hence H itself), the
* rotated Rayleigh quotient is exactly upper triangular:
*
* H_new = Q H Q^dag = S + σI (upper triangular) (3)
*
* Truncation to Nk is therefore exact: the off-diagonal coupling block
* H_new[Nk:, :Nk] = 0 by triangularity. The Krylov-Schur relation after
* restart is exact and block Arnoldi resumes cleanly from F.
*
* Convergence
* -----------
* For a harmonic Ritz pair (θ, y) the true Ritz residual bound is
*
* || (A - θI) V y || ≤ || B^H y ||
*
* (same as for standard Ritz, because B captures the full coupling).
* Convergence is declared when || B^H y || < Tolerance * approxLambdaMax.
* Convergence is declared when || B^H y_k || < Tolerance * approxLambdaMax
* for each Ritz pair (λ_k, y_k) of the truncated H.
*
* Parameters
* ----------
* shift : target shift σ (default 0.0)
* shift : target shift σ (default 0.0); Schur values sorted by |λ - σ|
* Nblock : block size p
* Nm : total Krylov dimension (must be divisible by Nblock)
* Nk : total vectors to retain after each restart (must be divisible by Nblock, Nk < Nm)
@@ -205,70 +184,38 @@ public:
verify(lbl);
}
// ---- Form harmonic Rayleigh quotient ----
// Hhat = H + (H - σI)^{-H} * B * B^H
CMat Hhat = harmonicRayleigh(H, B, N);
// ---- Schur decompose Hhat ----
ComplexSchurDecomposition schur(Hhat, false, ritzFilter);
// ---- Schur decompose (H - σI) to select Schur vectors closest to σ ----
// Sorting the Schur values of (H - σI) by |S(i,i)| = |λ - σ| gives the
// Ritz values of H nearest the target shift without any matrix inversion.
// Because Q diagonalises (H - σI), it also diagonalises H:
// H_new = Q H Q^dag = S + σI (upper triangular)
// Truncation is therefore exact.
CMat Hshift = H - shift * CMat::Identity(N, N);
ComplexSchurDecomposition schur(Hshift, false, ritzFilter);
schur.schurReorder(Nk);
std::cout << GridLogMessage
<< "HarmonicBlockKrylovSchur: harmonic Ritz values (first Nk):" << std::endl;
<< "HarmonicBlockKrylovSchur: Ritz values nearest shift (first Nk):" << std::endl;
CMat S = schur.getMatrixS();
for (int i = 0; i < Nk; i++)
std::cout << GridLogMessage << " [" << i << "] " << S(i, i) << std::endl;
std::cout << GridLogMessage << " [" << i << "] " << S(i, i) + shift << std::endl;
CMat Q = schur.getMatrixQ();
CMat Qt = Q.adjoint();
// ---- Rotate Krylov basis using Q from Hhat ----
// ---- Rotate Krylov basis ----
std::vector<Field> basis2;
constructUR(basis2, basis, Qt, N);
basis = basis2;
// ---- Update H and B (rotate H, not Hhat) ----
H = Q * H * Qt;
// ---- Update H and B ----
// H_new = S + σI is upper triangular; off-diagonal block H_new[Nk:,:Nk] = 0
H = S + shift * CMat::Identity(N, N);
B = Q * B;
// ---- Truncate to Nk ----
// ---- Truncate to Nk (exact: H upper triangular) ----
int Nkeep = Nk;
// ---- Option B: corrected restart starting block -------------------------
//
// In standard Krylov-Schur, Q diagonalises H itself, so Q H Q^H = S is
// upper triangular and the off-diagonal coupling block H_dk = S[Nkeep:,:Nkeep] = 0.
// Truncation is therefore exact.
//
// Here Q diagonalises Hhat (not H), so H_new = Q H Q^H is generally dense.
// The off-diagonal block
//
// H_dk = H_new[Nkeep:, :Nkeep] (size (N-Nkeep) x Nkeep)
//
// is non-zero, and the true KS relation after truncation is:
//
// A V_k = V_k H_k + V_disc H_dk + F B_k^H
//
// If we restart from F only, the V_disc H_dk term is lost.
//
// Fix: include the V_disc coupling in the new starting block:
//
// G[t] = F[t] + sum_{s=Nkeep}^{N-1} basis[s] * H(s, t), t = 0..Nblock-1
//
// G[:,t] is the t-th column of (V_disc H_dk + F B_k^H) restricted to the
// first Nblock columns of H_dk. Since F ⊥ V_k and V_disc ⊥ V_k, G is
// automatically orthogonal to the retained subspace.
//
// This must be computed BEFORE basis[Nkeep:] and H[Nkeep:, :] are discarded.
std::vector<Field> G(Nblock, Field(Grid_));
for (int t = 0; t < Nblock; t++) {
G[t] = F[t];
for (int s = Nkeep; s < N; s++)
G[t] += basis[s] * H(s, t);
}
blockQR(G); // orthonormalise within G (G is already ⊥ to basis[0:Nkeep])
CMat Htmp = H(Eigen::seqN(0, Nkeep), Eigen::seqN(0, Nkeep));
H = CMat::Zero(N, N);
H(Eigen::seqN(0, Nkeep), Eigen::seqN(0, Nkeep)) = Htmp;
@@ -284,10 +231,8 @@ public:
std::cout << GridLogMessage
<< "HarmonicBlockKrylovSchur: beta_k = " << beta_k << std::endl;
// Use corrected starting block G (not bare F).
// G encodes the coupling from the discarded basis vectors V_disc through
// H_dk[:,0:Nblock], restoring the exact KS relation to first order.
startBlock = G;
// Restart from F (exact: no discarded-basis correction needed)
startBlock = F;
if (doVerify) {
std::string lbl = "iter " + std::to_string(iter) + " after restart+truncation";
@@ -327,8 +272,7 @@ public:
* A V = V H + F B^dag (KS)
*
* by explicit operator applications. H here is the standard Rayleigh
* quotient (not Hhat), so the KS relation is the same as for
* BlockKrylovSchur.
* quotient, so the KS relation is the same as for BlockKrylovSchur.
*
* Prints:
* - H (current Rayleigh quotient, nBasis × nBasis)
@@ -445,32 +389,6 @@ public:
private:
//--------------------------------------------------------------------
// Harmonic Rayleigh quotient
//--------------------------------------------------------------------
/**
* Forms the harmonic Rayleigh quotient relative to shift σ:
*
* Hhat = H + (H - σI)^{-H} * B * B^H
*
* where H is the N×N block-Hessenberg, B is the N×Nblock coupling matrix.
*
* The N×N solve (H - σI)^H X = B B^H is done via Eigen's LU
* factorisation. If H - σI is (nearly) singular the result is
* ill-conditioned; in that case σ should be perturbed slightly.
*/
CMat harmonicRayleigh(const CMat& H_, const CMat& B_, int N)
{
CMat K = H_ - shift * CMat::Identity(N, N);
CMat KH = K.adjoint(); // (H - σI)^H
// Solve KH * X = B B^H → X = KH^{-1} B B^H = (H-σI)^{-H} B B^H
CMat BBH = B_ * B_.adjoint(); // N × N
CMat X = KH.lu().solve(BBH); // N × N
return H_ + X;
}
//--------------------------------------------------------------------
// Block Arnoldi iteration
//--------------------------------------------------------------------
@@ -491,24 +409,15 @@ private:
} else {
// Append the new starting block to the retained basis.
//
// Standard KS (startBlock = F):
// The exact truncated relation is A V_k = V_k H_k + F B_k^dag,
// so the coupling rows are H[Nkeep+t, j] = conj(B_k[j,t]).
//
// Harmonic KS Option B (startBlock = G, where G = F + V_disc H_dk[:,0:Nblock]):
// The exact coupling rows are H[Nkeep+t, j] = <G[t], A V_k[:,j]>,
// which differs from conj(B_k[j,t]) because G ≠ F.
// For a Hermitian operator these preset rows are overwritten by the
// Hermitian symmetry fill inside blockArnoldiStep (via explicit inner
// products), so the approximate preset below does no harm.
// For a non-Hermitian operator the preset is approximate; a more
// expensive fix would compute <G[t], A V_k[:,j]> explicitly here.
// The exact truncated KS relation is A V_k = V_k H_k + F B_k^dag,
// so the coupling rows are H[Nkeep+t, j] = conj(B_k[j,t]).
// Since H_new = S + σI is upper triangular, the off-diagonal block
// H_new[Nkeep:, :Nkeep] = 0 and the restart from F is exact.
int Nkeep = startIdx * Nblock;
for (auto& v : startBlock) basis.push_back(v);
// Fill restart coupling rows into H (exact for standard KS; approximate
// for harmonic KS with Option-B starting block, but overwritten by
// Hermitian symmetry fill for Hermitian operators).
// Fill restart coupling rows into H (exact: H_new is upper triangular,
// so B encodes the only non-zero coupling to the new block).
for (int t = 0; t < Nblock; t++)
for (int j = 0; j < Nkeep; j++)
H(Nkeep + t, j) = std::conj(B(j, t));
@@ -560,10 +469,6 @@ private:
for (int s = 0; s < Nblock; s++)
B(kBase + t, s) = std::conj(R(s, t)); // B_block = R^H
beta_k = R.norm();
// Hermitian symmetry fill for last block (same as non-last path below)
for (int t = 0; t < Nblock; t++)
for (int j = 0; j < kBase; j++)
H(kBase + t, j) = std::conj(H(j, kBase + t));
return;
}
@@ -578,23 +483,6 @@ private:
for (int t = 0; t < Nblock; t++)
basis.push_back(F[t]);
// Hermitian symmetry fill: H[kBase+t, j] = conj(H[j, kBase+t]) for j < kBase.
//
// In a fresh block Arnoldi the Krylov structure forces H[kBase+t, j] = 0 for
// j < kBase-Nblock (sub-subdiagonal), so this is a no-op.
//
// After a non-Schur restart (e.g. harmonic restart where H_new = Q H Q^dag is
// a full matrix), A v_k_j for j < Nkeep has components in ALL new extended
// vectors, making these elements non-zero. The Arnoldi step fills column
// kBase+t (H[j, kBase+t] for j < prevN) via inner products, but never fills
// the corresponding row. For a Hermitian operator the two are related by
// H[kBase+t, j] = <basis[kBase+t] | A basis[j]>
// = conj(<basis[j] | A basis[kBase+t]>) = conj(H[j, kBase+t])
// Filling these ensures H = H^dag and fixes the M != H discrepancy that
// corrupts subsequent Arnoldi steps after a harmonic restart.
for (int t = 0; t < Nblock; t++)
for (int j = 0; j < kBase; j++)
H(kBase + t, j) = std::conj(H(j, kBase + t));
}
//--------------------------------------------------------------------
+4 -18
View File
@@ -56,6 +56,7 @@ struct LanczosParameters: Serializable {
Integer, maxIter,
Integer, Nblock,
Integer, verify,
RealD, shift ,
RealD, resid,
RealD, ChebyLow,
RealD, ChebyHigh,
@@ -117,12 +118,9 @@ public:
InvertNonHermitianLinearOperator(Matrix &Mat,RealD stp=1e-8): _Mat(Mat),_stp(stp){};
// Support for coarsening to a multigrid
void OpDiag (const Field &in, Field &out) {
// _Mat.Mdiag(in,out);
// out = out + shift*in;
assert(0);
}
void OpDir (const Field &in, Field &out,int dir,int disp) {
// _Mat.Mdir(in,out,dir,disp);
assert(0);
}
void OpDirAll (const Field &in, std::vector<Field> &out){
@@ -131,11 +129,6 @@ public:
};
void Op (const Field &in, Field &out){
Field tmp(in.Grid());
// _Mat.M(in,out);
// RealD mass=-shift;
// WilsonCloverFermionD Dw(Umu, Grid, RBGrid, mass, csw_r, csw_t);
// NonHermitianLinearOperator<Matrix,Field> HermOp(_Mat);
// BiCGSTAB<Field> CG(_stp,10000);
_Mat.Mdag(in,tmp);
MdagMLinearOperator<Matrix,Field> HermOp(_Mat);
ConjugateGradient<Field> CG(_stp,10000);
@@ -341,12 +334,7 @@ int main (int argc, char ** argv)
// Run KrylovSchur and Arnoldi on a Hermitian matrix
std::cout << GridLogMessage << "Running Krylov Schur" << std::endl;
// KrylovSchur KrySchur (Dsq, FGrid, 1e-8, EvalNormLarge);
// KrylovSchur KrySchur (Dsq, FGrid, 1e-8,EvalImNormSmall);
// KrySchur(src, maxIter, Nm, Nk, Nstop);
// KrylovSchur KrySchur (HermOp2, UGrid, resid,EvalNormSmall);
// Hacked, really EvalImagSmall
RealD shift=1.5;
RealD shift=LanParams.shift;
#if 0
KrylovSchur KrySchur (Dwilson, UGrid, resid,EvalImNormSmall);
// KrySchur(src[0], maxIter, Nm, Nk, Nstop);
@@ -357,10 +345,8 @@ int main (int argc, char ** argv)
Nblock=LanParams.Nblock;
bool if_verify=false;
if(LanParams.verify) if_verify=true;
// KrylovSchur KrySchur (Dwilson, UGrid, resid,EvalImNormSmall);
// KrySchur(src, maxIter, Nm, Nk, Nstop,true,if_verify);
BlockKrylovSchur KrySchur (Dwilson, UGrid, resid,EvalImNormSmall);
// HarmonicBlockKrylovSchur KrySchur (Dwilson, UGrid, resid,shift,EvalImNormSmall);
// BlockKrylovSchur KrySchur (Dwilson, UGrid, resid,EvalImNormSmall);
HarmonicBlockKrylovSchur KrySchur (Dwilson, UGrid, resid,shift,EvalNormSmall);
KrySchur(src, maxIter, Nm, Nk, Nstop,Nblock,true,if_verify);
std::cout << GridLogMessage << "BlockKrylovSchur evec.size= " << KrySchur.evecs.size()<< std::endl;
#endif
@@ -61,11 +61,11 @@ int main(int argc, char** argv)
//----------------------------------------------------------------------
// Parameters (kept small so output is readable)
//----------------------------------------------------------------------
const int Nblock = 2;
const int Nm = 12; // total vectors (= 6 blocks * Nblock=2)
const int Nk = 6; // total kept (= 3 blocks * Nblock=2)
const int Nstop = 2;
const int maxIter = 4;
const int Nblock = 4;
const int Nm = 20; // total vectors (= 5 blocks * Nblock=4)
const int Nk = 8; // total kept (= 2 blocks * Nblock=4)
const int Nstop = 4;
const int maxIter = 8;
const RealD tol = 1e-6;
// Two identical starting blocks
@@ -73,7 +73,7 @@ int main(int argc, char** argv)
std::vector<Field> v0b(Nblock, Field(grid));
for (int t = 0; t < Nblock; t++) {
random(RNG, v0[t]);
v0b[t] = v0[t];
v0b[t] = v0[t]; // identical start for fair comparison
}
//----------------------------------------------------------------------
@@ -87,7 +87,7 @@ int main(int argc, char** argv)
std::cout << GridLogMessage
<< "========================================\n" << std::endl;
BlockKrylovSchur<Field> bks(op, grid, tol, EvalReSmall);
BlockKrylovSchur<Field> bks(op, grid, tol, EvalImNormSmall);
bks(v0, maxIter, Nm, Nk, Nstop, Nblock,
/*doubleOrthog=*/true, /*doVerify=*/true);
@@ -108,7 +108,7 @@ int main(int argc, char** argv)
std::cout << GridLogMessage
<< "========================================\n" << std::endl;
HarmonicBlockKrylovSchur<Field> hbks(op, grid, tol, 0.0, EvalNormSmall);
HarmonicBlockKrylovSchur<Field> hbks(op, grid, tol, 0.0, EvalImNormSmall);
hbks(v0b, maxIter, Nm, Nk, Nstop, Nblock,
/*doubleOrthog=*/true, /*doVerify=*/true);