Skip to content

Commit f32fd11

Browse files
committed
Refactor PPCG block subspace solver
1 parent ef80502 commit f32fd11

11 files changed

Lines changed: 1533 additions & 1601 deletions

source/source_hsolver/diago_ppcg.cpp

Lines changed: 6 additions & 1489 deletions
Large diffs are not rendered by default.

source/source_hsolver/diago_ppcg.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ namespace hsolver {
2424
// (File 2 approach).
2525
// BLOCK_SUBSPACE — block subspace diagonalization (File 1 approach).
2626
//
27-
// CONJUGATE_GRADIENT is the default because it is the tested production path.
28-
// BLOCK_SUBSPACE is kept as an explicit experimental strategy.
27+
// BLOCK_SUBSPACE is the production path used by ks_solver=ppcg.
28+
// CONJUGATE_GRADIENT is kept as an explicit fallback strategy.
2929
// -----------------------------------------------------------------------------
3030

3131
enum class PpcgStrategy { BLOCK_SUBSPACE, CONJUGATE_GRADIENT };
@@ -53,7 +53,7 @@ class DiagoPPCG
5353
const int& sbsize,
5454
const int& rr_step,
5555
const bool gamma_g0_real,
56-
const PpcgStrategy strategy = PpcgStrategy::CONJUGATE_GRADIENT);
56+
const PpcgStrategy strategy = PpcgStrategy::BLOCK_SUBSPACE);
5757

5858
// -------------------------------------------------------------------------
5959
// Main entry point
@@ -159,6 +159,8 @@ class DiagoPPCG
159159
std::vector<T> k; // K matrix (projected H)
160160
std::vector<T> m; // M matrix (projected S)
161161
std::vector<Real> eval; // eigenvalues
162+
std::vector<Real> w_scale;
163+
std::vector<Real> p_scale;
162164
};
163165

164166
void lock_epairs(const std::vector<T>& residual,

source/source_hsolver/hsolver_pw.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ double run_ppcg_pw(const HPsiFunc& hpsi_func,
5151
sbsize,
5252
rr_step,
5353
gamma_only,
54-
PpcgStrategy::CONJUGATE_GRADIENT);
54+
PpcgStrategy::BLOCK_SUBSPACE);
5555

5656
return ppcg.diag(hpsi_func,
5757
spsi_func,
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
namespace hsolver {
2+
3+
//==============================================================================
4+
// CONJUGATE_GRADIENT STRATEGY
5+
//==============================================================================
6+
7+
// ---------------------------------------------------------------------------
8+
// Compute gradient: grad_i = H|psi_i> - eps_i * S|psi_i>
9+
// ---------------------------------------------------------------------------
10+
template <typename T, typename Device>
11+
void DiagoPPCG<T, Device>::calc_gradient(
12+
const Real* /*prec*/,
13+
const T* hpsi,
14+
const T* spsi,
15+
const T* /*psi*/,
16+
const Real* eigenvalue,
17+
std::vector<T>& grad) const
18+
{
19+
grad.assign(ld_psi_ * n_band_, T(0));
20+
for (int j = 0; j < n_band_; ++j)
21+
{
22+
const Real ej = eigenvalue[j];
23+
for (int ig = 0; ig < n_dim_; ++ig)
24+
grad[idx(ig, j, ld_psi_)] = hpsi[idx(ig, j, ld_psi_)]
25+
- spsi[idx(ig, j, ld_psi_)] * ej;
26+
}
27+
}
28+
29+
// ---------------------------------------------------------------------------
30+
// Orthogonalize gradient: grad_j -= sum_i <psi_i|grad_j> * S|psi_i>
31+
// ---------------------------------------------------------------------------
32+
template <typename T, typename Device>
33+
void DiagoPPCG<T, Device>::orth_gradient(
34+
const T* psi, const T* spsi,
35+
std::vector<T>& grad) const
36+
{
37+
for (int j = 0; j < n_band_; ++j)
38+
{
39+
for (int i = 0; i < n_band_; ++i)
40+
{
41+
// Full complex inner product <psi_i | grad_j>
42+
T coeff = 0;
43+
const T* pi = psi + i * ld_psi_;
44+
const T* gj = grad.data() + j * ld_psi_;
45+
for (int ig = 0; ig < n_dim_; ++ig)
46+
coeff += std::conj(pi[ig]) * gj[ig];
47+
if (std::abs(coeff) <= std::numeric_limits<Real>::epsilon())
48+
continue;
49+
// grad_j -= S|psi_i> * coeff
50+
const T* si = spsi + i * ld_psi_;
51+
T* gj_out = grad.data() + j * ld_psi_;
52+
for (int ig = 0; ig < n_dim_; ++ig)
53+
gj_out[ig] -= si[ig] * coeff;
54+
}
55+
}
56+
}
57+
58+
// ---------------------------------------------------------------------------
59+
// Polak-Ribiere conjugate gradient update with preconditioning:
60+
// z_new = -P^{-1} * r_new
61+
// beta = max(0, <z_new, r_new - r_old> / <z_old, r_old>)
62+
// d_new = z_new + beta * d_old
63+
// ---------------------------------------------------------------------------
64+
template <typename T, typename Device>
65+
void DiagoPPCG<T, Device>::update_polak_ribiere(
66+
const std::vector<T>& grad,
67+
std::vector<T>& p,
68+
std::vector<T>& grad_old,
69+
std::vector<T>& z_old,
70+
std::vector<Real>& beta_denom,
71+
const Real* prec) const
72+
{
73+
const bool first_iter = p.empty();
74+
if (first_iter)
75+
{
76+
p.assign(ld_psi_ * n_band_, T(0));
77+
z_old.assign(ld_psi_ * n_band_, T(0));
78+
beta_denom.assign(n_band_, std::numeric_limits<Real>::infinity());
79+
}
80+
81+
std::vector<T> z_new(ld_psi_ * n_band_, T(0));
82+
83+
for (int j = 0; j < n_band_; ++j)
84+
{
85+
const T* g = grad.data() + j * ld_psi_;
86+
T* pj = p.data() + j * ld_psi_;
87+
T* zn = z_new.data() + j * ld_psi_;
88+
T* zo = z_old.data() + j * ld_psi_;
89+
90+
Real beta_num_zr = 0;
91+
Real beta_num_zo = 0;
92+
93+
for (int ig = 0; ig < n_dim_; ++ig)
94+
{
95+
// z_new = -P^{-1} * grad
96+
T z = -g[ig] / std::max(prec[ig], static_cast<Real>(1.0e-12));
97+
zn[ig] = z;
98+
99+
// r_old = -P * z_old (recover old raw residual)
100+
T r_old = -prec[ig] * zo[ig];
101+
102+
beta_num_zr += static_cast<Real>(std::real(z * std::conj(g[ig])));
103+
beta_num_zo += static_cast<Real>(std::real(z * std::conj(r_old)));
104+
}
105+
106+
Real beta = 0;
107+
const Real denom = beta_denom[j];
108+
if (denom > static_cast<Real>(1.0e-30))
109+
{
110+
beta = (beta_num_zr - beta_num_zo) / denom;
111+
if (beta < 0)
112+
beta = 0;
113+
}
114+
115+
// d_new = z_new + beta * d_old
116+
for (int ig = 0; ig < n_dim_; ++ig)
117+
pj[ig] = zn[ig] + beta * pj[ig];
118+
119+
// Save <z_new, r_new> as denominator for next iteration.
120+
beta_denom[j] = beta_num_zr + static_cast<Real>(1.0e-30);
121+
}
122+
123+
// Persist state for next iteration.
124+
z_old.swap(z_new);
125+
grad_old = grad;
126+
}
127+
128+
// ---------------------------------------------------------------------------
129+
// Line minimization along search direction:
130+
// For each band j: find optimal step α by minimizing the Rayleigh quotient
131+
// in the 2D subspace spanned by |psi_j> and |p_j>.
132+
//
133+
// The Rayleigh quotient:
134+
// R(α) = (h_ii + 2α h_ip + α² h_pp) / (s_ii + 2α s_ip + α² s_pp)
135+
//
136+
// Setting dR/dα = 0 gives a QUADRATIC equation A α² + B α + C = 0 with:
137+
// A = s_ip * h_pp - h_ip * s_pp
138+
// B = s_ii * h_pp - h_ii * s_pp
139+
// C = s_ii * h_ip - h_ii * s_ip
140+
//
141+
// The linear approximation α = -C / B (dropping the α² term) picks one of
142+
// the two stationary points more-or-less arbitrarily. For bands far from
143+
// convergence this can select the MAXIMUM, driving ψ toward high-energy
144+
// states. We solve the full quadratic and explicitly pick the root with
145+
// the lower Rayleigh quotient.
146+
//
147+
// Update: |psi> += α |p>
148+
// H|psi> += α H|p>
149+
// S|psi> += α S|p>
150+
// ---------------------------------------------------------------------------
151+
template <typename T, typename Device>
152+
void DiagoPPCG<T, Device>::line_minimize(
153+
T* psi, T* hpsi, T* spsi,
154+
const T* p, const T* hp, const T* sp,
155+
int ncol) const
156+
{
157+
for (int j = 0; j < ncol; ++j)
158+
{
159+
const int off = j * ld_psi_;
160+
T* pj = psi + off;
161+
T* hj = hpsi + off;
162+
T* sj = spsi + off;
163+
const T* pp = p + off;
164+
const T* hpp = hp + off;
165+
const T* spp = sp + off;
166+
167+
Real h_ii = gamma_dot(pj, hj);
168+
Real s_ii = gamma_dot(pj, sj);
169+
const T h_ip_c = complex_dot(pj, hpp);
170+
const T s_ip_c = complex_dot(pj, spp);
171+
Real h_pp = gamma_dot(pp, hpp);
172+
Real s_pp = gamma_dot(pp, spp);
173+
174+
// Rotate the search direction so the first-order Rayleigh quotient
175+
// derivative is real. The scalar alpha solve below stays unchanged for
176+
// real problems, while complex PW states can use a complex step.
177+
T phase = T(1);
178+
const Real lambda = h_ii / std::max(s_ii, static_cast<Real>(1e-30));
179+
const T q = h_ip_c - T(lambda) * s_ip_c;
180+
const Real q_abs = std::abs(q);
181+
if (q_abs > static_cast<Real>(1e-30))
182+
phase = std::conj(q) / q_abs;
183+
184+
Real h_ip = static_cast<Real>(std::real(phase * h_ip_c));
185+
Real s_ip = static_cast<Real>(std::real(phase * s_ip_c));
186+
187+
// Coefficients of A alpha^2 + B alpha + C = 0
188+
const Real A = s_ip * h_pp - h_ip * s_pp;
189+
const Real B = s_ii * h_pp - h_ii * s_pp;
190+
const Real C = s_ii * h_ip - h_ii * s_ip;
191+
192+
auto ray_quot = [&](Real a) -> Real {
193+
return (h_ii + static_cast<Real>(2) * a * h_ip + a * a * h_pp)
194+
/ std::max(s_ii + static_cast<Real>(2) * a * s_ip + a * a * s_pp,
195+
static_cast<Real>(1e-30));
196+
};
197+
198+
Real alpha = 0;
199+
Real alpha_linear = (std::abs(B) > static_cast<Real>(1e-30))
200+
? -C / B : static_cast<Real>(0);
201+
202+
const Real tol = std::numeric_limits<Real>::epsilon() * static_cast<Real>(100);
203+
if (std::abs(A) > tol * std::max(static_cast<Real>(1), std::abs(B)))
204+
{
205+
const Real disc = B * B - static_cast<Real>(4) * A * C;
206+
if (disc >= static_cast<Real>(0))
207+
{
208+
const Real sqrt_disc = std::sqrt(disc);
209+
const Real a1 = (-B + sqrt_disc) / (static_cast<Real>(2) * A);
210+
const Real a2 = (-B - sqrt_disc) / (static_cast<Real>(2) * A);
211+
212+
const Real r1 = ray_quot(a1);
213+
const Real r2 = ray_quot(a2);
214+
const Real r_lin = ray_quot(alpha_linear);
215+
216+
if (r1 < r2 && r1 < r_lin)
217+
alpha = a1;
218+
else if (r2 < r1 && r2 < r_lin)
219+
alpha = a2;
220+
else
221+
alpha = alpha_linear;
222+
}
223+
else
224+
{
225+
alpha = alpha_linear;
226+
}
227+
}
228+
else
229+
{
230+
alpha = alpha_linear;
231+
}
232+
233+
for (int ig = 0; ig < n_dim_; ++ig)
234+
{
235+
const T step = T(alpha) * phase;
236+
pj[ig] += step * pp[ig];
237+
hj[ig] += step * hpp[ig];
238+
sj[ig] += step * spp[ig];
239+
}
240+
}
241+
}
242+
243+
// ---------------------------------------------------------------------------
244+
// Cholesky orthonormalization (S-orthonormal):
245+
// 1. Form S-gram matrix J = psi^H * S * psi
246+
// 2. Cholesky: J = U^T * U (upper)
247+
// 3. Invert U: U^{-1}
248+
// 4. psi *= U^{-1}, Hpsi *= U^{-1}, Spsi *= U^{-1}
249+
// ---------------------------------------------------------------------------
250+
template <typename T, typename Device>
251+
void DiagoPPCG<T, Device>::orth_cholesky(
252+
T* psi, T* hpsi, T* spsi, int ncol) const
253+
{
254+
// Save original vectors in case Cholesky fails numerically.
255+
std::vector<T> psi_orig(psi, psi + ld_psi_ * ncol);
256+
std::vector<T> hpsi_orig(hpsi, hpsi + ld_psi_ * ncol);
257+
std::vector<T> spsi_orig(spsi, spsi + ld_psi_ * ncol);
258+
259+
// Gram matrix of S-orthonormality: J_{ij} = <psi_i | S | psi_j>
260+
std::vector<T> gram_s(ncol * ncol, T(0));
261+
for (int j = 0; j < ncol; ++j)
262+
for (int i = 0; i < ncol; ++i)
263+
gram_s[i + j * ncol] = complex_dot(psi + i * ld_psi_,
264+
spsi + j * ld_psi_);
265+
266+
bool cholesky_ok = false;
267+
try
268+
{
269+
HermitianLapack<T>::potrf(ncol, gram_s.data());
270+
HermitianLapack<T>::trtri(ncol, gram_s.data());
271+
272+
std::vector<T> tmp(ld_psi_ * ncol, T(0));
273+
for (int j = 0; j < ncol; ++j)
274+
for (int i = 0; i < ncol; ++i) {
275+
const T uinv = gram_s[i + j * ncol];
276+
for (int ig = 0; ig < n_dim_; ++ig)
277+
tmp[idx(ig, j, ld_psi_)] += psi[idx(ig, i, ld_psi_)] * uinv;
278+
}
279+
std::copy(tmp.begin(), tmp.end(), psi);
280+
281+
set_zero(tmp);
282+
for (int j = 0; j < ncol; ++j)
283+
for (int i = 0; i < ncol; ++i) {
284+
const T uinv = gram_s[i + j * ncol];
285+
for (int ig = 0; ig < n_dim_; ++ig)
286+
tmp[idx(ig, j, ld_psi_)] += hpsi[idx(ig, i, ld_psi_)] * uinv;
287+
}
288+
std::copy(tmp.begin(), tmp.end(), hpsi);
289+
290+
set_zero(tmp);
291+
for (int j = 0; j < ncol; ++j)
292+
for (int i = 0; i < ncol; ++i) {
293+
const T uinv = gram_s[i + j * ncol];
294+
for (int ig = 0; ig < n_dim_; ++ig)
295+
tmp[idx(ig, j, ld_psi_)] += spsi[idx(ig, i, ld_psi_)] * uinv;
296+
}
297+
std::copy(tmp.begin(), tmp.end(), spsi);
298+
299+
cholesky_ok = is_s_orthonormal(psi, spsi, ncol);
300+
}
301+
catch (const std::runtime_error&) { cholesky_ok = false; }
302+
303+
if (!cholesky_ok)
304+
{
305+
std::copy(psi_orig.begin(), psi_orig.end(), psi);
306+
std::copy(hpsi_orig.begin(), hpsi_orig.end(), hpsi);
307+
std::copy(spsi_orig.begin(), spsi_orig.end(), spsi);
308+
s_gram_schmidt(psi, hpsi, spsi, ncol);
309+
}
310+
}
311+
312+
} // namespace hsolver

0 commit comments

Comments
 (0)