votca 2026-dev
Loading...
Searching...
No Matches
convergenceacc.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20#include <algorithm>
21
22// Local VOTCA includes
24
25namespace votca {
26namespace xtp {
27
36
37// Build the symmetric orthogonalization matrix X = S^{-1/2}. All Fock-like
38// matrices are diagonalized in the orthogonal AO basis X^T F X.
40 S_ = &S;
41 // Own eigendecomposition rather than AOOverlap::Pseudo_InvSqrt: the
42 // removed directions are needed as well, see removed_projector_.
43 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(S.Matrix());
44 const Eigen::VectorXd& s_eig = es.eigenvalues();
45 Eigen::VectorXd inv_sqrt = Eigen::VectorXd::Zero(s_eig.size());
46 Index removed = 0;
47 for (Index i = 0; i < s_eig.size(); ++i) {
48 if (s_eig(i) < etol) {
49 ++removed;
50 } else {
51 inv_sqrt(i) = 1.0 / std::sqrt(s_eig(i));
52 }
53 }
55 es.eigenvectors() * inv_sqrt.asDiagonal() * es.eigenvectors().transpose();
56 removed_projector_.resize(0, 0);
57 if (removed > 0) {
58 // In SolveFockmatrix, H_ortho = X^T H X vanishes on these directions,
59 // so they would come out as eigenvalue-0 "orbitals" in the middle of
60 // the spectrum. Shifting them far up keeps them out of the occupied
61 // and low virtual space.
62 const Eigen::MatrixXd U = es.eigenvectors().leftCols(removed);
63 removed_projector_ = U * U.transpose();
64 }
66 << TimeStamp() << " Smallest value of AOOverlap matrix is " << s_eig(0)
67 << std::flush;
69 << TimeStamp() << " Removed " << removed
70 << " basisfunction from inverse overlap matrix (threshold " << etol << ")"
71 << std::flush;
72 if (removed > 0) {
74 << TimeStamp() << " The " << removed
75 << " removed direction(s) appear as zero orbitals at +" << kRemovedShift
76 << " Hartree." << std::flush;
77 } else if (s_eig(0) < 1e3 * etol) {
79 << TimeStamp()
80 << " WARNING: the overlap matrix is nearly singular; if the SCF is "
81 "unstable, raise xtpdft.overlap_tolerance above "
82 << s_eig(0) << "." << std::flush;
83 }
84 return;
85}
86
87// Perform one SCF acceleration step.
88//
89// The commutator residual
90//
91// R = X^T (F P S - S P F) X
92//
93// vanishes at self-consistency in a non-orthogonal AO basis. Its maximum
94// element is used as the DIIS error metric, while the history of Fock and
95// density matrices is passed to either DIIS or ADIIS to construct the next
96// extrapolated Fock matrix. When the extrapolation is deemed unsafe, linear
97// density mixing is used instead.
98Eigen::MatrixXd ConvergenceAcc::Iterate(const Eigen::MatrixXd& dmat,
99 Eigen::MatrixXd& H,
100 tools::EigenSystem& MOs, double totE) {
101 totE_.push_back(totE);
102 // The MOs of the previous step, for the level shift.
103 const Eigen::MatrixXd MOs_old = MOs.eigenvectors();
104 const Eigen::VectorXd MOs_old_energies = MOs.eigenvalues();
105
106 const Eigen::MatrixXd& S = S_->Matrix();
107 const Eigen::MatrixXd errormatrix =
108 Sminusahalf.transpose() * (H * dmat * S - S * dmat * H) * Sminusahalf;
109 diiserror_ = errormatrix.cwiseAbs().maxCoeff();
110
112 << TimeStamp() << " DIIs error " << getDIIsError() << std::flush;
114 << TimeStamp() << " Delta Etot " << getDeltaE() << std::flush;
115
116 // Energy-rise reset. An extrapolated Fock matrix that sends the energy
117 // far above anything seen before is not a step to build on: the history
118 // that produced it is discarded and the SCF continues, damped, from the
119 // lowest-energy density so far.
121 if (opt_.energy_reset > 0.0 && have_best_ &&
122 totE > best_energy_ + opt_.energy_reset &&
128 << TimeStamp() << " WARNING: energy rose by " << totE - best_energy_
129 << " Ha above the lowest so far (" << best_energy_
130 << " Ha); discarding the (A)DIIS history and restarting from that "
131 "density (reset "
132 << energy_resets_ << ")" << std::flush;
133 mathist_.clear();
134 dmatHist_.clear();
135 errhist_.clear();
136 diis_.Clear();
137 Eigen::MatrixXd H_restart = best_H_;
138 if (opt_.levelshift > 0.0) {
139 Levelshift(H_restart, MOs_old);
140 }
141 MOs = SolveFockmatrix(H_restart);
142 usedmixing_ = true;
143 return opt_.mixingparameter * best_dmat_ +
144 (1.0 - opt_.mixingparameter) * DensityMatrix(MOs);
145 }
146 if (!have_best_ || totE < best_energy_) {
147 have_best_ = true;
148 best_energy_ = totE;
149 best_dmat_ = dmat;
150 best_H_ = H;
151 }
152
153 // History, trimmed at one index for all of mathist_, dmatHist_, errhist_
154 // and DIIS's own error history: the oldest entry, or with DIIS_maxout the
155 // one with the largest error. All four are always the same length, so
156 // DIIS::Update trims at the same moment and the same index.
157 Index drop = 0;
158 if (Index(mathist_.size()) == opt_.histlength) {
159 if (opt_.maxout) {
160 drop = Index(std::max_element(errhist_.begin(), errhist_.end()) -
161 errhist_.begin());
162 }
163 mathist_.erase(mathist_.begin() + drop);
164 dmatHist_.erase(dmatHist_.begin() + drop);
165 errhist_.erase(errhist_.begin() + drop);
166 }
167 // The UNSHIFTED Fock matrix goes into the history: the level shift is a
168 // device for the next diagonalization only, and would otherwise enter
169 // the DIIS errors and the ADIIS energy model.
170 mathist_.push_back(H);
171 dmatHist_.push_back(dmat);
172 errhist_.push_back(diiserror_);
173 diis_.Update(drop, errormatrix);
174
175 bool diis_error = false;
176 Eigen::MatrixXd H_guess = H;
177 // Below DIIS_start the density is already close to self-consistency (as
178 // after a warm start from the previous QM/MM iteration): no damping, a
179 // plain step from the current Fock matrix first and DIIS from two
180 // history entries on. Far from it (a cold start), the first steps are
181 // damped until the history holds three entries, as before.
182 const bool near_convergence = diiserror_ < opt_.diis_start;
183 const std::size_t min_history = near_convergence ? 1 : 2;
184 if ((diiserror_ < opt_.adiis_start || diiserror_ < opt_.diis_start) &&
185 opt_.usediis && mathist_.size() > min_history) {
186 Eigen::VectorXd coeffs;
187 // ADIIS above DIIS_start, and also below it whenever the energy went
188 // up: plain DIIS does not minimize the energy and can run away from a
189 // bad step.
190 const bool energy_rose = getDeltaE() > kEnergyRiseForADIIS;
191 if (diiserror_ > opt_.diis_start || energy_rose) {
192 coeffs = adiis_.CalcCoeff(dmatHist_, mathist_);
193 diis_error = !adiis_.Info();
195 << TimeStamp() << " Using ADIIS for next guess" << std::flush;
196 } else {
197 coeffs = diis_.CalcCoeff();
198 diis_error = !diis_.Info();
200 << TimeStamp() << " Using DIIS for next guess" << std::flush;
201 }
202 if (diis_error) {
204 << TimeStamp() << " (A)DIIS failed using mixing instead"
205 << std::flush;
206 } else {
207 H_guess.setZero();
208 for (Index i = 0; i < coeffs.size(); i++) {
209 if (std::abs(coeffs(i)) < 1e-8) {
210 continue;
211 }
212 H_guess += coeffs(i) * mathist_[i];
213 }
214 }
215 }
216
217 if (opt_.mode != KSmode::fractional && nocclevels_ > 0 &&
218 nocclevels_ < MOs_old_energies.size()) {
219 const double gap =
220 MOs_old_energies(nocclevels_) - MOs_old_energies(nocclevels_ - 1);
221 if ((diiserror_ > opt_.levelshiftend && opt_.levelshift > 0.0) ||
222 gap < 1e-6) {
223 Levelshift(H_guess, MOs_old);
224 }
225 }
226
227 MOs = SolveFockmatrix(H_guess);
228
229 Eigen::MatrixXd dmatout = DensityMatrix(MOs);
230 // mixing_end, not ADIIS_start, decides on damping (as in the UKS path):
231 // the two are separate options.
232 if (diiserror_ > opt_.mixingend || !opt_.usediis || diis_error ||
233 (mathist_.size() <= 2 && !near_convergence)) {
234 usedmixing_ = true;
235 dmatout =
236 opt_.mixingparameter * dmat + (1.0 - opt_.mixingparameter) * dmatout;
238 << TimeStamp() << " Using Mixing with alpha=" << opt_.mixingparameter
239 << std::flush;
240 } else {
241 usedmixing_ = false;
242 }
243 return dmatout;
244}
245
247 const std::vector<Eigen::MatrixXd>& errors = diis_.ErrorHistory();
248 if (errors.size() != mathist_.size() || dmatHist_.size() != mathist_.size() ||
249 errhist_.size() != mathist_.size()) {
250 return false;
251 }
252 const Eigen::MatrixXd& S = S_->Matrix();
253 for (std::size_t k = 0; k < mathist_.size(); ++k) {
254 const Eigen::MatrixXd e =
255 Sminusahalf.transpose() *
256 (mathist_[k] * dmatHist_[k] * S - S * dmatHist_[k] * mathist_[k]) *
258 if ((e - errors[k]).cwiseAbs().maxCoeff() > 1e-12 ||
259 std::abs(e.cwiseAbs().maxCoeff() - errhist_[k]) > 1e-12) {
260 return false;
261 }
262 }
263 return true;
264}
265
268 << TimeStamp() << " Convergence Options:" << std::flush;
270 << "\t\t Delta E [Ha]: " << opt_.Econverged << std::flush;
272 << "\t\t DIIS max error: " << opt_.error_converged << std::flush;
273 if (opt_.usediis) {
275 << "\t\t DIIS histlength: " << opt_.histlength << std::flush;
277 << "\t\t ADIIS start: " << opt_.adiis_start << std::flush;
279 << "\t\t DIIS start: " << opt_.diis_start << std::flush;
280 std::string del = "oldest";
281 if (opt_.maxout) {
282 del = "largest";
283 }
285 << "\t\t Deleting " << del << " element from DIIS hist" << std::flush;
286 }
288 << "\t\t Levelshift[Ha]: " << opt_.levelshift << std::flush;
290 << "\t\t Levelshift end: " << opt_.levelshiftend << std::flush;
292 << "\t\t Mixing Parameter alpha: " << opt_.mixingparameter << std::flush;
294 << "\t\t Mixing end: " << opt_.mixingend << std::flush;
296 << "\t\t Energy reset [Ha]: " << opt_.energy_reset
297 << (opt_.energy_reset > 0.0 ? "" : " (off)") << std::flush;
298}
299
300// Solve the generalized Roothaan-Hall problem
301//
302// F C = S C eps
303//
304// by symmetric orthogonalization: diagonalize X^T F X with X = S^{-1/2} and
305// back-transform the eigenvectors as C = X C' .
307 const Eigen::MatrixXd& H) const {
308 // transform to orthogonal for
309 Eigen::MatrixXd H_ortho = Sminusahalf.transpose() * H * Sminusahalf;
310 if (removed_projector_.size() > 0) {
312 }
313 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(H_ortho);
314
315 if (es.info() != Eigen::ComputationInfo::Success) {
316 throw std::runtime_error("Matrix Diagonalisation failed. DiagInfo" +
317 std::to_string(es.info()));
318 }
319
320 tools::EigenSystem result;
321 result.eigenvalues() = es.eigenvalues();
322
323 result.eigenvectors() = Sminusahalf * es.eigenvectors();
324 return result;
325}
326
327// Add a virtual-space level shift
328//
329// F <- F + S C_virt Delta C_virt^T S,
330//
331// implemented by placing the scalar shift on the virtual diagonal in the MO
332// basis built from the previous iteration. This leaves occupied orbitals
333// unchanged while opening the HOMO-LUMO gap during difficult SCF phases.
334void ConvergenceAcc::Levelshift(Eigen::MatrixXd& H,
335 const Eigen::MatrixXd& MOs_old) const {
336 if (opt_.levelshift < 1e-9) {
337 return;
338 }
339 Eigen::VectorXd virt = Eigen::VectorXd::Zero(H.rows());
340 for (Index i = nocclevels_; i < H.rows(); i++) {
341 virt(i) = opt_.levelshift;
342 }
343
345 << TimeStamp() << " Using levelshift:" << opt_.levelshift << " Hartree"
346 << std::flush;
347 Eigen::MatrixXd vir = S_->Matrix() * MOs_old * virt.asDiagonal() *
348 MOs_old.transpose() * S_->Matrix();
349 H += vir;
350 return;
351}
352
353/*
354Eigen::MatrixXd ConvergenceAcc::DensityMatrix(
355 const tools::EigenSystem& MOs) const {
356 Eigen::MatrixXd result;
357 if (opt_.mode == KSmode::closed) {
358 result = DensityMatrixGroundState(MOs.eigenvectors());
359 } else if (opt_.mode == KSmode::open) {
360 result = DensityMatrixGroundState_unres(MOs.eigenvectors());
361 } else if (opt_.mode == KSmode::fractional) {
362 result = DensityMatrixGroundState_frac(MOs);
363 }
364 return result;
365} */
366
367// Closed-shell AO density matrix
368//
369// P = 2 C_occ C_occ^T,
370//
371// where each occupied spatial orbital contributes one alpha and one beta
372// electron.
374 const Eigen::MatrixXd& MOs) const {
375 const Eigen::MatrixXd occstates = MOs.leftCols(nocclevels_);
376 Eigen::MatrixXd dmatGS = 2.0 * occstates * occstates.transpose();
377 return dmatGS;
378}
379
380// Spin-resolved unrestricted AO density matrix for one spin channel,
381//
382// P^sigma = C_occ^sigma (C_occ^sigma)^T,
383//
384// with no factor of two because a single spin channel is represented.
386 const Eigen::MatrixXd& MOs) const {
387 if (nocclevels_ == 0) {
388 return Eigen::MatrixXd::Zero(MOs.rows(), MOs.rows());
389 }
390 Eigen::MatrixXd occstates = MOs.leftCols(nocclevels_);
391 Eigen::MatrixXd dmatGS = occstates * occstates.transpose();
392 return dmatGS;
393}
394
395// Fractionally occupied AO density matrix
396//
397// P = C f C^T,
398//
399// where f is a diagonal matrix of orbital occupations assembled from the
400// configured electron count.
401// Fractional-occupation AO density matrix
402//
403// P = C n C^T,
404//
405// where n is the diagonal matrix of orbital occupations provided in the
406// EigenSystem container.
408 const tools::EigenSystem& MOs) const {
409 if (opt_.numberofelectrons == 0) {
410 return Eigen::MatrixXd::Zero(MOs.eigenvectors().rows(),
411 MOs.eigenvectors().rows());
412 }
413
414 Eigen::VectorXd occupation = Eigen::VectorXd::Zero(MOs.eigenvalues().size());
415 std::vector<std::vector<Index> > degeneracies;
416 double buffer = 1e-4;
417 degeneracies.push_back(std::vector<Index>{0});
418 for (Index i = 1; i < occupation.size(); i++) {
419 if (MOs.eigenvalues()(i) <
420 MOs.eigenvalues()(degeneracies[degeneracies.size() - 1][0]) + buffer) {
421 degeneracies[degeneracies.size() - 1].push_back(i);
422 } else {
423 degeneracies.push_back(std::vector<Index>{i});
424 }
425 }
426 Index numofelec = opt_.numberofelectrons;
427 for (const std::vector<Index>& deglevel : degeneracies) {
428 Index numofpossibleelectrons = 2 * Index(deglevel.size());
429 if (numofpossibleelectrons <= numofelec) {
430 for (Index i : deglevel) {
431 occupation(i) = 2;
432 }
433 numofelec -= numofpossibleelectrons;
434 } else {
435 double occ = double(numofelec) / double(deglevel.size());
436 for (Index i : deglevel) {
437 occupation(i) = occ;
438 }
439 break;
440 }
441 }
442 Eigen::MatrixXd dmatGS = MOs.eigenvectors() * occupation.asDiagonal() *
443 MOs.eigenvectors().transpose();
444 return dmatGS;
445}
446
447/*******************************************************
448 * EXTENSION FOR SPIN-KS-DFT
449 *******************************************************/
450
453 const Eigen::MatrixXd& MOs) const {
454
455 const Index n_docc =
456 std::min(opt_.number_alpha_electrons, opt_.number_beta_electrons);
457 const Index n_socc_alpha = opt_.number_alpha_electrons - n_docc;
458
459 SpinDensity result;
460 result.alpha = Eigen::MatrixXd::Zero(MOs.rows(), MOs.rows());
461 result.beta = Eigen::MatrixXd::Zero(MOs.rows(), MOs.rows());
462
463 if (n_docc > 0) {
464 const Eigen::MatrixXd docc = MOs.leftCols(n_docc);
465 const Eigen::MatrixXd d_docc = docc * docc.transpose();
466 result.alpha += d_docc;
467 result.beta += d_docc;
468 }
469
470 if (n_socc_alpha > 0) {
471 const Eigen::MatrixXd socc = MOs.middleCols(n_docc, n_socc_alpha);
472 result.alpha += socc * socc.transpose();
473 }
474
475 return result;
476}
477
478// Construct spin-resolved densities according to the configured occupation
479// model. For restricted open-shell cases the same spatial orbitals are split
480// into doubly and singly occupied subsets using the stored alpha/beta counts.
482 const tools::EigenSystem& MOs) const {
483
484 if (opt_.mode == KSmode::restricted_open) {
486 } else if (opt_.mode == KSmode::closed) {
487 Eigen::MatrixXd d = DensityMatrixGroundState(MOs.eigenvectors());
488 return {0.5 * d, 0.5 * d};
489 } else if (opt_.mode == KSmode::open) {
490 Eigen::MatrixXd d = DensityMatrixGroundState_unres(MOs.eigenvectors());
491 return {d, Eigen::MatrixXd::Zero(d.rows(), d.cols())};
492 } else {
493 Eigen::MatrixXd d = DensityMatrixGroundState_frac(MOs);
494 return {0.5 * d, 0.5 * d};
495 }
496}
497
499 const tools::EigenSystem& MOs) const {
500 SpinDensity spin_dmat = DensityMatrixSpinResolved(MOs);
501 return spin_dmat.total();
502}
503
504} // namespace xtp
505} // namespace votca
const Eigen::VectorXd & eigenvalues() const
Definition eigensystem.h:30
const Eigen::MatrixXd & eigenvectors() const
Definition eigensystem.h:33
Eigen::MatrixXd DensityMatrix(const tools::EigenSystem &MOs) const
static constexpr double kRemovedShift
Eigen::MatrixXd DensityMatrixGroundState_unres(const Eigen::MatrixXd &MOs) const
std::vector< Eigen::MatrixXd > mathist_
Eigen::MatrixXd Iterate(const Eigen::MatrixXd &dmat, Eigen::MatrixXd &H, tools::EigenSystem &MOs, double totE)
double getDIIsError() const
Return the DIIS commutator norm from the latest iteration.
tools::EigenSystem SolveFockmatrix(const Eigen::MatrixXd &H) const
Solve the generalized eigenvalue problem for the current Fock matrix.
std::vector< double > errhist_
void PrintConfigOptions() const
Print the active convergence-acceleration settings to the logger.
void Levelshift(Eigen::MatrixXd &H, const Eigen::MatrixXd &MOs_old) const
Apply a virtual-space level shift in the molecular-orbital basis.
static constexpr Index kResetCooldown
double getDeltaE() const
Return the total-energy change between the two most recent SCF iterations.
static constexpr Index kMaxEnergyResets
Eigen::MatrixXd DensityMatrixGroundState_frac(const tools::EigenSystem &MOs) const
Construct a fractional-occupation density matrix from orbital occupations.
std::vector< double > totE_
void setOverlap(AOOverlap &S, double etol)
Precompute overlap-dependent quantities used when solving the Fock matrix.
Eigen::MatrixXd removed_projector_
Eigen::MatrixXd DensityMatrixGroundState(const Eigen::MatrixXd &MOs) const
std::vector< Eigen::MatrixXd > dmatHist_
static constexpr double kEnergyRiseForADIIS
SpinDensity DensityMatrixSpinResolved(const tools::EigenSystem &MOs) const
SpinDensity DensityMatrixGroundState_restricted_open(const Eigen::MatrixXd &MOs) const
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
#define XTP_LOG(level, log)
Definition logger.h:40
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
Spin-resolved density matrices returned for open-shell SCF updates.
Eigen::MatrixXd total() const
Return the total density P = P^alpha + P^beta.