votca 2026-dev
Loading...
Searching...
No Matches
bse_fullsolver.h
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20#pragma once
21#ifndef VOTCA_XTP_BSE_FULLSOLVER_H
22#define VOTCA_XTP_BSE_FULLSOLVER_H
23
24// Standard includes
25#include <algorithm>
26#include <chrono>
27#include <cmath>
28#include <numeric>
29#include <stdexcept>
30#include <vector>
31
32// Third party includes
33#include <boost/format.hpp>
34
35// Local VOTCA includes
36#include "eigen.h"
37#include "logger.h"
39
40namespace votca {
41namespace xtp {
42
47inline Index NormalizeExcitationVectors(Eigen::MatrixXd& X,
48 Eigen::MatrixXd& Y) {
49 Index problems = 0;
50 for (Index i = 0; i < X.cols(); ++i) {
51 const double norm = X.col(i).squaredNorm() - Y.col(i).squaredNorm();
52 const double scale = X.col(i).squaredNorm() + Y.col(i).squaredNorm();
53 if (norm > 1e-12 * scale && scale > 0.0) {
54 const double f = 1.0 / std::sqrt(norm);
55 X.col(i) *= f;
56 Y.col(i) *= f;
57 continue;
58 }
59 ++problems;
60 if (std::abs(norm) > 1e-12 * scale && scale > 0.0) {
61 const double f = 1.0 / std::sqrt(std::abs(norm));
62 X.col(i) *= f;
63 Y.col(i) *= f;
64 }
65 }
66 return problems;
67}
68
92 public:
93 struct Options {
94 double tolerance = 1e-4; // residual norm (Ha) per normalised vector
96 Index max_subspace = 0; // 0: 10 * nroots
97 };
98
99 FullBSEDavidson(Logger& log, Options opt) : log_(log), opt_(opt) {}
100
101 Index Iterations() const { return iterations_; }
103
104 template <class OpA, class OpB>
105 tools::EigenSystem Solve(const OpA& A, const OpB& B, Index nroots) {
106 const auto start = std::chrono::steady_clock::now();
107 const Index n = A.rows();
108 nroots = std::min(nroots, n);
109 // A few guard roots above the requested ones, converged to a looser
110 // tolerance: without them the highest requested root can converge to a
111 // state above a root the subspace has not seen yet.
112 const Index nwork = std::min(n, nroots + std::max<Index>(2, nroots / 4));
113 const Index max_subspace = std::min(
114 n, std::max(opt_.max_subspace > 0 ? opt_.max_subspace : 10 * nroots,
115 6 * nwork));
116
117 // Diagonal estimate w_i^2 = (a-b)(a+b) for the initial vectors and the
118 // preconditioner.
119 const Eigen::VectorXd adiag = A.diagonal();
120 const Eigen::VectorXd bdiag = B.diagonal();
121 std::vector<Index> order(static_cast<std::size_t>(n));
122 std::iota(order.begin(), order.end(), 0);
123 auto estimate = [&](Index i) {
124 return std::max(0.0, (adiag(i) - bdiag(i)) * (adiag(i) + bdiag(i)));
125 };
126 std::stable_sort(order.begin(), order.end(), [&](Index i, Index j) {
127 return estimate(i) < estimate(j);
128 });
129 const Index nguess = std::min(n, 2 * nwork);
130 Eigen::MatrixXd V = Eigen::MatrixXd::Zero(n, nguess);
131 for (Index i = 0; i < nguess; ++i) {
132 V(order[std::size_t(i)], i) = 1.0;
133 }
134 Eigen::MatrixXd P; // (A+B) V
135 Eigen::MatrixXd Q; // (A-B) V
136 Extend(A, B, V, P, Q, V);
137
139 << TimeStamp() << " Full BSE (Hermitian form, " << n << " pairs, "
140 << nroots << " roots, tolerance " << opt_.tolerance << ")"
141 << std::flush;
142
143 Eigen::VectorXd omega;
144 Eigen::MatrixXd x; // subspace coefficients of |X+Y>
145 Eigen::MatrixXd y; // subspace coefficients of |X-Y>
146 Eigen::MatrixXd XpY;
147 Eigen::MatrixXd XmY;
148 bool converged = false;
149 for (iterations_ = 1; iterations_ <= opt_.max_iterations; ++iterations_) {
150 ProjectedRoots(V, P, Q, nwork, omega, x, y);
151 XpY = V * x;
152 XmY = V * y;
153 const Eigen::MatrixXd R1 = P * x - XmY * omega.asDiagonal();
154 const Eigen::MatrixXd R2 = Q * y - XpY * omega.asDiagonal();
155 const Index nr = omega.size();
156 Eigen::VectorXd res(nr);
157 for (Index i = 0; i < nr; ++i) {
158 res(i) = std::max(R1.col(i).norm() / XpY.col(i).norm(),
159 R2.col(i).norm() / XmY.col(i).norm());
160 }
161 // Requested roots to the tolerance. Guard roots are refined towards
162 // 10 x tolerance, but once the requested roots are converged a guard
163 // within 100 x tolerance is enough to stop: a guard in a degenerate
164 // set cut by the guard window can otherwise take several more
165 // iterations without changing the requested roots.
166 auto Done = [&](Index i) {
167 return res(i) < (i < nroots ? 1.0 : 10.0) * opt_.tolerance;
168 };
169 Index nconv = 0;
170 for (Index i = 0; i < nroots; ++i) {
171 nconv += Done(i) ? 1 : 0;
172 }
173 const double guard_res =
174 (nr > nroots) ? res.tail(nr - nroots).maxCoeff() : 0.0;
175 const bool all_done =
176 nconv == nroots && guard_res < 100.0 * opt_.tolerance;
178 << TimeStamp()
179 << boost::format(
180 " iter %1$3d subspace %2$5d max residual "
181 "%3$4.2e converged %4$4d/%5$d guard roots %6$4.2e") %
182 iterations_ % V.cols() % res.head(nroots).maxCoeff() % nconv %
183 nroots % guard_res
184 << std::flush;
185 if (all_done) {
186 converged = true;
187 break;
188 }
189
190 // Corrections from the diagonal approximation A ~ a, B ~ b of
191 // (A+B) dp - w dm = -r1, (A-B) dm - w dp = -r2,
192 // solved per pair (2x2 system with determinant a^2 - b^2 - w^2).
193 std::vector<Eigen::VectorXd> directions;
194 for (Index i = 0; i < nr; ++i) {
195 if (Done(i)) {
196 continue;
197 }
198 const double w = omega(i);
199 Eigen::ArrayXd det =
200 adiag.array().square() - bdiag.array().square() - w * w;
201 det = det.sign() * det.abs().max(1e-4) +
202 (det == 0.0).cast<double>() * 1e-4;
203 const Eigen::ArrayXd r1 = R1.col(i).array();
204 const Eigen::ArrayXd r2 = R2.col(i).array();
205 directions.push_back(
206 (-((adiag - bdiag).array() * r1 + w * r2) / det).matrix());
207 directions.push_back(
208 (-(w * r1 + (adiag + bdiag).array() * r2) / det).matrix());
209 }
210 Eigen::MatrixXd T(n, Index(directions.size()));
211 for (Index i = 0; i < T.cols(); ++i) {
212 T.col(i) = directions[std::size_t(i)];
213 }
214
215 if (V.cols() + T.cols() > max_subspace) {
216 // thick restart: keep the Ritz pairs of twice as many roots
217 Eigen::VectorXd omega_keep;
218 Eigen::MatrixXd x_keep;
219 Eigen::MatrixXd y_keep;
220 ProjectedRoots(V, P, Q, std::min(2 * nwork, V.cols()), omega_keep,
221 x_keep, y_keep);
222 Collapse(V, P, Q, x_keep, y_keep);
223 }
224 const Index before = V.cols();
225 Extend(A, B, T, P, Q, V);
226 if (V.cols() == before) {
228 << TimeStamp()
229 << " Full BSE: no new search directions, stopping with max "
230 "residual "
231 << res.head(nroots).maxCoeff() << std::flush;
232 break;
233 }
234 }
235 if (!converged) {
236 ProjectedRoots(V, P, Q, nwork, omega, x, y);
237 XpY = V * x;
238 XmY = V * y;
240 << TimeStamp() << " WARNING: full BSE not converged after "
241 << opt_.max_iterations << " iterations" << std::flush;
242 }
243
244 // X = (|X+Y> + |X-Y>)/2, Y = (|X+Y> - |X-Y>)/2; x^T y = 1 gives
245 // X^T X - Y^T Y = 1.
246 tools::EigenSystem result;
247 const Index nout = std::min(nroots, Index(omega.size()));
248 result.eigenvalues() = omega.head(nout);
249 result.eigenvectors() = 0.5 * (XpY + XmY).leftCols(nout);
250 result.eigenvectors2() = 0.5 * (XpY - XmY).leftCols(nout);
251 const double seconds =
252 std::chrono::duration<double>(std::chrono::steady_clock::now() - start)
253 .count();
255 << TimeStamp()
256 << " Full BSE done: " << std::min(iterations_, opt_.max_iterations)
257 << " iterations, " << applications_ << " vectors through A and B, "
258 << seconds << " s" << std::flush;
259 return result;
260 }
261
262 private:
263 // Orthonormalises T against V and itself (two Gram-Schmidt passes), drops
264 // directions without a significant new component, applies A and B to the
265 // rest and appends them to V, P and Q.
266 template <class OpA, class OpB>
267 void Extend(const OpA& A, const OpB& B, Eigen::MatrixXd T, Eigen::MatrixXd& P,
268 Eigen::MatrixXd& Q, Eigen::MatrixXd& V) {
269 const bool fresh = (P.cols() == 0);
270 Eigen::MatrixXd basis = fresh ? Eigen::MatrixXd(V.rows(), 0) : V;
271 std::vector<Index> kept;
272 Eigen::MatrixXd accepted(V.rows(), 0);
273 for (Index i = 0; i < T.cols(); ++i) {
274 Eigen::VectorXd t = T.col(i);
275 const double norm0 = t.norm();
276 if (norm0 == 0.0) {
277 continue;
278 }
279 for (int pass = 0; pass < 2; ++pass) {
280 if (basis.cols() > 0) {
281 t -= basis * (basis.transpose() * t);
282 }
283 if (accepted.cols() > 0) {
284 t -= accepted * (accepted.transpose() * t);
285 }
286 }
287 const double norm = t.norm();
288 if (norm < 1e-8 * norm0 || norm < 1e-12) {
289 continue;
290 }
291 accepted.conservativeResize(Eigen::NoChange, accepted.cols() + 1);
292 accepted.col(accepted.cols() - 1) = t / norm;
293 }
294 if (accepted.cols() == 0) {
295 if (fresh) {
296 V.resize(V.rows(), 0);
297 }
298 return;
299 }
300 const Eigen::MatrixXd At = A.matmul(accepted);
301 const Eigen::MatrixXd Bt = B.matmul(accepted);
302 applications_ += accepted.cols();
303 if (fresh) {
304 V = accepted;
305 P = At + Bt;
306 Q = At - Bt;
307 return;
308 }
309 const Index m = V.cols();
310 const Index k = accepted.cols();
311 V.conservativeResize(Eigen::NoChange, m + k);
312 P.conservativeResize(Eigen::NoChange, m + k);
313 Q.conservativeResize(Eigen::NoChange, m + k);
314 V.rightCols(k) = accepted;
315 P.rightCols(k) = At + Bt;
316 Q.rightCols(k) = At - Bt;
317 }
318
319 void ProjectedRoots(const Eigen::MatrixXd& V, const Eigen::MatrixXd& P,
320 const Eigen::MatrixXd& Q, Index nroots,
321 Eigen::VectorXd& omega, Eigen::MatrixXd& x,
322 Eigen::MatrixXd& y) const {
323 Eigen::MatrixXd Mp = V.transpose() * P;
324 Eigen::MatrixXd Mm = V.transpose() * Q;
325 Mp = 0.5 * (Mp + Mp.transpose()).eval();
326 Mm = 0.5 * (Mm + Mm.transpose()).eval();
327 Eigen::LLT<Eigen::MatrixXd> llt(Mm);
328 if (llt.info() != Eigen::Success) {
329 throw std::runtime_error(
330 "FullBSEDavidson: projected A-B is not positive definite");
331 }
332 const Eigen::MatrixXd L = llt.matrixL();
333 Eigen::MatrixXd H = L.transpose() * Mp * L;
334 H = 0.5 * (H + H.transpose()).eval();
335 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(H);
336 const Index nr = std::min(nroots, Index(es.eigenvalues().size()));
337 if (es.eigenvalues()(0) <= 0.0) {
338 throw std::runtime_error(
339 "FullBSEDavidson: non-positive root, A+B is not positive definite "
340 "(instability of the reference)");
341 }
342 omega = es.eigenvalues().head(nr).cwiseSqrt();
343 const Eigen::MatrixXd u = es.eigenvectors().leftCols(nr);
344 // x = L u / sqrt(w), y = L^-T u sqrt(w): M+ x = w y, M- y = w x, x^T y = 1
345 x = L * u * omega.cwiseSqrt().cwiseInverse().asDiagonal();
346 y = L.transpose().triangularView<Eigen::Upper>().solve(u) *
347 omega.cwiseSqrt().asDiagonal();
348 }
349
350 // Replaces the subspace by an orthonormal basis of the current |X+Y> and
351 // |X-Y> Ritz vectors; P and Q follow by the same linear combination.
352 static void Collapse(Eigen::MatrixXd& V, Eigen::MatrixXd& P,
353 Eigen::MatrixXd& Q, const Eigen::MatrixXd& x,
354 const Eigen::MatrixXd& y) {
355 Eigen::MatrixXd S(x.rows(), x.cols() + y.cols());
356 S << x, y;
357 Eigen::ColPivHouseholderQR<Eigen::MatrixXd> qr(S);
358 const Index rank = qr.rank();
359 const Eigen::MatrixXd C = Eigen::MatrixXd(qr.householderQ()).leftCols(rank);
360 V = (V * C).eval();
361 P = (P * C).eval();
362 Q = (Q * C).eval();
363 }
364
369};
370
371} // namespace xtp
372} // namespace votca
373
374#endif // VOTCA_XTP_BSE_FULLSOLVER_H
const Eigen::MatrixXd & eigenvectors2() const
Definition eigensystem.h:36
const Eigen::VectorXd & eigenvalues() const
Definition eigensystem.h:30
const Eigen::MatrixXd & eigenvectors() const
Definition eigensystem.h:33
void Extend(const OpA &A, const OpB &B, Eigen::MatrixXd T, Eigen::MatrixXd &P, Eigen::MatrixXd &Q, Eigen::MatrixXd &V)
void ProjectedRoots(const Eigen::MatrixXd &V, const Eigen::MatrixXd &P, const Eigen::MatrixXd &Q, Index nroots, Eigen::VectorXd &omega, Eigen::MatrixXd &x, Eigen::MatrixXd &y) const
static void Collapse(Eigen::MatrixXd &V, Eigen::MatrixXd &P, Eigen::MatrixXd &Q, const Eigen::MatrixXd &x, const Eigen::MatrixXd &y)
tools::EigenSystem Solve(const OpA &A, const OpB &B, Index nroots)
FullBSEDavidson(Logger &log, Options opt)
Logger is used for thread-safe output of messages.
Definition logger.h:164
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
#define XTP_LOG(level, log)
Definition logger.h:40
Index NormalizeExcitationVectors(Eigen::MatrixXd &X, Eigen::MatrixXd &Y)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26