votca 2026-dev
Loading...
Searching...
No Matches
gw.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2023 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// Standard includes
21#include <algorithm>
22#include <chrono>
23#include <cmath>
24#include <fstream>
25#include <iostream>
26
27// Local VOTCA includes
31#include "votca/xtp/citations.h"
33#include "votca/xtp/gw.h"
36#include "votca/xtp/rpa.h"
39
40namespace votca {
41namespace xtp {
42
43void GW::configure(const options& opt) {
44 opt_ = opt;
45
46 // Normalize legacy and new grid-search settings once at configuration time.
47 // From this point on, the solver operates only on the decoupled canonical
48 // controls, independent of how the user specified them in XML.
50 qptotal_ = opt_.qpmax - opt_.qpmin + 1;
51 rpa_.configure(opt_.homo, opt_.rpamin, opt_.rpamax);
52 rpa_.setOutOfWindowShift(opt_.out_of_window_shift);
53 sigma_ = SigmaFactory().Create(opt_.sigma_integration, Mmn_, rpa_);
54 sigma_->setTimings(&timings_);
55 rpa_.setTimings(&timings_);
56 Sigma_base::options sigma_opt;
57 sigma_opt.homo = opt_.homo;
58 sigma_opt.qpmax = opt_.qpmax;
59 sigma_opt.qpmin = opt_.qpmin;
60 sigma_opt.rpamin = opt_.rpamin;
61 sigma_opt.rpamax = opt_.rpamax;
62 sigma_opt.eta = opt_.eta;
63 sigma_opt.alpha = opt_.alpha;
64 sigma_opt.quadrature_scheme = opt_.quadrature_scheme;
65 sigma_opt.order = opt_.order;
66 sigma_->configure(sigma_opt);
67 Sigma_x_ = Eigen::MatrixXd::Zero(qptotal_, qptotal_);
68 Sigma_c_ = Eigen::MatrixXd::Zero(qptotal_, qptotal_);
69 Sigma_reac_ = Eigen::MatrixXd::Zero(qptotal_, qptotal_);
70}
71
72void GW::setReactionField(const Eigen::MatrixXd& R) {
75 : Eigen::MatrixXd();
76}
77
79 if (dressing_.size() > 0 && !Mmn_.Dressed()) {
80 Mmn_.DressAuxIndex(dressing_);
82 << TimeStamp()
83 << " Dressed 3c integrals with (1+R)^1/2 for environment screening"
84 << std::flush;
85 }
86}
87
89 const Index homo_rel = opt_.homo - opt_.qpmin;
91 << TimeStamp()
92 << " Environment reaction field, static COH+SEX, diagonal [eV]:"
93 << std::flush;
94 for (Index i = 0; i < qptotal_; i++) {
95 std::string level = " Level";
96 if (i == homo_rel) {
97 level = " HOMO ";
98 } else if (i == homo_rel + 1) {
99 level = " LUMO ";
100 }
102 << level
103 << (boost::format(" = %1$4d Sigma_reac = %2$+1.6f") % (i + opt_.qpmin) %
105 .str()
106 << std::flush;
107 }
108 if (homo_rel >= 0 && homo_rel + 1 < qptotal_) {
110 << (boost::format(" HOMO %1$+1.4f eV, LUMO %2$+1.4f eV, gap %3$+1.4f "
111 "eV") %
112 (Sigma_reac_(homo_rel, homo_rel) * tools::conv::hrt2ev) %
113 (Sigma_reac_(homo_rel + 1, homo_rel + 1) * tools::conv::hrt2ev) %
114 ((Sigma_reac_(homo_rel + 1, homo_rel + 1) -
115 Sigma_reac_(homo_rel, homo_rel)) *
117 .str()
118 << std::flush;
119 }
120}
121
122double GW::CalcHomoLumoShift(Eigen::VectorXd frequencies) const {
123 double DFTgap = dft_energies_(opt_.homo + 1) - dft_energies_(opt_.homo);
124 double QPgap = frequencies(opt_.homo + 1 - opt_.qpmin) -
125 frequencies(opt_.homo - opt_.qpmin);
126 return QPgap - DFTgap;
127}
128
129Eigen::MatrixXd GW::getHQP() const {
130 return Sigma_x_ + Sigma_c_ + Sigma_reac_ - vxc_ +
131 Eigen::MatrixXd(
132 dft_energies_.segment(opt_.qpmin, qptotal_).asDiagonal());
133}
134
135Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> GW::DiagonalizeQPHamiltonian()
136 const {
137 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> qpdiag(getHQP());
138 PrintQP_Energies(qpdiag.eigenvalues());
139 return qpdiag;
140}
141
143 Eigen::VectorXd gwa_energies = getGWAResults();
144 double shift = CalcHomoLumoShift(gwa_energies);
145
147 << (boost::format(
148 " ====== Perturbative quasiparticle energies (Hartree) ====== "))
149 .str()
150 << std::flush;
152 << (boost::format(" DeltaHLGap = %1$+1.6f Hartree") % shift).str()
153 << std::flush;
154
155 for (Index i = 0; i < qptotal_; i++) {
156 std::string level = " Level";
157 if ((i + opt_.qpmin) == opt_.homo) {
158 level = " HOMO ";
159 } else if ((i + opt_.qpmin) == opt_.homo + 1) {
160 level = " LUMO ";
161 }
162
163 // With an environment, its static part of the correlation gets a
164 // column of its own, S-R; S-C stays the dynamic correlation.
165 const std::string env =
166 (reaction_field_.size() > 0)
167 ? (boost::format(" S-R = %1$+1.4f") % Sigma_reac_(i, i)).str()
168 : std::string();
170 << level
171 << (boost::format(" = %1$4d DFT = %2$+1.4f VXC = %3$+1.4f S-X = "
172 "%4$+1.4f S-C = %5$+1.4f") %
173 (i + opt_.qpmin) % dft_energies_(i + opt_.qpmin) % vxc_(i, i) %
174 Sigma_x_(i, i) % Sigma_c_(i, i))
175 .str()
176 << env << (boost::format(" GWA = %1$+1.4f") % gwa_energies(i)).str()
177 << std::flush;
178 }
179 return;
180}
181
182void GW::PrintQSGW_Composition(double threshold) const {
184 << TimeStamp()
185 << " ====== QSGW QP state composition (dominant KS contributions) ======"
186 << std::flush;
187
188 for (Index n = 0; n < qptotal_; n++) {
189 const Index abs_n = n + opt_.qpmin;
190 std::string level = " Level";
191 if (abs_n == opt_.homo)
192 level = " HOMO ";
193 else if (abs_n == opt_.homo + 1)
194 level = " LUMO ";
195
196 // Collect contributions above threshold, sorted by weight descending
197 Eigen::VectorXd weights = qsgw_rotation_.col(n).cwiseAbs2();
198 std::vector<std::pair<double, Index>> contribs;
199 for (Index m = 0; m < qptotal_; m++) {
200 if (weights(m) >= threshold) {
201 contribs.push_back({weights(m), m + opt_.qpmin});
202 }
203 }
204 std::sort(contribs.begin(), contribs.end(),
205 [](const auto& a, const auto& b) { return a.first > b.first; });
206
207 std::string line;
208 for (const auto& [w, ks] : contribs) {
209 std::string ks_label;
210 if (ks == opt_.homo)
211 ks_label = "(HOMO)";
212 else if (ks == opt_.homo + 1)
213 ks_label = "(LUMO)";
214
215 line += (boost::format(" %1$5.1f%% KS_%2$d%3$s") % (100.0 * w) % ks %
216 ks_label)
217 .str();
218 }
220 << level << (boost::format(" = %1$4d:") % abs_n).str() << line
221 << std::flush;
222 }
223}
224
225void GW::PrintQSGW_Energies(const std::string& seed_label,
226 const Eigen::VectorXd& seed_energies,
227 const Eigen::VectorXd& qsgw_energies) const {
229 << TimeStamp() << " QSGW quasiparticle energies" << std::flush;
231 << (boost::format(
232 " ====== QSGW quasiparticle energies (Hartree) ====== "))
233 .str()
234 << std::flush;
235
236 for (Index i = 0; i < qptotal_; i++) {
237 std::string level = " Level";
238 if ((i + opt_.qpmin) == opt_.homo) {
239 level = " HOMO ";
240 } else if ((i + opt_.qpmin) == opt_.homo + 1) {
241 level = " LUMO ";
242 }
244 << level
245 << (boost::format(" = %1$4d %2$s = %3$+1.6f QSGW = %4$+1.6f") %
246 (i + opt_.qpmin) % seed_label % seed_energies(i) % qsgw_energies(i))
247 .str()
248 << std::flush;
249 }
250}
251
252void GW::PrintQP_Energies(const Eigen::VectorXd& qp_diag_energies) const {
253 Eigen::VectorXd gwa_energies = getGWAResults();
255 << TimeStamp() << " Full quasiparticle Hamiltonian " << std::flush;
257 << (boost::format(
258 " ====== Diagonalized quasiparticle energies (Hartree) "
259 "====== "))
260 .str()
261 << std::flush;
262 for (Index i = 0; i < qptotal_; i++) {
263 std::string level = " Level";
264 if ((i + opt_.qpmin) == opt_.homo) {
265 level = " HOMO ";
266 } else if ((i + opt_.qpmin) == opt_.homo + 1) {
267 level = " LUMO ";
268 }
270 << level
271 << (boost::format(" = %1$4d PQP = %2$+1.6f DQP = %3$+1.6f ") %
272 (i + opt_.qpmin) % gwa_energies(i) % qp_diag_energies(i))
273 .str()
274 << std::flush;
275 }
276 return;
277}
278
280 const Eigen::VectorXd& dft_energies) const {
281 Eigen::VectorXd shifted_energies = dft_energies;
282 shifted_energies.segment(opt_.homo + 1, dft_energies.size() - opt_.homo - 1)
283 .array() += opt_.shift;
284 return shifted_energies;
285}
286
288 timings_.Reset();
289 qsgw_final_energies_.resize(0);
290 {
291 auto t = timings_.Measure("exchange Sigma_x");
292 Sigma_x_ = (1 - opt_.ScaHFX) * sigma_->CalcExchangeMatrix();
293 if (core_exchange_.size() > 0) {
294 Sigma_x_ += (1 - opt_.ScaHFX) * core_exchange_;
295 }
296 }
298 << TimeStamp() << " Calculated Hartree exchange contribution"
299 << std::flush;
300 // The environment's static correlation, W - v restricted to v_reac:
301 // part of Sigma_c by its physics, held apart because it is frequency
302 // independent. Not scaled by ScaHFX: the DFT starting point has no
303 // reaction field to double count.
304 if (reaction_field_.size() > 0) {
305 auto t = timings_.Measure("reaction field Sigma_reac");
306 Sigma_reac_ = sigma_->CalcReactionFieldMatrix(reaction_field_);
308 }
309 // From here on the RPA and Sigma_c see u = v + v_reac.
310 {
311 auto t = timings_.Measure("dress Mmn for the environment");
313 }
314 // dftenergies has size aobasissize
315 // rpaenergies/Mmn have size rpatotal
316 // gwaenergies/frequencies have size qptotal
317 // homo index is relative to dft_energies
319 << TimeStamp() << " Scissor shifting DFT energies by: " << opt_.shift
320 << " Hrt" << std::flush;
321 Eigen::VectorXd dft_shifted_energies = ScissorShift_DFTlevel(dft_energies_);
322 rpa_.setRPAInputEnergies(
323 dft_shifted_energies.segment(opt_.rpamin, opt_.rpamax - opt_.rpamin + 1));
324 Eigen::VectorXd frequencies =
325 dft_shifted_energies.segment(opt_.qpmin, qptotal_);
326
327 Anderson mixing_;
328 mixing_.Configure(opt_.gw_mixing_order, opt_.gw_mixing_alpha);
329 // The QP energies of the last QP solve and the RPA energies it saw (G and
330 // W); after the solve, the RPA energies and, with mixing, frequencies are
331 // already the input of the next iteration.
332 Eigen::VectorXd qp_solution = frequencies;
333 Eigen::VectorXd rpa_energies_solved = rpa_.getRPAInputEnergies();
334
335 ScreeningUpdate screening;
336 screening.Configure(ScreeningUpdate::Parse(opt_.screening_update),
337 opt_.screening_update_ratio,
338 opt_.screening_update_max_inner);
339 bool rebuild_screening = true;
340 // QP iterations since the mixing history was (re)started: it restarts when
341 // an adaptive update rebuilds W, since the fixed-point map changes then
342 Index mixing_iteration = 0;
343 Index n_iterations = 0;
344 // root tracking: off in the first iteration and for the verifying full
345 // search after convergence (and for the rest of the run if that search
346 // moves a level)
347 tracked_root_.resize(0);
348 tracking_active_ = false;
350 bool tracking_verified = false;
351 bool verifying = false; // this iteration is the verifying full search
352
353 for (Index i_gw = 0; i_gw < opt_.gw_sc_max_iterations; ++i_gw) {
354 gw_sc_iteration_ = i_gw;
355 tracking_active_ = opt_.qp_root_tracking && i_gw > 0 && !tracking_verified;
356 const Eigen::VectorXd roots_before = tracked_root_;
357 if (opt_.reset_3c > 0 && i_gw % opt_.reset_3c == 0 && i_gw != 0) {
358 auto t = timings_.Measure("rebuild Mmn");
359 Mmn_.Rebuild();
361 << TimeStamp() << " Rebuilding 3c integrals" << std::flush;
362 DressForEnvironment(); // Rebuild leaves them bare
363 rebuild_screening = true; // the screening frame is gone too
364 }
365 if (rebuild_screening) {
366 if (i_gw == 0) {
367 XTP_LOG(Log::error, log_) << TimeStamp() << " Building the screening; "
368 << MemoryBudget::Usage() << std::flush;
369 }
370 {
371 auto t = timings_.Measure("screening: other");
372 sigma_->PrepareScreening();
373 }
374 screening.Built(rpa_.getRPAInputEnergies());
375 if (screening.mode() == ScreeningUpdate::Mode::Adaptive &&
376 screening.builds() > 1) {
377 mixing_ = Anderson();
378 mixing_.Configure(opt_.gw_mixing_order, opt_.gw_mixing_alpha);
379 mixing_iteration = 0;
380 }
381 if (const std::string summary = sigma_->ScreeningSummary();
382 !summary.empty()) {
383 const Log::Level level =
384 (i_gw == 0) ? Log::Level::error : Log::Level::info;
385 XTP_LOG(level, log_) << TimeStamp() << " " << summary << std::flush;
386 }
387 if (i_gw == 0) {
388 XTP_LOG(Log::error, log_) << TimeStamp() << " Screening built; "
389 << MemoryBudget::Usage() << std::flush;
390 }
392 << TimeStamp() << " Calculated screening via RPA (build "
393 << screening.builds() << ")" << std::flush;
394 }
396 << TimeStamp() << " Solving QP equations " << std::flush;
397 const bool mix = opt_.gw_mixing_order > 0 && mixing_iteration > 0;
398 if (mix) {
399 mixing_.UpdateInput(frequencies);
400 }
401
402 const Eigen::VectorXd qp_input = frequencies;
403 {
404 auto t = timings_.Measure("QP equations (Sigma_c diagonal)");
405 frequencies = SolveQP(frequencies);
406 }
407 qp_solution = frequencies;
408 rpa_energies_solved = rpa_.getRPAInputEnergies();
409 ++mixing_iteration;
410 ++n_iterations;
411
412 if (opt_.gw_sc_max_iterations > 1) {
413 Eigen::VectorXd rpa_energies_old = rpa_.getRPAInputEnergies();
414 if (mix) {
415 if (opt_.gw_mixing_order == 1) {
417 << "GWSC using linear mixing with alpha: " << opt_.gw_mixing_alpha
418 << std::flush;
419 } else {
421 << "GWSC using Anderson mixing with history "
422 << opt_.gw_mixing_order << ", alpha: " << opt_.gw_mixing_alpha
423 << std::flush;
424 }
425 mixing_.UpdateOutput(frequencies);
426 Eigen::VectorXd mixed_frequencies = mixing_.MixHistory();
427 rpa_.UpdateRPAInputEnergies(dft_energies_, mixed_frequencies,
428 opt_.qpmin);
429 frequencies = mixed_frequencies;
430
431 } else {
432 XTP_LOG(Log::debug, log_) << "GWSC using plain update " << std::flush;
433 rpa_.UpdateRPAInputEnergies(dft_energies_, frequencies, opt_.qpmin);
434 }
435
436 for (int i = 0; i < frequencies.size(); i++) {
438 << "... GWSC iter " << i_gw << " state " << i << " "
439 << std::setprecision(9) << frequencies(i) << std::flush;
440 }
441
443 << TimeStamp() << " GW_Iteration:" << i_gw
444 << " Shift[Hrt]:" << CalcHomoLumoShift(frequencies) << std::flush;
445 // logs the largest change of the (mixed) energies; the decision is
446 // ScreeningUpdate's, on the residual of the QP solve
447 Converged(rpa_.getRPAInputEnergies(), rpa_energies_old, opt_.gw_sc_limit);
448 const double residual = (qp_solution - qp_input).cwiseAbs().maxCoeff();
449 XTP_LOG(Log::info, log_) << TimeStamp() << " QP residual (solution - "
450 << "input) max=" << residual << std::flush;
452 << TimeStamp() << " Largest QP residuals (level: input -> solution): "
453 << qp_solver::LargestResiduals(qp_solution, qp_input, opt_.qpmin)
454 << std::flush;
455 double drift = 0.0;
456 const ScreeningUpdate::Next next = screening.Decide(
457 residual, rpa_.getRPAInputEnergies(), opt_.gw_sc_limit, &drift);
458 if (screening.mode() == ScreeningUpdate::Mode::Adaptive) {
460 << TimeStamp() << " Change since the screening was built: " << drift
461 << " Hrt after " << screening.inner() << " iteration(s)"
462 << ((next == ScreeningUpdate::Next::Rebuild) ? "; rebuilding W"
463 : "")
464 << std::flush;
465 }
466 if (verifying && roots_before.size() == qptotal_) {
467 // the full search after a converged tracked run: report levels it
468 // put on another root than tracking did
469 std::vector<Index> moved;
470 for (Index l = 0; l < qptotal_; ++l) {
471 if (std::abs(tracked_root_(l) - roots_before(l)) >
472 10.0 * opt_.gw_sc_limit) {
473 moved.push_back(l + opt_.qpmin);
474 }
475 }
476 if (!moved.empty() && next != ScreeningUpdate::Next::Converged) {
477 IndexParser rp;
479 << TimeStamp() << " QP root tracking: the full search chose "
480 << "other roots for levels " << rp.CreateIndexString(moved)
481 << "; continuing with the full search" << std::flush;
482 }
483 verifying = false;
484 }
485 if (next == ScreeningUpdate::Next::Converged && opt_.qp_root_tracking &&
486 !tracking_verified && i_gw > 0) {
487 // converged on the tracked roots: one QP solve with the full search
488 // (same G and W) checks that they are the roots the full search
489 // chooses; if so it converges at once
490 tracking_verified = true;
491 verifying = true;
492 rebuild_screening = false;
494 << TimeStamp()
495 << " QP root tracking converged; verifying with a full search"
496 << std::flush;
497 if (i_gw < opt_.gw_sc_max_iterations - 1) {
498 continue;
499 }
500 }
502 XTP_LOG(Log::info, log_) << TimeStamp() << " Converged after "
503 << i_gw + 1 << " GW iterations." << std::flush;
504 break;
505 } else if (i_gw == opt_.gw_sc_max_iterations - 1) {
507 << TimeStamp()
508 << " WARNING! GW-self-consistency cycle not converged after "
509 << opt_.gw_sc_max_iterations << " iterations." << std::flush;
511 << TimeStamp() << " Run continues. Inspect results carefully!"
512 << std::flush;
513 break;
514 }
515 rebuild_screening = (next == ScreeningUpdate::Next::Rebuild);
516 }
517 }
518 screening_builds_ = screening.builds();
519 qp_iterations_ = n_iterations;
520 tracking_active_ = false;
521 if (opt_.gw_sc_max_iterations > 1) {
523 << TimeStamp() << " evGW: " << n_iterations << " QP iterations, "
524 << screening.builds() << " screening builds ("
525 << ScreeningUpdate::Name(screening.mode()) << ")"
526 << (opt_.qp_root_tracking
527 ? "; root tracking solved " +
528 std::to_string(tracked_local_total_) + " of " +
529 std::to_string(n_iterations * qptotal_) +
530 " level solves locally"
531 : std::string())
532 << std::flush;
533 }
534 {
535 // Sigma_c of the last QP solve, at its solution and with the energies in
536 // G it saw, so that the PQP energies solve their QP equation. The RPA
537 // energies updated after the solve (input of a next iteration) stay.
539 auto t = timings_.Measure("QP equations (Sigma_c diagonal)");
540 const Eigen::VectorXd rpa_energies_next = rpa_.getRPAInputEnergies();
541 rpa_.setRPAInputEnergies(rpa_energies_solved);
542 Sigma_c_.diagonal() = sigma_->CalcCorrelationDiag(qp_solution);
543 rpa_.setRPAInputEnergies(rpa_energies_next);
544 qp_solution_ = qp_solution;
545 rpa_energies_solved_ = rpa_energies_solved;
546 }
548 timings_.Report(log_, Log::error, "GW timing summary");
549}
550
551Eigen::VectorXd GW::getGWAResults() const {
552 // When QSGW with virtual trimming has run, return the pre-computed merged
553 // energy vector (QSGW for the trimmed window, seed for excluded virtuals).
554 // This avoids recomputing from sigma matrices which were zeroed post-loop.
555 if (qsgw_final_energies_.size() > 0) {
557 }
558 return Sigma_x_.diagonal() + Sigma_c_.diagonal() + Sigma_reac_.diagonal() -
559 vxc_.diagonal() + dft_energies_.segment(opt_.qpmin, qptotal_);
560}
561
563 Index n = 0;
564 for (const auto& c : competing_roots_) {
565 n += c.competing ? 1 : 0;
566 }
567 if (n == 0) {
568 return;
569 }
570 XTP_LOG(level, log_) << TimeStamp() << " " << n
571 << " QP level(s) with two roots of comparable weight "
572 "(second Z at least half the largest):"
573 << std::flush;
574 for (Index i = 0; i < Index(competing_roots_.size()); ++i) {
575 const auto& c = competing_roots_[std::size_t(i)];
576 if (c.competing) {
577 XTP_LOG(level, log_)
578 << (boost::format(" Level = %1$4d chosen %2$+1.6f (Z = %3$.3f), "
579 "other %4$+1.6f (Z = %5$.3f)") %
580 (i + opt_.qpmin) % c.omega % c.Z % c.omega_alt % c.Z_alt)
581 .str()
582 << std::flush;
583 }
584 }
585}
586
587Eigen::VectorXd GW::SolveQP(const Eigen::VectorXd& frequencies) const {
588 sigma_->ResetDiagEvalCounter();
590 Eigen::VectorXd env = Eigen::VectorXd::Zero(qptotal_);
591
592 const Eigen::VectorXd intercepts =
593 dft_energies_.segment(opt_.qpmin, qptotal_) + Sigma_x_.diagonal() +
594 Sigma_reac_.diagonal() - vxc_.diagonal();
595
596 Eigen::VectorXd frequencies_new = frequencies;
597 Eigen::Array<bool, Eigen::Dynamic, 1> converged =
598 Eigen::Array<bool, Eigen::Dynamic, 1>::Zero(qptotal_);
599
600 QPStats total_stats;
601 const bool track = opt_.qp_root_tracking;
602 const bool use_tracked =
603 track && tracking_active_ && tracked_root_.size() == qptotal_;
604 Eigen::VectorXd roots_new = frequencies;
605 Eigen::VectorXd Z_new = Eigen::VectorXd::Zero(qptotal_);
606 Index n_local = 0;
607
608 // HOMO-LUMO midpoint of the current QP guesses (DFT energies where a
609 // level is outside the QP range), for SolveQP_Grid's restricted window.
610 {
611 const Index h = opt_.homo - opt_.qpmin;
612 const double e_homo =
613 (h >= 0 && h < qptotal_) ? frequencies[h] : dft_energies_(opt_.homo);
614 const double e_lumo = (h + 1 >= 0 && h + 1 < qptotal_)
615 ? frequencies[h + 1]
616 : dft_energies_(opt_.homo + 1);
617 qp_midgap_ = 0.5 * (e_homo + e_lumo);
618 }
619
620#ifdef _OPENMP
621 Index use_threads =
623#else
624 Index use_threads = 1;
625#endif
626
627#pragma omp parallel for schedule(dynamic) num_threads(use_threads)
628 for (Index gw_level = 0; gw_level < qptotal_; ++gw_level) {
629
630 double initial_f = frequencies[gw_level];
631 double intercept = intercepts[gw_level];
632 boost::optional<double> newf;
633 QPStats local_stats;
634 double Z_root = 0.0;
635
636 if (use_tracked) {
637 auto cand = SolveQP_Tracked(intercept, initial_f, gw_level, &local_stats);
638 if (cand) {
639 newf = cand->omega;
640 Z_root = cand->Z;
641#pragma omp atomic
642 ++n_local;
643 }
644 }
645 if (!newf && opt_.qp_solver == "fixedpoint") {
646 newf = SolveQP_FixedPoint(intercept, initial_f, gw_level, &local_stats);
647 }
648 if (newf) {
649 frequencies_new[gw_level] = newf.value();
650 converged[gw_level] = true;
651 } else {
652 QPStats grid_stats;
653 newf = SolveQP_Grid(intercept, initial_f, gw_level, &grid_stats);
654 local_stats.Add(grid_stats);
655 if (newf) {
656 frequencies_new[gw_level] = newf.value();
657 converged[gw_level] = true;
658 } else {
659 QPStats lin_stats;
660 newf =
661 SolveQP_Linearisation(intercept, initial_f, gw_level, &lin_stats);
662 local_stats.Add(lin_stats);
663 if (newf) {
664 frequencies_new[gw_level] = newf.value();
665 }
666 }
667 }
668 if (track && newf) {
669 roots_new[gw_level] = newf.value();
670 if (Z_root <= 0.0) {
671 // weight of a root from the full search, for the next local one
672 QPFunc fqp(gw_level, *sigma_.get(), intercept);
673 const double d = fqp.deriv(newf.value());
674 Z_root = (std::abs(d) > 1e-14) ? -1.0 / d : 0.0;
675 }
676 Z_new[gw_level] = Z_root;
677 }
678
679#pragma omp critical
680 {
681 total_stats.Add(local_stats);
682 }
683 }
684
685 if (track) {
687 (tracked_root_.size() == qptotal_)
688 ? Eigen::VectorXd((roots_new - tracked_root_).cwiseAbs())
689 : Eigen::VectorXd::Zero(qptotal_);
690 tracked_root_ = roots_new;
691 tracked_Z_ = Z_new;
692 tracked_local_ = n_local;
693 tracked_local_total_ += n_local;
694 if (use_tracked) {
696 << TimeStamp() << " QP root tracking: " << n_local << " of "
697 << qptotal_ << " levels solved near their previous root"
698 << std::flush;
699 }
700 }
701
702 if (!converged.all()) {
703 std::vector<Index> states;
704 for (Index s = 0; s < converged.size(); s++) {
705 if (!converged[s]) {
706 states.push_back(s);
707 }
708 }
709 IndexParser rp;
710 XTP_LOG(Log::error, log_) << TimeStamp() << " Not converged PQP states are:"
711 << rp.CreateIndexString(states) << std::flush;
713 << TimeStamp() << " Increase the grid search interval" << std::flush;
714 }
715
717 << " Sigma diagonal evaluations in SolveQP: "
718 << sigma_->GetDiagEvalCounter() << std::flush;
719
720 XTP_LOG(Log::info, log_) << TimeStamp() << " QP diagnostics: " << "scan="
721 << total_stats.sigma_scan_calls
722 << " refine=" << total_stats.sigma_refine_calls
723 << " deriv_sigma="
724 << total_stats.sigma_derivative_calls
725 << " other=" << total_stats.sigma_other_calls
726 << " total_sigma=" << total_stats.TotalSigmaCalls()
727 << " unique_omega="
728 << total_stats.sigma_unique_frequencies
729 << " repeat_sigma=" << total_stats.sigma_repeat_calls
730 << " deriv_calls=" << total_stats.deriv_calls
731 << std::flush;
732
733 return frequencies_new;
734}
735
736boost::optional<qp_solver::RootCandidate> GW::SolveQP_Tracked(
737 double intercept0, double frequency0, Index gw_level,
738 QPStats* stats) const {
739 const double center = tracked_root_(gw_level);
740 // a few times the last change of the root or of the input, within limits
741 const double moved =
742 std::max(tracked_change_(gw_level), std::abs(frequency0 - center));
743 const double halfwidth = std::clamp(4.0 * moved, 0.01, 0.1);
744 QPFunc fqp(gw_level, *sigma_.get(), intercept0);
745 qp_solver::SolverOptions solver_opt;
746 solver_opt.g_sc_limit = opt_.g_sc_limit;
747 solver_opt.qp_bisection_max_iter = opt_.g_sc_max_iterations;
748 solver_opt.qp_dense_spacing = opt_.qp_dense_spacing;
749 auto cand = qp_solver::SolveQP_Tracked(fqp, center, halfwidth,
750 tracked_Z_(gw_level), 0.5, solver_opt,
751 opt_.qp_root_finder == "brent");
752 if (stats != nullptr) {
753 *stats = fqp.GetStats();
754 }
755 return cand;
756}
757
758boost::optional<double> GW::SolveQP_Linearisation(double intercept0,
759 double frequency0,
760 Index gw_level,
761 QPStats* stats) const {
762 boost::optional<double> newf = boost::none;
763
764 QPFunc fqp(gw_level, *sigma_.get(), intercept0);
765
766 // fqp.deriv is f'(w) = dSigma_c/dw - 1
767 newf = qp_solver::LinearisedRoot(frequency0,
768 fqp.value(frequency0, EvalStage::Other),
769 fqp.deriv(frequency0));
770
771 if (stats != nullptr) {
772 *stats = fqp.GetStats();
773 }
774 return newf;
775}
776
778 double intercept0, double frequency0, Index gw_level, double left_limit,
779 double right_limit, bool allow_rejected_return, QPStats* stats) const {
780
781 QPFunc fqp(gw_level, *sigma_.get(), intercept0);
782
783 qp_solver::SolverOptions solver_opt;
784
785 // Pass only canonical search controls into the shared grid-search utility.
786 // RKS and UKS intentionally use the same search semantics; only the sigma
787 // evaluator differs between the two implementations.
788 solver_opt.g_sc_limit = opt_.g_sc_limit;
789 solver_opt.qp_bisection_max_iter = opt_.g_sc_max_iterations;
790 solver_opt.qp_full_window_half_width = opt_.qp_full_window_half_width;
791 solver_opt.qp_dense_spacing = opt_.qp_dense_spacing;
792 solver_opt.qp_adaptive_shell_width = opt_.qp_adaptive_shell_width;
793 solver_opt.qp_adaptive_shell_count = opt_.qp_adaptive_shell_count;
794 solver_opt.prefer_nearest_root =
795 opt_.qp_root_continuity && gw_sc_iteration_ > 0;
796
798 std::vector<QPRootCandidate> accepted_roots;
799 std::vector<QPRootCandidate> rejected_roots;
800
801 bool use_brent = false;
802 if (opt_.qp_root_finder == "brent") {
803 use_brent = true;
804 }
805
807 fqp, frequency0, left_limit, right_limit, gw_sc_iteration_, solver_opt,
808 &wdiag, &accepted_roots, &rejected_roots, use_brent);
809
811#pragma omp critical
812 {
813 if (!accepted_roots.empty() || !rejected_roots.empty()) {
815 << " Adaptive scan qplevel:" << gw_level << " center="
816 << std::max(left_limit, std::min(right_limit, frequency0))
817 << " shells=" << wdiag.shells_explored
818 << " first_interval_shell=" << wdiag.first_interval_shell
819 << " first_accepted_shell=" << wdiag.first_accepted_shell
820 << " intervals=" << wdiag.intervals_found << std::flush;
821 }
822 }
823 }
824
825 if (stats != nullptr) {
826 *stats = fqp.GetStats();
827 }
828
829 if (!accepted_roots.empty()) {
830 if (result) {
831 RecordCompetingRoots(gw_level, accepted_roots, result.value());
832 }
833 return result;
834 }
835
836 if (!rejected_roots.empty() && !allow_rejected_return) {
838#pragma omp critical
839 {
841 << " Adaptive scan qplevel:" << gw_level
842 << " produced only rejected roots in [" << left_limit << ", "
843 << right_limit << "], forcing wider retry" << std::flush;
844 }
845 }
846 return boost::none;
847 }
848
849 return result;
850}
851
852boost::optional<double> GW::SolveQP_Grid_Windowed_Dense(
853 double intercept0, double frequency0, Index gw_level, double left_limit,
854 double right_limit, bool allow_rejected_return, QPStats* stats) const {
855
856 QPFunc fqp(gw_level, *sigma_.get(), intercept0);
857
858 qp_solver::SolverOptions solver_opt;
859
860 // The dense path performs a uniform sign-change scan over the requested
861 // interval. Its spacing is qp_dense_spacing, not the adaptive shell width.
862 solver_opt.g_sc_limit = opt_.g_sc_limit;
863 solver_opt.qp_bisection_max_iter = opt_.g_sc_max_iterations;
864 solver_opt.qp_full_window_half_width = opt_.qp_full_window_half_width;
865 solver_opt.qp_dense_spacing = opt_.qp_dense_spacing;
866 solver_opt.qp_adaptive_shell_width = opt_.qp_adaptive_shell_width;
867 solver_opt.qp_adaptive_shell_count = opt_.qp_adaptive_shell_count;
868 solver_opt.prefer_nearest_root =
869 opt_.qp_root_continuity && gw_sc_iteration_ > 0;
870
871 const bool use_brent = (opt_.qp_root_finder == "brent");
872
873 std::vector<QPRootCandidate> accepted_roots;
874 std::vector<QPRootCandidate> rejected_roots;
875 std::vector<std::pair<double, double>> roots; // omega : Z
876
877 if (left_limit < right_limit) {
878 double freq_prev = left_limit;
879 double targ_prev = fqp.value(freq_prev, EvalStage::Scan);
880
881 // Dense scanning is the robustness path: it uniformly samples the entire
882 // interval to avoid missing sign changes that a coarser adaptive shell
883 // search might step over.
884 const Index n_steps = std::max<Index>(
885 2, static_cast<Index>(
886 std::ceil((right_limit - left_limit) / opt_.qp_dense_spacing)) +
887 1);
888
889 auto node = [&](Index i_node) {
890 return (i_node == n_steps - 1)
891 ? right_limit
892 : std::min(right_limit,
893 left_limit + static_cast<double>(i_node) *
894 opt_.qp_dense_spacing);
895 };
896 {
897 // all scan nodes in one batch
898 std::vector<double> nodes;
899 for (Index i_node = 1; i_node < n_steps; ++i_node) {
900 nodes.push_back(node(i_node));
901 }
902 qp_solver::PrefetchIfAvailable(fqp, nodes, 0);
903 }
904
905 for (Index i_node = 1; i_node < n_steps; ++i_node) {
906 const double freq = node(i_node);
907
908 const double targ = fqp.value(freq, EvalStage::Scan);
909
910 if (targ_prev * targ < 0.0) {
911 auto cand =
912 qp_solver::RefineQPInterval(freq_prev, targ_prev, freq, targ, fqp,
913 frequency0, solver_opt, use_brent);
914 if (cand) {
915 roots.emplace_back(cand->omega, cand->Z);
916 if (cand->accepted) {
917 accepted_roots.push_back(*cand);
918 } else {
919 rejected_roots.push_back(*cand);
920 }
921 }
922 }
923
924 freq_prev = freq;
925 targ_prev = targ;
926 }
927 }
928
930#pragma omp critical
931 {
932 if (accepted_roots.empty() && rejected_roots.empty()) {
934 << " Dense scan qplevel:" << gw_level << " found no roots in ["
935 << left_limit << ", " << right_limit << "]" << std::flush;
936 } else {
938 << " Dense scan qplevel:" << gw_level << " roots (omega:Z)\n\t\t";
939 for (const auto& root : roots) {
940 XTP_LOG(Log::info, log_) << std::setprecision(5) << root.first << ":"
941 << root.second << " ";
942 }
943 XTP_LOG(Log::info, log_) << std::flush;
944 }
945 }
946 }
947
948 if (stats != nullptr) {
949 *stats = fqp.GetStats();
950 }
951
952 if (!accepted_roots.empty()) {
953 const double best = qp_solver::SelectRoot(accepted_roots, solver_opt).omega;
954 RecordCompetingRoots(gw_level, accepted_roots, best);
955 return best;
956 }
957
958 if (!rejected_roots.empty()) {
959 if (!allow_rejected_return) {
961#pragma omp critical
962 {
964 << " Dense scan qplevel:" << gw_level
965 << " produced only rejected roots in [" << left_limit << ", "
966 << right_limit << "], forcing wider retry" << std::flush;
967 }
968 }
969 return boost::none;
970 }
971
972 auto least_bad = std::max_element(
973 rejected_roots.begin(), rejected_roots.end(),
974 [](const QPRootCandidate& a, const QPRootCandidate& b) {
975 return qp_solver::ScoreRoot(a) < qp_solver::ScoreRoot(b);
976 });
977 return least_bad->omega;
978 }
979
980 return boost::none;
981}
982
983boost::optional<double> GW::SolveQP_Grid_Windowed(
984 double intercept0, double frequency0, Index gw_level, double left_limit,
985 double right_limit, bool allow_rejected_return, QPStats* stats) const {
986
987 if (opt_.qp_grid_search_mode == "adaptive") {
988 return SolveQP_Grid_Windowed_Adaptive(intercept0, frequency0, gw_level,
989 left_limit, right_limit,
990 allow_rejected_return, stats);
991 }
992
993 if (opt_.qp_grid_search_mode == "dense") {
994 return SolveQP_Grid_Windowed_Dense(intercept0, frequency0, gw_level,
995 left_limit, right_limit,
996 allow_rejected_return, stats);
997 }
998
999 if (opt_.qp_grid_search_mode == "adaptive_with_dense_fallback") {
1000 QPStats total_stats;
1001 auto adaptive = SolveQP_Grid_Windowed_Adaptive(
1002 intercept0, frequency0, gw_level, left_limit, right_limit,
1003 allow_rejected_return, &total_stats);
1004 if (adaptive) {
1005 if (stats != nullptr) {
1006 *stats = total_stats;
1007 }
1008 return adaptive;
1009 }
1010
1011 QPStats dense_stats;
1012 auto dense = SolveQP_Grid_Windowed_Dense(
1013 intercept0, frequency0, gw_level, left_limit, right_limit,
1014 allow_rejected_return, &dense_stats);
1015 total_stats.Add(dense_stats);
1016
1018#pragma omp critical
1019 {
1021 << " Adaptive QP scan failed for qplevel:" << gw_level
1022 << ", retrying dense grid scan in [" << left_limit << ", "
1023 << right_limit << "]" << std::flush;
1024 }
1025 }
1026
1027 if (stats != nullptr) {
1028 *stats = total_stats;
1029 }
1030 return dense;
1031 }
1032
1033 throw std::runtime_error("Unknown gw.qp_grid_search_mode '" +
1034 opt_.qp_grid_search_mode + "'");
1035}
1036
1037boost::optional<double> GW::SolveQP_Grid(double intercept0, double frequency0,
1038 Index gw_level, QPStats* stats) const {
1039 // The full QP search window is now controlled explicitly and no longer
1040 // inferred from the dense-grid spacing.
1041 const double range = opt_.qp_full_window_half_width;
1042
1043 const double full_left_limit = frequency0 - range;
1044 const double full_right_limit = frequency0 + range;
1045
1046 double restricted_left_limit = full_left_limit;
1047 double restricted_right_limit = full_right_limit;
1048
1049 bool use_restricted_window = false;
1050
1051 if (opt_.qp_restrict_search) {
1052 const Index mo_level = gw_level + opt_.qpmin;
1053 const bool is_occupied = (mo_level <= opt_.homo);
1054
1055 // The fixed bounds (-qp_zero_margin for occupied levels,
1056 // qp_virtual_min_energy for virtual ones) assume the level itself lies
1057 // inside them. An environment can move it outside -- an embedded LUMO
1058 // below -0.1 Ha -- and then every restricted scan fails and falls back
1059 // to the full dense window. In that case only, the bound moves to the
1060 // HOMO-LUMO midpoint, which separates occupied from virtual levels
1061 // wherever they are.
1062 if (is_occupied) {
1063 const double bound = (frequency0 < -opt_.qp_zero_margin)
1064 ? -opt_.qp_zero_margin
1065 : qp_midgap_;
1066 restricted_right_limit = std::min(full_right_limit, bound);
1067 } else {
1068 const double bound = (frequency0 > opt_.qp_virtual_min_energy)
1069 ? opt_.qp_virtual_min_energy
1070 : qp_midgap_;
1071 restricted_left_limit = std::max(full_left_limit, bound);
1072 }
1073
1074 const double tol = 1e-12;
1075 use_restricted_window =
1076 (std::abs(restricted_left_limit - full_left_limit) > tol) ||
1077 (std::abs(restricted_right_limit - full_right_limit) > tol);
1078 }
1079
1080 if (use_restricted_window && restricted_left_limit < restricted_right_limit) {
1081 auto restricted =
1082 SolveQP_Grid_Windowed(intercept0, frequency0, gw_level,
1083 restricted_left_limit, restricted_right_limit,
1084 false, // accepted roots only
1085 stats);
1086 if (restricted) {
1087 return restricted;
1088 }
1089
1091#pragma omp critical
1092 {
1094 << " Restricted QP search failed for qplevel:" << gw_level
1095 << " in window [" << restricted_left_limit << ", "
1096 << restricted_right_limit << "], forcing full dense window ["
1097 << full_left_limit << ", " << full_right_limit << "]" << std::flush;
1098 }
1099 }
1100
1101 // Important: do NOT go back to adaptive here.
1102 // The restricted window already failed to produce an accepted root.
1103 // Force the old robust full-window dense search.
1104 return SolveQP_Grid_Windowed_Dense(intercept0, frequency0, gw_level,
1105 full_left_limit, full_right_limit, true,
1106 stats);
1107 }
1108
1109 return SolveQP_Grid_Windowed(intercept0, frequency0, gw_level,
1110 full_left_limit, full_right_limit, true, stats);
1111}
1112
1113boost::optional<double> GW::SolveQP_FixedPoint(double intercept0,
1114 double frequency0,
1115 Index gw_level,
1116 QPStats* stats) const {
1117 boost::optional<double> newf = boost::none;
1118 QPFunc f(gw_level, *sigma_.get(), intercept0);
1120 opt_.g_sc_max_iterations, opt_.g_sc_limit, opt_.qp_solver_alpha);
1121 double freq_new = newton.FindRoot(f, frequency0);
1122 if (newton.getInfo() == NewtonRapson<QPFunc>::success) {
1123 newf = freq_new;
1124 }
1125 if (stats != nullptr) {
1126 *stats = f.GetStats();
1127 }
1128 return newf;
1129}
1130
1131bool GW::Converged(const Eigen::VectorXd& e1, const Eigen::VectorXd& e2,
1132 double epsilon) const {
1133 Index state = 0;
1134 bool energies_converged = true;
1135 double diff_max = (e1 - e2).cwiseAbs().maxCoeff(&state);
1136 if (diff_max > epsilon) {
1137 energies_converged = false;
1138 }
1139 XTP_LOG(Log::info, log_) << TimeStamp() << " E_diff max=" << diff_max
1140 << " StateNo:" << state << std::flush;
1141 return energies_converged;
1142}
1143
1145 if (!sigma_->HasOffDiagonal()) {
1147 << TimeStamp() << " WARNING: no off-diagonal Sigma_c for sigma "
1148 << "integration " << opt_.sigma_integration
1149 << "; the DQP Hamiltonian has no off-diagonal correlation"
1150 << std::flush;
1151 }
1152 const auto start = std::chrono::steady_clock::now();
1153 Eigen::VectorXd diag_backup = Sigma_c_.diagonal();
1154 Sigma_c_ = sigma_->CalcCorrelationOffDiag(getGWAResults());
1155 Sigma_c_.diagonal() = diag_backup;
1157 << TimeStamp() << " Sigma_c off-diagonal ("
1158 << qptotal_ * (qptotal_ - 1) / 2 << " pairs) took "
1159 << std::chrono::duration<double>(std::chrono::steady_clock::now() - start)
1160 .count()
1161 << " s" << std::flush;
1162}
1163
1164// =============================================================================
1165// GW::CalculateQSGW
1166//
1167// Quasiparticle self-consistent GW: iterates the rotation of the
1168// three-centre integrals until the QP energies are converged.
1169//
1170// Index conventions (same as evGW throughout):
1171// gw_level : 0-based index within the QP window [0, qptotal_)
1172// MO level : absolute MO index = gw_level + opt_.qpmin
1173//
1174// In the DFT-MO basis of the window
1175// H_QSGW = (diag(e_DFT) - V_xc) + U tilde_Sigma U^T,
1176// with U the current QP orbitals (columns, phi_i = sum_k U_ki psi_k) and
1177// tilde_Sigma_ij = Sigma_x,ij + 1/2 [Sigma_c,ij(E_i) + Sigma_c,ij(E_j)]
1178// in the QP basis, E_i the QP energy of phi_i. tilde_Sigma is evaluated
1179// with the integrals rotated to the QP orbitals (both orbital indices), so
1180// external indices, internal sums, energies and screening all refer to the
1181// same orbitals. CalcCorrelationOffDiag returns the symmetrised off-diagonal
1182// part, CalcCorrelationDiag the diagonal.
1183// =============================================================================
1186 << TimeStamp() << " Starting QSGW self-consistency loop " << std::flush;
1187
1189 << TimeStamp() << " QSGW start: qptotal_=" << qptotal_
1190 << " vxc rows=" << vxc_.rows() << " cols=" << vxc_.cols()
1191 << " Sigma_x rows=" << Sigma_x_.rows() << std::flush;
1192
1193 // Initialise: start from evGW/G0W0 seed energies.
1194 const Eigen::VectorXd e_qp_full = getGWAResults(); // full [qpmin,qpmax]
1195 qsgw_seed_energies_ = e_qp_full;
1196
1197 // ── Virtual-level threshold: auto-trim qpmax ─────────────────────────────
1198 // Scan virtual levels from LUMO upward. Exclude the first virtual whose
1199 // perturbative correction |e_QP - e_DFT| exceeds qsgw_max_virt_correction.
1200 // Occupied states are never excluded — large core corrections are physical.
1201 Index qsgw_qpmax = opt_.qpmax;
1202 {
1203 const Index lumo_local = opt_.homo - opt_.qpmin + 1;
1204 bool trimmed = false;
1205 for (Index n = lumo_local; n < qptotal_; n++) {
1206 const double corr =
1207 std::abs(e_qp_full(n) - dft_energies_(opt_.qpmin + n));
1208 if (corr > opt_.qsgw_max_virt_correction) {
1209 qsgw_qpmax = opt_.qpmin + n - 1;
1210 trimmed = true;
1212 << TimeStamp() << " QSGW virtual threshold: level "
1213 << (opt_.qpmin + n)
1214 << " correction = " << corr * votca::tools::conv::hrt2ev
1215 << " eV exceeds "
1216 << opt_.qsgw_max_virt_correction * votca::tools::conv::hrt2ev
1217 << " eV. Trimming QSGW window to [" << opt_.qpmin << ","
1218 << qsgw_qpmax << "]." << std::flush;
1219 break;
1220 }
1221 }
1222 if (!trimmed) {
1224 << TimeStamp() << " QSGW virtual threshold: all corrections within "
1225 << opt_.qsgw_max_virt_correction * votca::tools::conv::hrt2ev
1226 << " eV. Using full window [" << opt_.qpmin << "," << qsgw_qpmax
1227 << "]." << std::flush;
1228 }
1229 }
1230
1231 const Index qsgw_qptotal = qsgw_qpmax - opt_.qpmin + 1;
1232 const bool window_trimmed = (qsgw_qpmax < opt_.qpmax);
1233
1234 // If trimmed, reconfigure sigma for the reduced window
1235 if (window_trimmed) {
1236 Sigma_base::options sigma_opt;
1237 sigma_opt.homo = opt_.homo;
1238 sigma_opt.qpmin = opt_.qpmin;
1239 sigma_opt.qpmax = qsgw_qpmax;
1240 sigma_opt.rpamin = opt_.rpamin;
1241 sigma_opt.rpamax = opt_.rpamax;
1242 sigma_opt.eta = opt_.eta;
1243 sigma_opt.quadrature_scheme = opt_.quadrature_scheme;
1244 sigma_opt.order = opt_.order;
1245 sigma_opt.alpha = opt_.alpha;
1246 sigma_->configure(sigma_opt);
1247 Sigma_x_ = Eigen::MatrixXd::Zero(qsgw_qptotal, qsgw_qptotal);
1248 Sigma_c_ = Eigen::MatrixXd::Zero(qsgw_qptotal, qsgw_qptotal);
1249 }
1250
1251 Eigen::VectorXd e_qp = e_qp_full.head(qsgw_qptotal);
1252 qsgw_rotation_ = Eigen::MatrixXd::Identity(qsgw_qptotal, qsgw_qptotal);
1253
1254 if (opt_.sigma_integration == "cda") {
1255 throw std::runtime_error(
1256 "GW::CalculateQSGW: QSGW is not supported with the CDA sigma "
1257 "integration method. The Gaussian quadrature grid used by CDA becomes "
1258 "ill-conditioned as the QP wavefunctions rotate away from the DFT-MO "
1259 "basis, causing numerical divergence. Use sigma_integration=ppm or "
1260 "sigma_integration=exact instead.");
1261 }
1262 if (opt_.qsgw_regularization == "srg") {
1263 citations::Cite("Marie:2023");
1265 << TimeStamp()
1266 << " QSGW static self-energy: SRG regularization (Marie and Loos), "
1267 "s = "
1268 << opt_.qsgw_srg_s << " Ha^-2 (eta-equivalent "
1269 << std::sqrt(0.5 / opt_.qsgw_srg_s) << " Ha)" << std::flush;
1270 }
1271
1272 // H0 = diag(e_DFT) - V_xc in the DFT-MO basis of the window (the full
1273 // V_xc matrix: H_KS is diagonal there, H_KS - V_xc is not)
1274 Eigen::MatrixXd H0 = -vxc_.topLeftCorner(qsgw_qptotal, qsgw_qptotal);
1275 H0.diagonal() += dft_energies_.segment(opt_.qpmin, qsgw_qptotal);
1276
1277 // Store a copy of the original (unrotated) Mmn_ QP-window slices.
1278 // At each iteration we restore Mmn_ to the DFT-MO state and apply the full
1279 // rotation qsgw_rotation_ in one shot.
1280 // This is necessary because dU (eigenvectors of H_QSGW, always in DFT-MO
1281 // basis) gives the total rotation from DFT-MOs directly, not an incremental
1282 // rotation relative to the previous QP basis.
1283 // Store ALL m-slices in the full RPA window, not just the QP window.
1284 // PrepareScreening (PPM, CDA) calls MultiplyRightWithAuxMatrix which
1285 // overwrites all slices including those outside the QP window (e.g. the
1286 // core level when qpmin > rpamin). If we only restore QP-window slices,
1287 // out-of-window slices accumulate PPM-basis corruption across iterations,
1288 // causing the persistent oscillation seen when qpmin > rpamin.
1289 // Dressed for the correlation part, as in CalculateGWPerturbation --
1290 // the caller may have refilled the integrals since. Exchange and the
1291 // reaction-field self-energy are computed with kernels that undo it.
1293 const Index mtotal = Mmn_.msize();
1294 std::vector<Eigen::MatrixXd> Mmn_orig(mtotal);
1295 for (Index m = 0; m < mtotal; m++) {
1296 Mmn_orig[m] = Mmn_[m];
1297 }
1298 // The auxiliary frame the snapshot is in (PrepareScreening of the seed
1299 // run may have rotated it), restored with it below. The bare Coulomb
1300 // terms are blind to it; an environment reaction field is not.
1301 const Eigen::MatrixXd aux_frame_orig = Mmn_.AuxFrame();
1302
1303 // Hybrid starting points. H_KS = h + J + a K[rho_DFT] + v_xc^sl, and V_xc
1304 // holds only the semilocal part (perturbative GW uses (1 - a) Sigma_x for
1305 // that reason), so diag(e_DFT) - V_xc still contains a K[rho_DFT]. The loop
1306 // adds the full exchange of the current QP orbitals; remove the starting
1307 // point's share once, here, so that H = h + J + Sigma_x[QP] + Sigma_c for
1308 // any functional. K[rho_DFT] is the exchange matrix in the unrotated DFT
1309 // orbitals, with the frozen core (the same terms as in the loop, at
1310 // iteration 0). For a = 0 nothing changes.
1311 if (opt_.ScaHFX > 0.0) {
1312 Eigen::MatrixXd K0 = sigma_->CalcExchangeMatrix();
1313 if (core_exchange_.size() > 0) {
1314 K0 += core_exchange_.topLeftCorner(qsgw_qptotal, qsgw_qptotal);
1315 }
1316 H0 -= opt_.ScaHFX * K0;
1318 << TimeStamp() << " QSGW from a hybrid start: removed " << opt_.ScaHFX
1319 << " x exchange of the DFT orbitals from H0" << std::flush;
1320 if (opt_.rpamin > 0 && core_exchange_.size() == 0) {
1321 // H_KS holds a K of the frozen core too; without core exchange the
1322 // loop neither removes it nor adds the full one back, so the core
1323 // exchange enters with weight a instead of 1 and the fixed point
1324 // depends on the functional.
1326 << TimeStamp()
1327 << " WARNING: QSGW from a hybrid start with a frozen core and "
1328 "core_exchange=false: the exchange with the core stays at "
1329 << opt_.ScaHFX
1330 << " x its full value, so the result depends on the starting "
1331 "functional. Use core_exchange=true."
1332 << std::flush;
1333 }
1334 }
1335
1336 // Hartree: H_KS contains J[rho_DFT]. With hartree update on, the loop adds
1337 // J[rho_QP] - J[rho_DFT] of the active occupied levels (the frozen core
1338 // does not change), so H = h + J[rho_QP] + Sigma_x[QP] + Sigma_c: the
1339 // occupied space is then fully self-consistent.
1340 //
1341 // In an environment H_KS also holds the potential of the dipoles that
1342 // rho_DFT induced in the QM/MM loop. Those must follow the density as
1343 // well: the update is then with u = v + v_reac, the interaction the
1344 // dressed W is built from, J_u[rho_QP] - J_u[rho_DFT]. The response is
1345 // that of the GW reaction field (smeared sites), not of the point Thole
1346 // sites of the ground-state loop; it is linear, so only the change of
1347 // the density enters.
1348 const bool hartree_screened = opt_.qsgw_hartree_update &&
1349 opt_.qsgw_hartree_screened &&
1350 reaction_field_.size() > 0;
1351 Eigen::MatrixXd J0; // J (or J_u) of the DFT orbitals
1352 Eigen::MatrixXd J0_bare; // bare part, for the decomposition in the log
1353 if (opt_.qsgw_hartree_update) {
1354 J0_bare = sigma_->CalcHartreeMatrix();
1355 J0 =
1356 hartree_screened ? sigma_->CalcHartreeMatrix(reaction_field_) : J0_bare;
1357 if (reaction_field_.size() > 0) {
1359 << TimeStamp() << " QSGW Hartree update with "
1360 << (hartree_screened ? "u = v + v_reac (screened)"
1361 : "the bare v (environment does not follow)")
1362 << std::flush;
1363 }
1364 }
1365 // Diagnostics of what drives the environment's effect, per iteration
1366 const Index homo_w = opt_.homo - opt_.qpmin; // HOMO in the window
1367 const Index nocc_w = homo_w + 1;
1368 const Index nvirt_w = qsgw_qptotal - nocc_w;
1369 Eigen::VectorXd sigma_reac_diag0;
1370
1371 // Fixed-point problem in tilde_Sigma (DFT-MO basis of the window):
1372 // S_in -> H = H0 + S_in -> (E, U) -> S_out = F(S_in),
1373 // solved with Anderson/Pulay mixing of the pairs (S_in, S_out); energies
1374 // and orbitals always come from the same H. Converged when the residual
1375 // max|S_out - S_in| and the change of the QP energies are below
1376 // qsgw_sc_limit; the result is the eigensystem of H0 + S_out.
1377 // Reuses gw_mixing_order and gw_mixing_alpha.
1378 Anderson qsgw_mixer;
1379 qsgw_mixer.Configure(opt_.gw_mixing_order, opt_.gw_mixing_alpha);
1381 << TimeStamp() << " QSGW mixer: order=" << opt_.gw_mixing_order
1382 << " alpha=" << opt_.gw_mixing_alpha << std::flush;
1383
1384 // Start consistently: G and W with the seed QP energies in iteration 0
1385 // (after G0W0 the RPA energies are still the DFT ones).
1386 rpa_.UpdateRPAInputEnergies(dft_energies_, e_qp, opt_.qpmin);
1387
1388 const Index nflat = qsgw_qptotal * qsgw_qptotal;
1389 auto diagonalise = [&](const Eigen::VectorXd& S, Index iter) {
1390 const Eigen::MatrixXd H = H0 + Eigen::Map<const Eigen::MatrixXd>(
1391 S.data(), qsgw_qptotal, qsgw_qptotal);
1392 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(H);
1393 if (es.info() != Eigen::ComputationInfo::Success) {
1394 throw std::runtime_error(
1395 "GW::CalculateQSGW: diagonalisation of H_QSGW failed at iter " +
1396 std::to_string(iter));
1397 }
1398 return es;
1399 };
1400
1401 Eigen::VectorXd S_in; // the tilde_Sigma the current (e_qp, U) came from
1402 bool converged = false;
1403 for (Index iter = 0; iter < opt_.qsgw_max_iterations; iter++) {
1404
1405 // ── Step 1: integrals in the current QP orbitals ────────────────────────
1406 for (Index m = 0; m < mtotal; m++) {
1407 Mmn_[m] = Mmn_orig[m];
1408 }
1409 Mmn_.RestoreAuxFrame(aux_frame_orig);
1410 if (iter > 0) {
1411 Mmn_.RotateOrbitals(qsgw_rotation_, opt_.qpmin, qsgw_qpmax);
1412 }
1413
1414 // Environment reaction field in the current QP basis: it depends on
1415 // the wavefunctions, not on the energies, so it is recomputed with the
1416 // rotation, like the exchange.
1417 Eigen::MatrixXd sigma_reac;
1418 if (reaction_field_.size() > 0) {
1419 sigma_reac = sigma_->CalcReactionFieldMatrix(reaction_field_);
1420 }
1421
1422 // Recompute screening W and Sigma in current QP basis.
1423 sigma_->PrepareScreening();
1424
1425 // Exchange: symmetric, frequency-independent. The frozen-core part is
1426 // fixed in the DFT MO basis; in the QP orbitals phi U it is U^T X U.
1427 Sigma_x_ = sigma_->CalcExchangeMatrix();
1428 if (core_exchange_.size() > 0) {
1429 const Eigen::MatrixXd Xw =
1430 core_exchange_.topLeftCorner(qsgw_qptotal, qsgw_qptotal);
1431 Sigma_x_ += qsgw_rotation_.transpose() * Xw * qsgw_rotation_;
1432 }
1433
1434 // ── Step 2: S_out, the symmetrised static self-energy in the QP basis
1435 // (e_qp(i) is the energy of QP orbital i), then in the DFT-MO basis
1436 Eigen::MatrixXd tilde_Sigma = Sigma_x_;
1437 if (opt_.qsgw_regularization == "srg") {
1438 tilde_Sigma += sigma_->CalcCorrelationSRG(e_qp, opt_.qsgw_srg_s);
1439 } else {
1440 tilde_Sigma += sigma_->CalcCorrelationOffDiag(e_qp);
1441 tilde_Sigma.diagonal() += sigma_->CalcCorrelationDiag(e_qp);
1442 }
1443 if (sigma_reac.size() > 0) {
1444 tilde_Sigma += sigma_reac; // static, symmetric: no symmetrization
1445 }
1446 tilde_Sigma = qsgw_rotation_ * tilde_Sigma * qsgw_rotation_.transpose();
1447 double dj_max = 0.0; // max |J[rho_QP] - J[rho_DFT]|, bare v
1448 double dj_reac_max = 0.0; // max of its v_reac part (screened only)
1449 if (opt_.qsgw_hartree_update) {
1450 // J of the current QP orbitals, in the DFT-MO basis, minus J[rho_DFT]
1451 const Eigen::MatrixXd dJ_bare = qsgw_rotation_ *
1452 sigma_->CalcHartreeMatrix() *
1453 qsgw_rotation_.transpose() -
1454 J0_bare;
1455 dj_max = dJ_bare.cwiseAbs().maxCoeff();
1456 if (hartree_screened) {
1457 const Eigen::MatrixXd dJ =
1458 qsgw_rotation_ * sigma_->CalcHartreeMatrix(reaction_field_) *
1459 qsgw_rotation_.transpose() -
1460 J0;
1461 dj_reac_max = (dJ - dJ_bare).cwiseAbs().maxCoeff();
1462 tilde_Sigma += dJ;
1463 } else {
1464 tilde_Sigma += dJ_bare;
1465 }
1466 }
1467 if (opt_.qsgw_hartree_update || sigma_reac.size() > 0) {
1468 // Occupied-virtual mixing of the current QP orbitals: the largest
1469 // principal angle between the QP and the DFT occupied spaces, the
1470 // only rotation the density (and so the Hartree update) sees.
1471 double angle = 0.0;
1472 if (nocc_w > 0 && nvirt_w > 0) {
1473 const Eigen::MatrixXd Uvo =
1474 qsgw_rotation_.bottomLeftCorner(nvirt_w, nocc_w);
1475 const double s =
1476 Eigen::JacobiSVD<Eigen::MatrixXd>(Uvo).singularValues().maxCoeff();
1477 angle = std::asin(std::min(1.0, s)) * 180.0 / tools::conv::Pi;
1478 }
1479 std::string reac_text;
1480 if (sigma_reac.size() > 0) {
1481 if (iter == 0) {
1482 sigma_reac_diag0 = sigma_reac.diagonal();
1483 }
1484 const Eigen::VectorXd dsr = sigma_reac.diagonal() - sigma_reac_diag0;
1485 reac_text = (boost::format(" dSigma_reac HOMO %1$+.4f LUMO %2$+.4f "
1486 "max|.| %3$.4f") %
1487 (nocc_w > 0 ? dsr(homo_w) * tools::conv::hrt2ev : 0.0) %
1488 (nvirt_w > 0 ? dsr(nocc_w) * tools::conv::hrt2ev : 0.0) %
1489 (dsr.cwiseAbs().maxCoeff() * tools::conv::hrt2ev))
1490 .str();
1491 }
1493 << TimeStamp() << " QSGW iter " << iter
1494 << (boost::format(" max|dJ| %1$.4f max|dJ_reac| %2$.4f") %
1495 (dj_max * tools::conv::hrt2ev) %
1496 (dj_reac_max * tools::conv::hrt2ev))
1497 .str()
1498 << reac_text
1499 << (boost::format(" eV occ-virt rotation %1$.3f deg") % angle).str()
1500 << std::flush;
1501 }
1502 const Eigen::VectorXd S_out =
1503 Eigen::Map<const Eigen::VectorXd>(tilde_Sigma.data(), nflat);
1504
1505 // ── Step 3: residual and next input ──────────────────────────────────────
1506 Eigen::VectorXd S_next;
1507 double residual = std::numeric_limits<double>::infinity();
1508 if (iter == 0) {
1509 // the seed state is not of the form H0 + S_in: start from its output
1510 S_next = S_out;
1511 } else {
1512 residual = (S_out - S_in).cwiseAbs().maxCoeff();
1513 qsgw_mixer.UpdateInput(S_in);
1514 qsgw_mixer.UpdateOutput(S_out);
1515 S_next = qsgw_mixer.MixHistory();
1516 }
1517
1518 // ── Step 4: QP energies and orbitals of the next input ───────────────────
1519 const Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es =
1520 diagonalise(S_next, iter);
1521 const double diff_max = (es.eigenvalues() - e_qp).cwiseAbs().maxCoeff();
1523 << TimeStamp() << " QSGW iter " << iter
1524 << " max|dE_QP| = " << diff_max * votca::tools::conv::hrt2ev
1525 << " eV max|Sigma_out - Sigma_in| = "
1526 << residual * votca::tools::conv::hrt2ev << " eV" << std::flush;
1527
1528 if (residual < opt_.qsgw_sc_limit && diff_max < opt_.qsgw_sc_limit) {
1529 XTP_LOG(Log::error, log_) << TimeStamp() << " QSGW converged in "
1530 << iter + 1 << " iterations." << std::flush;
1531 converged = true;
1532 // the result: the eigensystem of H0 + S_out, the output of the last
1533 // evaluation (it differs from the input by less than the residual)
1534 const Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es_out =
1535 diagonalise(S_out, iter);
1536 e_qp = es_out.eigenvalues();
1537 qsgw_rotation_ = es_out.eigenvectors();
1538 rpa_.UpdateRPAInputEnergies(dft_energies_, e_qp, opt_.qpmin);
1539 break;
1540 }
1541
1542 S_in = S_next;
1543 e_qp = es.eigenvalues();
1544 qsgw_rotation_ = es.eigenvectors();
1545 rpa_.UpdateRPAInputEnergies(dft_energies_, e_qp, opt_.qpmin);
1546 }
1547 if (!converged) {
1549 << TimeStamp() << " WARNING: QSGW did not converge in "
1550 << opt_.qsgw_max_iterations << " iterations. Inspect results carefully."
1551 << std::flush;
1552 }
1553
1554 // The QSGW energies are the eigenvalues of H_QSGW, the QP orbitals its
1555 // eigenvectors; getGWAResults() returns the former. Sigma_x_ holds the
1556 // last iteration's exchange in the QP basis of that iteration.
1557 qsgw_final_energies_ = e_qp;
1558
1559 // If the virtual window was trimmed, merge converged QSGW energies for
1560 // [qpmin, qsgw_qpmax] with perturbative seed energies for (qsgw_qpmax,
1561 // qpmax]. Pad rotation matrix with identity for excluded virtuals so that
1562 // getQSGWRotation() returns a qptotal_ x qptotal_ matrix for BSE hookup.
1563 if (window_trimmed) {
1564 const Index n_excluded = qptotal_ - qsgw_qptotal;
1566 << TimeStamp() << " QSGW: merging converged energies [" << opt_.qpmin
1567 << "," << qsgw_qpmax << "] with perturbative seed energies for "
1568 << n_excluded << " excluded virtual(s) [" << qsgw_qpmax + 1 << ","
1569 << opt_.qpmax << "]" << std::flush;
1570
1571 // Pad rotation with identity for excluded virtual levels
1572 Eigen::MatrixXd U_full = Eigen::MatrixXd::Identity(qptotal_, qptotal_);
1573 U_full.topLeftCorner(qsgw_qptotal, qsgw_qptotal) = qsgw_rotation_;
1574 qsgw_rotation_ = U_full;
1575
1576 // Build merged energy vector: QSGW for trimmed window, seed for rest.
1577 // Store in qsgw_final_energies_ so getGWAResults() returns the correct
1578 // full-window result without needing to recompute from sigma matrices.
1579 Eigen::VectorXd e_merged = e_qp_full;
1580 e_merged.head(qsgw_qptotal) = e_qp;
1581 qsgw_final_energies_ = e_merged;
1582
1583 // Update RPA with merged energies so RPAInputEnergies() is consistent.
1584 rpa_.UpdateRPAInputEnergies(dft_energies_, e_merged, opt_.qpmin);
1585
1586 // Restore Sigma_x_ and Sigma_c_ to full qptotal_ x qptotal_ size so that
1587 // any downstream code doesn't encounter a size mismatch with
1588 // vxc_.diagonal() (which has qptotal_ elements). getGWAResults() bypasses
1589 // these matrices via qsgw_final_energies_, so zeroing them is safe.
1590 Sigma_x_ = Eigen::MatrixXd::Zero(qptotal_, qptotal_);
1591 Sigma_c_ = Eigen::MatrixXd::Zero(qptotal_, qptotal_);
1592
1593 // Restore the full-size sigma configuration.
1594 Sigma_base::options sigma_opt_full;
1595 sigma_opt_full.homo = opt_.homo;
1596 sigma_opt_full.qpmin = opt_.qpmin;
1597 sigma_opt_full.qpmax = opt_.qpmax;
1598 sigma_opt_full.rpamin = opt_.rpamin;
1599 sigma_opt_full.rpamax = opt_.rpamax;
1600 sigma_opt_full.eta = opt_.eta;
1601 sigma_opt_full.quadrature_scheme = opt_.quadrature_scheme;
1602 sigma_opt_full.order = opt_.order;
1603 sigma_opt_full.alpha = opt_.alpha;
1604 sigma_->configure(sigma_opt_full);
1605 }
1606
1608 << TimeStamp() << " QSGW loop complete." << std::flush;
1609}
1610
1611namespace {
1612// Restores the RPA input energies on scope exit
1613class RPAEnergyGuard {
1614 public:
1615 RPAEnergyGuard(RPA& rpa, const Eigen::VectorXd& energies)
1616 : rpa_(rpa), saved_(rpa.getRPAInputEnergies()) {
1617 if (energies.size() == saved_.size()) {
1618 rpa_.setRPAInputEnergies(energies);
1619 }
1620 }
1621 ~RPAEnergyGuard() { rpa_.setRPAInputEnergies(saved_); }
1622 RPAEnergyGuard(const RPAEnergyGuard&) = delete;
1623 RPAEnergyGuard& operator=(const RPAEnergyGuard&) = delete;
1624
1625 private:
1626 RPA& rpa_;
1627 Eigen::VectorXd saved_;
1628};
1629} // namespace
1630
1631Eigen::MatrixXd GW::SigmaCurves(const std::vector<Index>& levels, Index steps,
1632 double spacing) const {
1633 // grid of level n: e_dft(n) + (k - (steps-1)/2) spacing, the same for all
1634 // GW flavours computed from one DFT calculation
1635 const Index nlev = Index(levels.size());
1636 Eigen::MatrixXd curves = Eigen::MatrixXd::Zero(nlev, steps);
1637 // One batched evaluation per level and chunk of frequencies: the
1638 // integrals (PPM) or residues (exact) of a level are read once per
1639 // chunk instead of once per point, and poles far from the chunk are
1640 // summed at a few Chebyshev nodes (exact_pole_sums::Values). Chunks keep
1641 // all threads busy for few levels and narrow the frequency range, which
1642 // moves more poles into the interpolated far field.
1643 const Index chunk = std::min<Index>(std::max<Index>(steps, 1), 96);
1644 const Index nchunks = (steps + chunk - 1) / chunk;
1645#pragma omp parallel for schedule(dynamic)
1646 for (Index task = 0; task < nlev * nchunks; task++) {
1647 const Index i = task / nchunks;
1648 const Index first = (task % nchunks) * chunk;
1649 const Index n = std::min(chunk, steps - first);
1650 const Index gw_level = levels[std::size_t(i)];
1651 const double center = dft_energies_(opt_.qpmin + gw_level);
1652 Eigen::VectorXd omega(n);
1653 for (Index k = 0; k < n; ++k) {
1654 omega(k) =
1655 center + (double(first + k) - (double(steps - 1) / 2.0)) * spacing;
1656 }
1657 curves.block(i, first, 1, n) =
1658 sigma_->CalcCorrelationDiagElements(gw_level, omega).transpose();
1659 }
1660 return curves;
1661}
1662
1663void GW::PlotSigma(std::string filename, Index steps, double spacing,
1664 std::string states) {
1665
1666 // The energies in G of the last QP solve (as for Sigma_c_ and the PQP
1667 // energies; in evGW the RPA energies were updated after it).
1668 RPAEnergyGuard guard(rpa_, rpa_energies_solved_);
1669
1670 // states are absolute MO indices; the GW arrays start at qpmin
1671 std::vector<Index> state_inds;
1672 IndexParser rp;
1673 std::vector<Index> parsed_states = rp.CreateIndexVector(states);
1674 for (Index gw_level : parsed_states) {
1675 if (gw_level >= opt_.qpmin && gw_level <= opt_.qpmax) {
1676 state_inds.push_back(gw_level);
1677 }
1678 }
1679 std::vector<Index> levels;
1680 for (Index s : state_inds) {
1681 levels.push_back(s - opt_.qpmin);
1682 }
1683 const Index num_states = Index(state_inds.size());
1684 const Eigen::VectorXd intercept =
1685 dft_energies_.segment(opt_.qpmin, qptotal_) + Sigma_x_.diagonal() +
1686 Sigma_reac_.diagonal() - vxc_.diagonal();
1687
1688 auto t0 = std::chrono::steady_clock::now();
1689 const Eigen::MatrixXd curves = SigmaCurves(levels, steps, spacing);
1690 const double seconds =
1691 std::chrono::duration<double>(std::chrono::steady_clock::now() - t0)
1692 .count();
1694 << TimeStamp() << " PQP(omega) written to '" << filename
1695 << "' for states " << rp.CreateIndexString(state_inds) << " ("
1696 << num_states * steps << " points, " << seconds << " s)" << std::flush;
1697
1698 Eigen::MatrixXd mat = Eigen::MatrixXd::Zero(steps, 2 * num_states);
1699 for (Index i = 0; i < num_states; i++) {
1700 const Index n = levels[std::size_t(i)];
1701 const double center = dft_energies_(opt_.qpmin + n);
1702 for (Index k = 0; k < steps; ++k) {
1703 mat(k, 2 * i) =
1704 center + (double(k) - (double(steps - 1) / 2.0)) * spacing;
1705 }
1706 mat.col(2 * i + 1) = curves.row(i).transpose().array() + intercept(n);
1707 }
1708
1709 std::ofstream out;
1710 out.open(filename);
1711 for (Index i = 0; i < num_states; i++) {
1712 const Index gw_level = state_inds[std::size_t(i)];
1713 out << boost::format("#%1$somega_%2$d\tE_QP(omega)_%2$d") %
1714 (i == 0 ? "" : "\t") % gw_level;
1715 }
1716 out << std::endl;
1717 boost::format numFormat("%+1.6f");
1718 Eigen::IOFormat matFormat(Eigen::StreamPrecision, 0, "\t", "\n");
1719 out << numFormat % mat.format(matFormat) << std::endl;
1720 out.close();
1721}
1722
1723void GW::DumpSigma(const sigma_dump_options& dopt, const std::string& gw_mode,
1724 const std::string& version) {
1725 if (!sigma_->HasPoleForm()) {
1726 throw std::runtime_error(
1727 "gw.sigma_dump needs sigma_integrator ppm or exact");
1728 }
1729 if (qp_solution_.size() != qptotal_) {
1730 throw std::runtime_error(
1731 "GW::DumpSigma called before CalculateGWPerturbation");
1732 }
1733 auto t0 = std::chrono::steady_clock::now();
1734 XTP_LOG(Log::error, log_) << TimeStamp() << " Dumping self-energy data to '"
1735 << dopt.filename << "'" << std::flush;
1736
1737 // the energies in G of the last QP solve, as for Sigma_c_
1738 RPAEnergyGuard guard(rpa_, rpa_energies_solved_);
1739
1740 const Index homo = opt_.homo;
1741 const double e_lumo = dft_energies_(homo + 1);
1742 // fixed reference of the imaginary axis: the DFT midgap, the same for all
1743 // GW flavours from one DFT calculation
1744 const double mu = 0.5 * (dft_energies_(homo) + e_lumo);
1745 const double eta2 = opt_.eta * opt_.eta;
1746
1747 // levels with spectra and imaginary-axis data: qpmin .. qpmin + nspec - 1
1748 Index nspec = qptotal_;
1749 if (dopt.levels_emax_ev > 0.0) {
1750 const double limit = e_lumo + dopt.levels_emax_ev * tools::conv::ev2hrt;
1751 nspec = 0;
1752 while (nspec < qptotal_ && dft_energies_(opt_.qpmin + nspec) <= limit) {
1753 ++nspec;
1754 }
1755 }
1756
1757 // Gauss-Legendre nodes on (-1, 1) (Golub-Welsch), mapped to (0, inf):
1758 // nu = s (1+x)/(1-x)
1759 const Index nimag = dopt.imag_points;
1760 Eigen::VectorXd nu = Eigen::VectorXd::Zero(nimag);
1761 Eigen::VectorXd nu_weights = Eigen::VectorXd::Zero(nimag);
1762 if (nimag > 0) {
1763 Eigen::MatrixXd J = Eigen::MatrixXd::Zero(nimag, nimag);
1764 for (Index k = 1; k < nimag; k++) {
1765 const double kd = double(k);
1766 J(k, k - 1) = J(k - 1, k) = kd / std::sqrt(4.0 * kd * kd - 1.0);
1767 }
1768 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(J);
1769 for (Index k = 0; k < nimag; k++) {
1770 const double x = es.eigenvalues()(k);
1771 const double w = 2.0 * std::pow(es.eigenvectors()(0, k), 2);
1772 nu(k) = dopt.imag_scale * (1.0 + x) / (1.0 - x);
1773 nu_weights(k) = w * dopt.imag_scale * 2.0 / ((1.0 - x) * (1.0 - x));
1774 }
1775 }
1776
1777 const Index nbins = std::max<Index>(
1778 1, Index(std::round((dopt.spectral_max - dopt.spectral_min) /
1779 dopt.spectral_binwidth)));
1780 Eigen::VectorXd bin_edges(nbins + 1);
1781 for (Index b = 0; b <= nbins; b++) {
1782 bin_edges(b) = dopt.spectral_min + double(b) * dopt.spectral_binwidth;
1783 }
1784
1785 Eigen::VectorXd sigma_check = Eigen::VectorXd::Zero(qptotal_);
1786 Eigen::VectorXd dsigma = Eigen::VectorXd::Zero(qptotal_);
1787 Eigen::VectorXd d2sigma = Eigen::VectorXd::Zero(qptotal_);
1788 Eigen::VectorXd sigma_dft = Eigen::VectorXd::Zero(qptotal_);
1789 Eigen::MatrixXf spectral = Eigen::MatrixXf::Zero(nspec, nbins);
1790 Eigen::VectorXd weight_below = Eigen::VectorXd::Zero(nspec);
1791 Eigen::VectorXd weight_above = Eigen::VectorXd::Zero(nspec);
1792 Eigen::VectorXd weight_total = Eigen::VectorXd::Zero(nspec);
1793 Eigen::VectorXd first_moment = Eigen::VectorXd::Zero(nspec);
1794 Eigen::MatrixXd imag_re = Eigen::MatrixXd::Zero(nspec, nimag);
1795 Eigen::MatrixXd imag_im = Eigen::MatrixXd::Zero(nspec, nimag);
1796
1797#pragma omp parallel for schedule(dynamic)
1798 for (Index n = 0; n < qptotal_; n++) {
1799 const Sigma_base::DiagPoleForm pf = sigma_->PoleForm(n);
1800 const ExactPoles& poles = pf.poles;
1801 const Index B = poles.nlevels();
1802 const double eqp = qp_solution_(n);
1803 const bool spec = (n < nspec);
1804 double s_qp = 0.0, d2 = 0.0, wsum = 0.0, m1 = 0.0, below = 0.0, above = 0.0;
1805 Eigen::VectorXd hist = Eigen::VectorXd::Zero(spec ? nbins : 0);
1806 Eigen::ArrayXd re = Eigen::ArrayXd::Zero(spec ? nimag : 0);
1807 Eigen::ArrayXd im = Eigen::ArrayXd::Zero(spec ? nimag : 0);
1808 // work arrays of the level, allocated once: per-mode temporaries were
1809 // 29 allocations per mode and level, which serialise the threads in
1810 // malloc on macOS
1811 Eigen::ArrayXd z(B), a(B), x(B), x2(B), den(B), xm(B), xm2(B), ad(B);
1812 for (Index s = 0; s < poles.nmodes(); ++s) {
1813 const double c =
1814 pf.prefactor * (pf.mode_factors ? pf.mode_factors[s] : 1.0);
1815 if (c == 0.0) {
1816 continue;
1817 }
1818 const double omega = poles.omegas(s);
1819 const double* r = pf.residues + B * s;
1820 for (Index m = 0; m < B; ++m) {
1821 z(m) = poles.energies(m) + ((m < poles.n_occ) ? -omega : omega);
1822 a(m) = c * r[m] * r[m];
1823 }
1824 // real axis at the QP solution, same broadening as the QP solver
1825 x = eqp - z;
1826 x2 = x.square();
1827 den = x2 + eta2;
1828 s_qp += (a * x / den).sum();
1829 d2 += (a * 2.0 * x * (x2 - 3.0 * eta2) / (den.square() * den)).sum();
1830 if (!spec) {
1831 continue;
1832 }
1833 wsum += a.sum();
1834 m1 += (a * z).sum();
1835 for (Index m = 0; m < B; ++m) {
1836 const double b =
1837 std::floor((z(m) - dopt.spectral_min) / dopt.spectral_binwidth);
1838 if (b < 0) {
1839 below += a(m);
1840 } else if (b >= double(nbins)) {
1841 above += a(m);
1842 } else {
1843 hist(Index(b)) += a(m);
1844 }
1845 }
1846 // imaginary axis mu + i nu: sum_p a_p / (mu + i nu - z_p)
1847 xm = mu - z;
1848 xm2 = xm.square();
1849 for (Index k = 0; k < nimag; k++) {
1850 ad = a / (xm2 + nu(k) * nu(k));
1851 re(k) += (ad * xm).sum();
1852 im(k) -= nu(k) * ad.sum();
1853 }
1854 }
1855 sigma_check(n) = s_qp;
1856 d2sigma(n) = d2;
1857 dsigma(n) = sigma_->CalcCorrelationDiagElementDerivative(n, eqp);
1858 sigma_dft(n) =
1859 sigma_->CalcCorrelationDiagElement(n, dft_energies_(opt_.qpmin + n));
1860 if (spec) {
1861 spectral.row(n) = hist.transpose().cast<float>();
1862 weight_below(n) = below;
1863 weight_above(n) = above;
1864 weight_total(n) = wsum;
1865 first_moment(n) = m1;
1866 imag_re.row(n) = re.matrix().transpose();
1867 imag_im.row(n) = im.matrix().transpose();
1868 }
1869 }
1870
1871 const Eigen::VectorXd sigma_c = Sigma_c_.diagonal();
1872 const double max_dev = (sigma_check - sigma_c).cwiseAbs().maxCoeff();
1874 << TimeStamp()
1875 << " Sigma dump: max |Sigma_c(pole sum) - Sigma_c| at the QP solution = "
1876 << max_dev << " Ha" << std::flush;
1877 const Eigen::VectorXd Z = (1.0 - dsigma.array()).inverse().matrix();
1878
1879 const Sigma_base::ScreeningData scr = sigma_->ScreeningDump(nu);
1880
1881 // Sigma_c(omega) of every QP level on a grid around its DFT energy
1882 Eigen::MatrixXf curves;
1883 if (dopt.plot_steps > 0) {
1884 std::vector<Index> all(static_cast<std::size_t>(qptotal_));
1885 for (Index n = 0; n < qptotal_; ++n) {
1886 all[std::size_t(n)] = n;
1887 }
1888 curves = SigmaCurves(all, dopt.plot_steps, dopt.plot_spacing).cast<float>();
1889 }
1890
1891 Eigen::Matrix<Index, Eigen::Dynamic, 1> levels(qptotal_);
1892 Eigen::Matrix<Index, Eigen::Dynamic, 1> competing(qptotal_);
1893 Eigen::VectorXd omega_alt = Eigen::VectorXd::Zero(qptotal_);
1894 Eigen::VectorXd Z_alt = Eigen::VectorXd::Zero(qptotal_);
1895 for (Index n = 0; n < qptotal_; n++) {
1896 levels(n) = n + opt_.qpmin;
1897 competing(n) = 0;
1898 if (n < Index(competing_roots_.size())) {
1899 const auto& c = competing_roots_[std::size_t(n)];
1900 competing(n) = c.competing ? 1 : 0;
1901 omega_alt(n) = c.omega_alt;
1902 Z_alt(n) = c.Z_alt;
1903 }
1904 }
1905
1907 {
1908 CheckpointWriter w = cpf.getWriter("/sigma");
1909 w(std::string(
1910 "Sigma_c,nn(w) = sum_p a_p Re[1/(w - z_p +- i eta)]; Hartree; "
1911 "levels = absolute MO indices qpmin..qpmax; per-level spectra "
1912 "and imaginary axis (z = mu + i nu) and the matrices vxc, sigma_x, "
1913 "Hqp for the first n_spec levels; sigma_c_curves(n, k) = "
1914 "Sigma_c(e_dft(n) + (k - (curve_steps-1)/2) curve_spacing) for all "
1915 "levels"),
1916 "description");
1917 w(std::string("2"), "format_version");
1918 w(version, "xtp_version");
1919 w(opt_.sigma_integration, "sigma_integrator");
1920 w(gw_mode, "gw_mode");
1921 w(homo, "homo");
1922 w(opt_.qpmin, "qpmin");
1923 w(opt_.qpmax, "qpmax");
1924 w(opt_.rpamin, "rpamin");
1925 w(opt_.rpamax, "rpamax");
1926 w(opt_.eta, "eta");
1927 w(opt_.ScaHFX, "ScaHFX");
1928 w(mu, "mu");
1929 w(nspec, "n_spec");
1930 w(dopt.levels_emax_ev, "levels_emax_ev");
1931 w(dopt.imag_scale, "imag_scale");
1932 w(dopt.spectral_binwidth, "spectral_binwidth");
1933 w(qp_iterations_, "qp_iterations");
1934 w(screening_builds_, "screening_builds");
1935 w(max_dev, "sigma_c_check_max_dev");
1936
1937 w(levels, "levels");
1938 w(Eigen::VectorXd(dft_energies_), "e_dft_all");
1939 w(Eigen::VectorXd(dft_energies_.segment(opt_.qpmin, qptotal_)), "e_dft");
1940 // diagonals for all QP levels, matrices for the first n_spec levels
1941 // (the full QP window of a G0W0 over all levels would dominate the file)
1942 w(Eigen::VectorXd(vxc_.diagonal()), "vxc_diag");
1943 w(Eigen::VectorXd(Sigma_x_.diagonal()), "sigma_x_diag");
1944 w(Eigen::VectorXd(Sigma_reac_.diagonal()), "sigma_reac");
1945 w(Eigen::MatrixXd(vxc_.topLeftCorner(nspec, nspec)), "vxc");
1946 w(Eigen::MatrixXd(Sigma_x_.topLeftCorner(nspec, nspec)), "sigma_x");
1947 w(Eigen::MatrixXd(getHQP().topLeftCorner(nspec, nspec)), "Hqp");
1948 w(getGWAResults(), "e_qp");
1949 w(qp_solution_, "qp_solution");
1950 w(sigma_c, "sigma_c");
1951 w(dsigma, "dsigma_c");
1952 w(d2sigma, "d2sigma_c");
1953 w(Z, "Z");
1954 w(sigma_dft, "sigma_c_at_e_dft");
1955 w(competing, "competing_roots");
1956 w(omega_alt, "omega_alt");
1957 w(Z_alt, "Z_alt");
1958 w(rpa_energies_solved_, "g_energies");
1959 w(scr.energies, "w_energies");
1960
1961 w(nu, "imag_nu");
1962 w(nu_weights, "imag_weights");
1963 w(imag_re, "sigma_c_imag_re");
1964 w(imag_im, "sigma_c_imag_im");
1965 w(bin_edges, "spectral_bin_edges");
1966 w(spectral, "spectral_weights");
1967 w(weight_below, "spectral_weight_below");
1968 w(weight_above, "spectral_weight_above");
1969 w(weight_total, "pole_weight_total");
1970 w(first_moment, "pole_first_moment");
1971 if (dopt.plot_steps > 0) {
1972 w(dopt.plot_steps, "curve_steps");
1973 w(dopt.plot_spacing, "curve_spacing");
1974 w(curves, "sigma_c_curves");
1975 }
1976 }
1977 {
1978 CheckpointWriter w = cpf.getWriter("/screening");
1979 w(std::string(
1980 "W_c = W - v in modes s (omegas) and in the plasmon-pole frame i "
1981 "(eigenvectors of eps^-1(0)): eps^-1_ii(i nu) - 1 = -sum_s a_is "
1982 "Omega_s/(nu^2 + Omega_s^2); frame_moments(i,k) = sum_s a_is "
1983 "Omega_s^(k-1); frame_weights = -(eps^-1(0) - 1)_ii"),
1984 "description");
1985 w(scr.omegas, "omegas");
1986 w(Eigen::VectorXf(scr.strength.cast<float>()), "strength");
1987 w(scr.frame_weights, "frame_weights");
1988 w(scr.frame_moments, "frame_moments");
1989 w(Eigen::MatrixXf(scr.frame_eps_inv_imag.cast<float>()),
1990 "frame_eps_inv_imag");
1991 w(nu, "imag_nu");
1992 }
1993 const double seconds =
1994 std::chrono::duration<double>(std::chrono::steady_clock::now() - t0)
1995 .count();
1997 << TimeStamp() << " Sigma dump: " << nspec << " of " << qptotal_
1998 << " levels with spectra, " << scr.omegas.size() << " screening modes, "
1999 << seconds << " s" << std::flush;
2000}
2001
2002} // namespace xtp
2003} // namespace votca
virtual std::unique_ptr< T > Create(const key_t &key, args_t &&...arguments)
Anderson mixing as convergence acceleration in SCF/fixed point problems.
void UpdateInput(const Eigen::VectorXd &newInput)
const Eigen::VectorXd MixHistory()
void UpdateOutput(const Eigen::VectorXd &newOutput)
void Configure(const Index order, const double alpha)
CheckpointWriter getWriter()
static Eigen::MatrixXd DressingMatrix(const Eigen::MatrixXd &R)
S = (1 + R)^(1/2), the dressing of the auxiliary index that turns the bare interaction into u = v + v...
double value(double frequency, EvalStage stage=EvalStage::Other) const
Definition gw.h:396
const QPStats & GetStats() const
Definition gw.h:407
double deriv(double frequency) const
Definition gw.h:400
boost::optional< double > SolveQP_Grid_Windowed(double intercept0, double frequency0, Index gw_level, double left_limit, double right_limit, bool allow_rejected_return=true, QPStats *stats=nullptr) const
Definition gw.cc:983
Index qptotal_
Definition gw.h:301
DFTTimings timings_
Definition gw.h:311
Eigen::MatrixXd reaction_field_
Definition gw.h:315
void setReactionField(const Eigen::MatrixXd &R)
Screen by a static polarizable environment (embedded GW).
Definition gw.cc:72
Eigen::SelfAdjointEigenSolver< Eigen::MatrixXd > DiagonalizeQPHamiltonian() const
Definition gw.cc:135
void PrintGWA_Energies() const
Definition gw.cc:142
Eigen::VectorXd tracked_change_
Definition gw.h:463
double qp_midgap_
Definition gw.h:454
void DumpSigma(const sigma_dump_options &dopt, const std::string &gw_mode, const std::string &version)
Write the self-energy and screening data of the finished GW (G0W0 or evGW, ppm or exact) to a separat...
Definition gw.cc:1723
RPA rpa_
Definition gw.h:342
boost::optional< qp_solver::RootCandidate > SolveQP_Tracked(double intercept0, double frequency0, Index gw_level, QPStats *stats) const
Definition gw.cc:736
void PrintQSGW_Energies(const std::string &seed_label, const Eigen::VectorXd &seed_energies, const Eigen::VectorXd &qsgw_energies) const
Print a two-column comparison of seed (G0W0 or evGW) vs converged QSGW quasiparticle energies.
Definition gw.cc:225
void PrintCompetingRoots(Log::Level level) const
Definition gw.cc:562
void CalculateQSGW()
Run quasiparticle self-consistent GW (QSGW).
Definition gw.cc:1184
bool Converged(const Eigen::VectorXd &e1, const Eigen::VectorXd &e2, double epsilon) const
Definition gw.cc:1131
Index qp_iterations_
Definition gw.h:339
bool tracking_active_
Definition gw.h:464
void configure(const options &opt)
Definition gw.cc:43
boost::optional< double > SolveQP_Grid_Windowed_Adaptive(double intercept0, double frequency0, Index gw_level, double left_limit, double right_limit, bool allow_rejected_return=true, QPStats *stats=nullptr) const
Definition gw.cc:777
options opt_
Definition gw.h:329
Eigen::MatrixXd SigmaCurves(const std::vector< Index > &levels, Index steps, double spacing) const
Definition gw.cc:1631
Eigen::VectorXd qp_solution_
Definition gw.h:308
Eigen::MatrixXd getHQP() const
Definition gw.cc:129
Index tracked_local_total_
Definition gw.h:466
Eigen::VectorXd SolveQP(const Eigen::VectorXd &frequencies) const
Definition gw.cc:587
TCMatrix_gwbse & Mmn_
Definition gw.h:333
Index screening_builds_
Definition gw.h:338
Index gw_sc_iteration_
Definition gw.h:340
void PrintReactionFieldShifts() const
Definition gw.cc:88
Eigen::VectorXd rpa_energies_solved_
Definition gw.h:309
qp_solver::Stats QPStats
Definition gw.h:42
boost::optional< double > SolveQP_Grid_Windowed_Dense(double intercept0, double frequency0, Index gw_level, double left_limit, double right_limit, bool allow_rejected_return=true, QPStats *stats=nullptr) const
Definition gw.cc:852
Eigen::MatrixXd Sigma_c_
Definition gw.h:314
qp_solver::RootCandidate QPRootCandidate
Definition gw.h:43
void PrintQSGW_Composition(double threshold=0.01) const
Print the dominant DFT-KS orbital contributions to each converged QSGW quasiparticle state.
Definition gw.cc:182
Eigen::MatrixXd Sigma_reac_
Definition gw.h:318
Eigen::VectorXd qsgw_seed_energies_
Definition gw.h:322
const Eigen::VectorXd & dft_energies_
Definition gw.h:335
std::unique_ptr< Sigma_base > sigma_
Definition gw.h:331
void CalculateHQP()
Definition gw.cc:1144
std::vector< qp_solver::CompetingRoots > competing_roots_
Definition gw.h:457
Eigen::VectorXd qsgw_final_energies_
Definition gw.h:327
Logger & log_
Definition gw.h:332
Eigen::VectorXd getGWAResults() const
Definition gw.cc:551
Eigen::VectorXd tracked_Z_
Definition gw.h:462
void DressForEnvironment()
Definition gw.cc:78
qp_solver::WindowDiagnostics QPWindowDiagnostics
Definition gw.h:44
double CalcHomoLumoShift(Eigen::VectorXd frequencies) const
Definition gw.cc:122
boost::optional< double > SolveQP_Grid(double intercept0, double frequency0, Index gw_level, QPStats *stats=nullptr) const
Definition gw.cc:1037
const Eigen::MatrixXd & vxc_
Definition gw.h:334
void RecordCompetingRoots(Index gw_level, const std::vector< QPRootCandidate > &roots, double chosen) const
Definition gw.h:470
void PrintQP_Energies(const Eigen::VectorXd &qp_diag_energies) const
Definition gw.cc:252
Index tracked_local_
Definition gw.h:465
void PlotSigma(std::string filename, Index steps, double spacing, std::string states)
Definition gw.cc:1663
boost::optional< double > SolveQP_FixedPoint(double intercept0, double frequency0, Index gw_level, QPStats *stats=nullptr) const
Definition gw.cc:1113
Eigen::VectorXd ScissorShift_DFTlevel(const Eigen::VectorXd &dft_energies) const
Definition gw.cc:279
Eigen::MatrixXd Sigma_x_
Definition gw.h:313
boost::optional< double > SolveQP_Linearisation(double intercept0, double frequency0, Index gw_level, QPStats *stats=nullptr) const
Definition gw.cc:758
void CalculateGWPerturbation()
Definition gw.cc:287
Eigen::MatrixXd qsgw_rotation_
Definition gw.h:320
Eigen::MatrixXd dressing_
Definition gw.h:316
Eigen::MatrixXd core_exchange_
Definition gw.h:337
Eigen::VectorXd tracked_root_
Definition gw.h:461
std::vector< Index > CreateIndexVector(const std::string &Ids) const
std::string CreateIndexString(const std::vector< Index > &indeces) const
static std::string Usage()
Newton Rapson rootfinder for 1d functions.
double FindRoot(const Func &f, double x0)
When evGW rebuilds the screening W.
void Built(const Eigen::VectorXd &energies)
W has just been built from these energies.
void Configure(Mode mode, double ratio, Index max_inner)
static std::string Name(Mode mode)
Next Decide(double residual, const Eigen::VectorXd &new_energies, double limit, double *drift_out=nullptr)
static Mode Parse(const std::string &name)
Index inner() const
QP iterations since W was last built (before the current one).
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
#define XTP_LOG(level, log)
Definition logger.h:40
const double ev2hrt
Definition constants.h:54
const double hrt2ev
Definition constants.h:53
const double Pi
Definition constants.h:36
Index getMaxThreads()
Definition eigen.h:145
void Cite(const std::string &key)
marks a reference as used (thread safe; unknown keys are printed as such)
Definition citations.cc:289
boost::optional< RootCandidate > SolveQP_Tracked(QPFunc &fqp, double center, double halfwidth, double Z_previous, double min_Z_fraction, const SolverOptions &opt, bool use_brent=false)
boost::optional< double > SolveQP_Grid_Windowed(QPFunc &fqp, double frequency0, double left_limit, double right_limit, Index gw_sc_iteration, const SolverOptions &opt, WindowDiagnostics *wdiag=nullptr, std::vector< RootCandidate > *accepted_roots_out=nullptr, std::vector< RootCandidate > *rejected_roots_out=nullptr, bool use_brent=false)
auto PrefetchIfAvailable(const QPFunc &fqp, const std::vector< double > &nodes, int) -> decltype(fqp.Prefetch(nodes), void())
std::string LargestResiduals(const Eigen::VectorXd &solution, const Eigen::VectorXd &input, Index first_level, Index n=5)
void NormalizeGridSearchOptions(Opt &opt)
boost::optional< double > LinearisedRoot(double w0, double f0, double fprime)
const RootCandidate & SelectRoot(const std::vector< RootCandidate > &roots, const SolverOptions &opt)
boost::optional< RootCandidate > RefineQPInterval(double lowerbound, double f_lowerbound, double upperbound, double f_upperbound, const QPFunc &f, double reference, const SolverOptions &opt, bool use_brent)
Charge transport classes.
Definition ERIs.h:28
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
static Level current_level
Definition globals.h:30
Level
be loud and noisy
Definition globals.h:28
Pole sums of the exact (RPA spectral) correlation self-energy.
const Eigen::VectorXd & energies
const Eigen::VectorXd & omegas
Options of DumpSigma (energies in Hartree unless noted).
Definition gw.h:260
void Add(const Stats &other)
std::size_t TotalSigmaCalls() const