votca 2026-dev
Loading...
Searching...
No Matches
rpa_uks.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2021 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#include "votca/xtp/rpa_uks.h"
22
23#include <algorithm>
24#include <cmath>
25#include <iomanip>
26#include <sstream>
27
28#include "votca/xtp/aomatrix.h"
30#include "votca/xtp/vc2index.h"
31
32namespace {
33
34constexpr double kUKSApBPrefactor = 2.0;
35
36} // namespace
37
38namespace votca {
39namespace xtp {
40
41void RPA_UKS::UpdateRPAInputEnergies(const Eigen::VectorXd& dftenergies_alpha,
42 const Eigen::VectorXd& dftenergies_beta,
43 const Eigen::VectorXd& gwaenergies_alpha,
44 const Eigen::VectorXd& gwaenergies_beta,
45 Index qpmin) {
46 // Total number of orbitals retained in the RPA window.
47 const Index rpatotal = rpamax_ - rpamin_ + 1;
48
49 // Start from the DFT energies in the configured RPA window for each spin.
50 energies_alpha_ = dftenergies_alpha.segment(rpamin_, rpatotal);
51 energies_beta_ = dftenergies_beta.segment(rpamin_, rpatotal);
52
53 // Number of GW-corrected states available in the qp window for each spin.
54 const Index gwsize_alpha = Index(gwaenergies_alpha.size());
55 const Index gwsize_beta = Index(gwaenergies_beta.size());
56
57 // Replace DFT energies by GW energies where available.
58 energies_alpha_.segment(qpmin - rpamin_, gwsize_alpha) = gwaenergies_alpha;
59 energies_beta_.segment(qpmin - rpamin_, gwsize_beta) = gwaenergies_beta;
60
61 // Outside the explicitly corrected qp window, shift the remaining occupied
62 // and virtual states (OutOfWindowShift), independently for alpha and beta.
64 qpmin, gwsize_alpha);
65 ShiftUncorrectedEnergies(energies_beta_, dftenergies_beta, homo_beta_, qpmin,
66 gwsize_beta);
67
69}
70
79
81 const Eigen::VectorXd*& omegas,
82 const std::vector<Eigen::VectorXd>*& modes) const {
83 if (!screening_cached_) {
85 }
86 omegas = &screening_omegas_;
87 modes = &screening_modes_;
88}
89
91 const rpa_eigensolution& rpa_solution = Diagonalize_H2p();
92 const Eigen::MatrixXd& XpY = rpa_solution.XpY;
93 const Eigen::VectorXd& omegas = rpa_solution.omega;
94
95 const Index lumo_alpha = homo_alpha_ + 1;
96 const Index lumo_beta = homo_beta_ + 1;
97
98 const Index n_occ_alpha = lumo_alpha - rpamin_;
99 const Index n_occ_beta = lumo_beta - rpamin_;
100 const Index n_unocc_alpha = rpamax_ - homo_alpha_;
101 const Index n_unocc_beta = rpamax_ - homo_beta_;
102
103 const Index size_alpha = n_occ_alpha * n_unocc_alpha;
104 const Index auxsize = Mmn_.alpha.auxsize();
105 const Index nmodes = Index(omegas.size());
106
107 // modes Z = sum_v M_v,virt^T (X+Y)_v over both spins, as two GEMMs over
108 // the stacked virtual rows of the hole slices
109 auto stacked_rows = [&](const TCMatrix_gwbse& Mmn, Index n_occ,
110 Index n_unocc) {
111 vc2index vc(0, 0, n_unocc);
112 Eigen::MatrixXd M(n_occ * n_unocc, auxsize);
113#pragma omp parallel for schedule(dynamic)
114 for (Index v = 0; v < n_occ; v++) {
115 M.middleRows(vc.I(v, 0), n_unocc) = Mmn[v].middleRows(n_occ, n_unocc);
116 }
117 return M;
118 };
119 const Index size_beta = n_occ_beta * n_unocc_beta;
120 Eigen::MatrixXd Z =
121 stacked_rows(Mmn_.alpha, n_occ_alpha, n_unocc_alpha).transpose() *
122 XpY.topRows(size_alpha);
123 Z.noalias() += stacked_rows(Mmn_.beta, n_occ_beta, n_unocc_beta).transpose() *
124 XpY.middleRows(size_alpha, size_beta);
125
126 std::vector<Eigen::VectorXd> all_modes;
127 all_modes.reserve(nmodes);
128 std::vector<double> all_norms;
129 all_norms.reserve(nmodes);
130 double max_norm = 0.0;
131 for (Index s = 0; s < nmodes; s++) {
132 const double norm = Z.col(s).norm();
133 max_norm = std::max(max_norm, norm);
134 all_modes.push_back(Z.col(s));
135 all_norms.push_back(norm);
136 }
137
138 const double tol = 1e-10 * std::max(1.0, max_norm);
139
140 std::vector<Eigen::VectorXd> active_modes;
141 std::vector<double> active_omegas;
142 active_modes.reserve(nmodes);
143 active_omegas.reserve(nmodes);
144
145 for (Index s = 0; s < nmodes; s++) {
146 if (all_norms[s] > tol) {
147 active_modes.push_back(std::move(all_modes[s]));
148 active_omegas.push_back(omegas(s));
149 }
150 }
151
152 screening_omegas_ = Eigen::VectorXd::Zero(Index(active_omegas.size()));
153 for (Index s = 0; s < screening_omegas_.size(); s++) {
154 screening_omegas_(s) = active_omegas[std::size_t(s)];
155 }
156
157 screening_modes_ = std::move(active_modes);
158 screening_cached_ = true;
159}
160
161void RPA_UKS::ShiftUncorrectedEnergies(Eigen::VectorXd& energies,
162 const Eigen::VectorXd& dftenergies,
163 Index homo, Index qpmin, Index gwsize) {
164 out_of_window_.Apply(energies, dftenergies, rpamin_, rpamax_, homo, qpmin,
165 qpmin + gwsize - 1);
166}
167Eigen::MatrixXd RPA_UKS::ResponseSum(const SpinWeightsFn& weights) const {
168 // The dielectric matrix lives in the auxiliary basis; alpha and beta
169 // channels add, each hole slice v contributing M_v^T diag(w_v) M_v.
170 const Index size = Mmn_.alpha.auxsize();
171 const Index n_occ_alpha = std::max<Index>(0, homo_alpha_ + 1 - rpamin_);
172 const Index n_occ_beta = std::max<Index>(0, homo_beta_ + 1 - rpamin_);
173 const Index n_unocc_alpha = std::max<Index>(0, rpamax_ - homo_alpha_);
174 const Index n_unocc_beta = std::max<Index>(0, rpamax_ - homo_beta_);
175 const Index nalpha = (n_unocc_alpha > 0) ? n_occ_alpha : 0;
176 const Index nbeta = (n_unocc_beta > 0) ? n_occ_beta : 0;
177 OptionalTiming timing(timings_, "screening: epsilon (RPA)");
178
179 auto spin_of = [&](Index block, Index& m) {
180 if (block < nalpha) {
181 m = block;
182 return false;
183 }
184 m = block - nalpha;
185 return true;
186 };
187
189 nalpha + nbeta, size,
190 [&](Index block, Eigen::VectorXd& w) {
191 Index m = 0;
192 const bool beta = spin_of(block, m);
193 weights(beta, m, w);
194 },
195 [&](Index block, const WeightedGram::RowsVisitor& use) {
196 Index m = 0;
197 const bool beta = spin_of(block, m);
198 if (beta) {
199 use(Mmn_.beta[m].bottomRows(n_unocc_beta));
200 } else {
201 use(Mmn_.alpha[m].bottomRows(n_unocc_alpha));
202 }
203 });
204}
205
206template <bool imag>
207Eigen::MatrixXd RPA_UKS::calculate_epsilon(double frequency) const {
208 const double freq2 = frequency * frequency;
209 const double eta2 = eta_ * eta_;
210 Eigen::MatrixXd result =
211 ResponseSum([&](bool beta, Index m_level, Eigen::VectorXd& denom) {
212 const Eigen::VectorXd& energies =
214 const Index homo = beta ? homo_beta_ : homo_alpha_;
215 const Index n_unocc = rpamax_ - homo;
216 // Bare particle-hole energy differences Delta_e = e_a - e_i
217 const Eigen::ArrayXd deltaE =
218 energies.tail(n_unocc).array() - energies(m_level);
219 if (imag) {
220 // 2 Delta_e / (Delta_e^2 + w^2) per spin channel (the closed-shell
221 // code has 4: both spins folded into one channel)
222 denom = 2.0 * deltaE / (deltaE.square() + freq2);
223 } else {
224 // (Delta_e - w)/((Delta_e - w)^2 + eta^2)
225 // + (Delta_e + w)/((Delta_e + w)^2 + eta^2)
226 Eigen::ArrayXd deltEf = deltaE - frequency;
227 Eigen::ArrayXd sum = deltEf / (deltEf.square() + eta2);
228 deltEf = deltaE + frequency;
229 sum += deltEf / (deltEf.square() + eta2);
230 denom = sum;
231 }
232 });
233 // epsilon = 1 - v chi0: the identity on the auxiliary-space diagonal
234 result.diagonal().array() += 1.0;
235 return result;
236}
237
238template Eigen::MatrixXd RPA_UKS::calculate_epsilon<true>(
239 double frequency) const;
240template Eigen::MatrixXd RPA_UKS::calculate_epsilon<false>(
241 double frequency) const;
242
244 std::complex<double> frequency) const {
245 // Real part at complex frequency z = w + i*gamma: terms of
246 // Re[1 / (w + i*gamma - Delta_e)] and its partner at -Delta_e.
247 const double sigma_1 = std::pow(frequency.imag() + eta_, 2);
248 const double sigma_2 = std::pow(frequency.imag() - eta_, 2);
249 Eigen::MatrixXd result =
250 ResponseSum([&](bool beta, Index m_level, Eigen::VectorXd& chi) {
251 const Eigen::VectorXd& energies =
253 const Index homo = beta ? homo_beta_ : homo_alpha_;
254 const Index n_unocc = rpamax_ - homo;
255 const Eigen::ArrayXd deltaE =
256 energies.tail(n_unocc).array() - energies(m_level);
257 const Eigen::ArrayXd deltaEm = frequency.real() - deltaE;
258 const Eigen::ArrayXd deltaEp = frequency.real() + deltaE;
259 // the overall factor -1 (restricted convention per spin channel)
260 // is folded into the weights
261 chi = -(deltaEm * (deltaEm.abs2() + sigma_1).inverse() -
262 deltaEp * (deltaEp.abs2() + sigma_2).inverse());
263 });
264 result.diagonal().array() += 1.0;
265 return result;
266}
267
269 if (h2p_cached_) {
270 return h2p_solution_cache_;
271 }
272 // AmB contains the bare particle-hole energy differences and is diagonal in
273 // the particle-hole basis.
274 Eigen::VectorXd AmB = Calculate_H2p_AmB();
275
276 // ApB adds the Coulomb coupling between particle-hole states, including
277 // alpha-alpha, beta-beta, and mixed alpha-beta / beta-alpha blocks.
278 Eigen::MatrixXd ApB = Calculate_H2p_ApB();
279
281 sol.ERPA_correlation = -0.25 * (ApB.trace() + AmB.sum());
282
283 // Build the symmetrized Hermitian matrix
284 //
285 // C = sqrt(AmB) (ApB) sqrt(AmB)
286 //
287 // exactly as done in the restricted code path.
288 Eigen::MatrixXd& C = ApB;
289 C.applyOnTheLeft(AmB.cwiseSqrt().asDiagonal());
290 C.applyOnTheRight(AmB.cwiseSqrt().asDiagonal());
291
293
294 sol.omega = Eigen::VectorXd::Zero(es.values.size());
295 sol.omega = es.values.cwiseSqrt();
296 sol.ERPA_correlation += 0.5 * sol.omega.sum();
297
299 << " Lowest neutral excitation energy (eV): "
300 << tools::conv::hrt2ev * sol.omega.minCoeff()
301 << std::flush;
302
304 << TimeStamp()
305 << " RPA correlation energy (Hartree): " << sol.ERPA_correlation
306 << std::flush;
307
308 const Index rpasize = Index(AmB.size());
309 sol.XpY = Eigen::MatrixXd(rpasize, rpasize);
310
311 // Reconstruct X+Y eigenvectors from the symmetrized eigenvectors.
312 const Eigen::VectorXd AmB_sqrt = AmB.cwiseSqrt();
313 const Eigen::VectorXd Omega_sqrt_inv = sol.omega.cwiseSqrt().cwiseInverse();
314 for (Index s = 0; s < rpasize; s++) {
315 sol.XpY.col(s) =
316 Omega_sqrt_inv(s) * AmB_sqrt.cwiseProduct(es.vectors.col(s));
317 }
318
319 h2p_solution_cache_ = std::move(sol);
320 h2p_cached_ = true;
321 return h2p_solution_cache_;
322}
323
324Eigen::VectorXd RPA_UKS::Calculate_H2p_AmB() const {
325 const Index lumo_alpha = homo_alpha_ + 1;
326 const Index lumo_beta = homo_beta_ + 1;
327
328 const Index n_occ_alpha = lumo_alpha - rpamin_;
329 const Index n_occ_beta = lumo_beta - rpamin_;
330 const Index n_unocc_alpha = rpamax_ - lumo_alpha + 1;
331 const Index n_unocc_beta = rpamax_ - lumo_beta + 1;
332
333 // Total number of alpha and beta particle-hole excitations in the selected
334 // window.
335 const Index size_alpha = n_occ_alpha * n_unocc_alpha;
336 const Index size_beta = n_occ_beta * n_unocc_beta;
337
338 Eigen::VectorXd AmB = Eigen::VectorXd::Zero(size_alpha + size_beta);
339
340 // alpha block:
341 // excitation basis index enumerates all alpha (v -> c) combinations
342 vc2index vc_alpha(0, 0, n_unocc_alpha);
343 for (Index v = 0; v < n_occ_alpha; v++) {
344 const Index i = vc_alpha.I(v, 0);
345 AmB.segment(i, n_unocc_alpha) =
346 energies_alpha_.segment(n_occ_alpha, n_unocc_alpha).array() -
348 }
349
350 // beta block:
351 // appended after all alpha excitations
352 vc2index vc_beta(0, 0, n_unocc_beta);
353 for (Index v = 0; v < n_occ_beta; v++) {
354 const Index i = size_alpha + vc_beta.I(v, 0);
355 AmB.segment(i, n_unocc_beta) =
356 energies_beta_.segment(n_occ_beta, n_unocc_beta).array() -
358 }
359
360 return AmB;
361}
362
363Eigen::MatrixXd RPA_UKS::Calculate_H2p_ApB() const {
364 const Index lumo_alpha = homo_alpha_ + 1;
365 const Index lumo_beta = homo_beta_ + 1;
366
367 const Index n_occ_alpha = lumo_alpha - rpamin_;
368 const Index n_occ_beta = lumo_beta - rpamin_;
369 const Index n_unocc_alpha = rpamax_ - lumo_alpha + 1;
370 const Index n_unocc_beta = rpamax_ - lumo_beta + 1;
371
372 const Index size_alpha = n_occ_alpha * n_unocc_alpha;
373 const Index size_beta = n_occ_beta * n_unocc_beta;
374
375 Eigen::MatrixXd ApB =
376 Eigen::MatrixXd::Zero(size_alpha + size_beta, size_alpha + size_beta);
377
378 vc2index vc_alpha(0, 0, n_unocc_alpha);
379 vc2index vc_beta(0, 0, n_unocc_beta);
380
381 // alpha-alpha block:
382 // Coulomb coupling between alpha particle-hole excitations.
383#pragma omp parallel for schedule(guided)
384 for (Index v2 = 0; v2 < n_occ_alpha; v2++) {
385 const Index i2 = vc_alpha.I(v2, 0);
386 const Eigen::MatrixXd Mmn_v2T =
387 Mmn_.alpha[v2].middleRows(n_occ_alpha, n_unocc_alpha).transpose();
388
389 for (Index v1 = v2; v1 < n_occ_alpha; v1++) {
390 const Index i1 = vc_alpha.I(v1, 0);
391
392 // Only a single factor 2.0 is kept here from the algebraic A+B
393 // structure. The closed-shell spin degeneracy factor of the restricted
394 // implementation is not present anymore; spin is represented explicitly
395 // by separate alpha and beta blocks.
396 ApB.block(i1, i2, n_unocc_alpha, n_unocc_alpha) =
397 kUKSApBPrefactor *
398 Mmn_.alpha[v1].middleRows(n_occ_alpha, n_unocc_alpha) * Mmn_v2T;
399 }
400 }
401
402 // beta-beta block:
403#pragma omp parallel for schedule(guided)
404 for (Index v2 = 0; v2 < n_occ_beta; v2++) {
405 const Index i2 = size_alpha + vc_beta.I(v2, 0);
406 const Eigen::MatrixXd Mmn_v2T =
407 Mmn_.beta[v2].middleRows(n_occ_beta, n_unocc_beta).transpose();
408
409 for (Index v1 = v2; v1 < n_occ_beta; v1++) {
410 const Index i1 = size_alpha + vc_beta.I(v1, 0);
411 ApB.block(i1, i2, n_unocc_beta, n_unocc_beta) =
412 kUKSApBPrefactor *
413 Mmn_.beta[v1].middleRows(n_occ_beta, n_unocc_beta) * Mmn_v2T;
414 }
415 }
416
417 // alpha-beta block:
418 // In RPA, Coulomb coupling acts on total density fluctuations and therefore
419 // couples alpha and beta particle-hole sectors as well.
420#pragma omp parallel for schedule(guided)
421 for (Index v_beta = 0; v_beta < n_occ_beta; v_beta++) {
422 const Index i_beta = size_alpha + vc_beta.I(v_beta, 0);
423 const Eigen::MatrixXd Mmn_beta_T =
424 Mmn_.beta[v_beta].middleRows(n_occ_beta, n_unocc_beta).transpose();
425
426 for (Index v_alpha = 0; v_alpha < n_occ_alpha; v_alpha++) {
427 const Index i_alpha = vc_alpha.I(v_alpha, 0);
428 ApB.block(i_alpha, i_beta, n_unocc_alpha, n_unocc_beta) =
429 kUKSApBPrefactor *
430 Mmn_.alpha[v_alpha].middleRows(n_occ_alpha, n_unocc_alpha) *
431 Mmn_beta_T;
432 }
433 }
434
435 // Symmetrize alpha-alpha block from its computed lower triangle.
436 ApB.block(0, 0, size_alpha, size_alpha)
437 .template triangularView<Eigen::StrictlyUpper>() =
438 ApB.block(0, 0, size_alpha, size_alpha)
439 .transpose()
440 .template triangularView<Eigen::StrictlyUpper>();
441
442 // Symmetrize beta-beta block from its computed lower triangle.
443 ApB.block(size_alpha, size_alpha, size_beta, size_beta)
444 .template triangularView<Eigen::StrictlyUpper>() =
445 ApB.block(size_alpha, size_alpha, size_beta, size_beta)
446 .transpose()
447 .template triangularView<Eigen::StrictlyUpper>();
448
449 // Copy the mixed alpha-beta block into the beta-alpha block.
450 ApB.block(size_alpha, 0, size_beta, size_alpha) =
451 ApB.block(0, size_alpha, size_alpha, size_beta).transpose();
452
453 // Add the diagonal bare transition energies.
454 ApB.diagonal() += Calculate_H2p_AmB();
455
456 return ApB;
457}
458
460 const Eigen::MatrixXd& C) const {
462 << TimeStamp() << " Diagonalizing two-particle Hamiltonian "
463 << std::flush;
464
466
468 << TimeStamp() << " Diagonalization done " << std::flush;
469
470 const double minCoeff = es.values.minCoeff();
471 if (minCoeff <= 0.0) {
473 << TimeStamp() << " Detected non-positive eigenvalue: " << minCoeff
474 << std::flush;
475 throw std::runtime_error("Detected non-positive eigenvalue.");
476 }
477
478 return es;
479}
480
481} // namespace xtp
482} // namespace votca
void BuildCachedScreeningModes() const
Definition rpa_uks.cc:90
Eigen::VectorXd screening_omegas_
Definition rpa_uks.h:370
Eigen::VectorXd energies_beta_
Definition rpa_uks.h:351
std::function< void(bool, Index, Eigen::VectorXd &)> SpinWeightsFn
Definition rpa_uks.h:361
Eigen::MatrixXd ResponseSum(const SpinWeightsFn &weights) const
Definition rpa_uks.cc:167
const TCMatrix_gwbse_spin & Mmn_
Definition rpa_uks.h:357
Eigen::MatrixXd calculate_epsilon(double frequency) const
Internal implementation of epsilon(w).
Definition rpa_uks.cc:207
const double eta_
Definition rpa_uks.h:347
rpa_eigensolution h2p_solution_cache_
Definition rpa_uks.h:366
void InvalidateH2pCache()
Definition rpa_uks.cc:71
std::vector< Eigen::VectorXd > screening_modes_
Definition rpa_uks.h:371
void GetCachedScreeningModes(const Eigen::VectorXd *&omegas, const std::vector< Eigen::VectorXd > *&modes) const
Definition rpa_uks.cc:80
Eigen::VectorXd Calculate_H2p_AmB() const
Construct the diagonal AmB block of the two-particle Hamiltonian.
Definition rpa_uks.cc:324
Eigen::VectorXd energies_alpha_
Definition rpa_uks.h:350
DFTTimings * timings_
Definition rpa_uks.h:358
void ShiftUncorrectedEnergies(Eigen::VectorXd &energies, const Eigen::VectorXd &dftenergies, Index homo, Index qpmin, Index gwsize)
Shift uncorrected states outside the GW window.
Definition rpa_uks.cc:161
OutOfWindowShift out_of_window_
Definition rpa_uks.h:336
Eigen::MatrixXd Calculate_H2p_ApB() const
Construct the ApB block of the unrestricted two-particle Hamiltonian.
Definition rpa_uks.cc:363
const rpa_eigensolution & Diagonalize_H2p() const
Diagonalize the unrestricted two-particle Hamiltonian.
Definition rpa_uks.cc:268
Eigen::MatrixXd calculate_epsilon_r(double frequency) const
Dielectric matrix on the real frequency axis.
Definition rpa_uks.h:131
SymmetricEigenSystem Diagonalize_H2p_C(const Eigen::MatrixXd &C) const
Diagonalize the symmetrized two-particle matrix C.
Definition rpa_uks.cc:459
void UpdateRPAInputEnergies(const Eigen::VectorXd &dftenergies_alpha, const Eigen::VectorXd &dftenergies_beta, const Eigen::VectorXd &gwaenergies_alpha, const Eigen::VectorXd &gwaenergies_beta, Index qpmin)
Update alpha and beta RPA input energies from DFT and GW energies.
Definition rpa_uks.cc:41
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
std::function< void(const Eigen::Ref< const Eigen::MatrixXd > &)> RowsVisitor
Calls use(R_m); R_m may be a block of stored data or a temporary.
static Eigen::MatrixXd Compute(Index nblocks, Index ncols, const WeightsFn &weights, const RowsFn &rows)
This class transforms a pair of indices v,c into a compound index I, via I=ctotal*v+c the fast dimens...
Definition vc2index.h:36
Index I(Index v, Index c) const
Definition vc2index.h:42
#define XTP_LOG(level, log)
Definition logger.h:40
const double hrt2ev
Definition constants.h:53
Charge transport classes.
Definition ERIs.h:28
SymmetricEigenSystem SymmetricEigen(const Eigen::MatrixXd &A, bool vectors=true)
With vectors = false only the eigenvalues are computed.
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
Solution of the two-particle Hamiltonian in the unrestricted basis.
Definition rpa_uks.h:252