votca 2026-dev
Loading...
Searching...
No Matches
uks_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// Local VOTCA includes
23
24#include <algorithm>
25
26#include <tuple>
27
28namespace votca {
29namespace xtp {
30
32 const options& opt_beta) {
33 opt_alpha_ = opt_alpha;
34 opt_beta_ = opt_beta;
35
36 nocclevels_alpha_ = opt_alpha_.numberofelectrons;
37 nocclevels_beta_ = opt_beta_.numberofelectrons;
38
39 // one shared DIIS/ADIIS history length
40 diis_.setHistLength(opt_alpha_.histlength);
41 // adiis_.setHistLength(opt_alpha_.histlength);
42}
43
45
47 // As ConvergenceAcc::setOverlap: removed directions are kept out of the
48 // occupied and low virtual space via removed_projector_.
49 S_ = &S;
50 const SymmetricEigenSystem es = SymmetricEigen(S.Matrix());
51 const Eigen::VectorXd& s_eig = es.values;
52 Eigen::VectorXd inv_sqrt = Eigen::VectorXd::Zero(s_eig.size());
53 Index removed = 0;
54 for (Index i = 0; i < s_eig.size(); ++i) {
55 if (s_eig(i) < etol) {
56 ++removed;
57 } else {
58 inv_sqrt(i) = 1.0 / std::sqrt(s_eig(i));
59 }
60 }
61 Sminusahalf = es.vectors * inv_sqrt.asDiagonal() * es.vectors.transpose();
62 removed_projector_.resize(0, 0);
63 if (removed > 0) {
64 const Eigen::MatrixXd U = es.vectors.leftCols(removed);
65 removed_projector_ = U * U.transpose();
66 }
68 << TimeStamp() << " Smallest value of AOOverlap matrix is " << s_eig(0)
69 << std::flush;
71 << TimeStamp() << " Removed " << removed
72 << " basisfunction from inverse overlap matrix (threshold " << etol << ")"
73 << std::flush;
74 if (removed == 0 && s_eig(0) < 1e3 * etol) {
76 << TimeStamp()
77 << " WARNING: the overlap matrix is nearly singular; if the SCF is "
78 "unstable, raise xtpdft.overlap_tolerance above "
79 << s_eig(0) << "." << std::flush;
80 }
81}
82
84 const Eigen::MatrixXd& H) const {
85 Eigen::MatrixXd H_ortho = Sminusahalf.transpose() * H * Sminusahalf;
86 if (removed_projector_.size() > 0) {
88 }
89 const SymmetricEigenSystem es = SymmetricEigen(H_ortho);
90 if (!es.values.allFinite()) {
91 throw std::runtime_error("Matrix Diagonalisation failed");
92 }
93
94 tools::EigenSystem result;
95 result.eigenvalues() = es.values;
96 result.eigenvectors() = Sminusahalf * es.vectors;
97 return result;
98}
99
101 const Eigen::VectorXd& v_ov, Index nao, Index nocclevels) const {
102 Index nvirt = nao - nocclevels;
103 Eigen::MatrixXd kappa = Eigen::MatrixXd::Zero(nao, nao);
104 for (Index i = 0; i < nocclevels; ++i) {
105 for (Index a = 0; a < nvirt; ++a) {
106 double val = v_ov(i * nvirt + a);
107 kappa(i, nocclevels + a) = val;
108 kappa(nocclevels + a, i) = -val;
109 }
110 }
111 return kappa;
112}
113
114std::pair<Eigen::MatrixXd, Eigen::MatrixXd>
116 Index nao_alpha,
117 Index nocclevels_alpha,
118 Index nao_beta,
119 Index nocclevels_beta) const {
120 Index n_ov_alpha = nocclevels_alpha * (nao_alpha - nocclevels_alpha);
121 Eigen::VectorXd v_alpha = v.head(n_ov_alpha);
122 Eigen::VectorXd v_beta = v.tail(v.size() - n_ov_alpha);
123 Eigen::MatrixXd kappa_alpha =
124 UnflattenRotation(v_alpha, nao_alpha, nocclevels_alpha);
125 Eigen::MatrixXd kappa_beta =
126 UnflattenRotation(v_beta, nao_beta, nocclevels_beta);
127 return {kappa_alpha, kappa_beta};
128}
129
131 const Eigen::VectorXd& v, const Eigen::MatrixXd& C_alpha,
132 Index nocclevels_alpha, const Eigen::MatrixXd& C_beta,
133 Index nocclevels_beta, const CoupledFockBuilder& coupled_fock_builder,
134 double finite_diff_step) const {
135 Index nao_alpha = C_alpha.rows();
136 Index nvirt_alpha = nao_alpha - nocclevels_alpha;
137 Index nao_beta = C_beta.rows();
138 Index nvirt_beta = nao_beta - nocclevels_beta;
139 Index n_ov_alpha = nocclevels_alpha * nvirt_alpha;
140 Index n_ov_beta = nocclevels_beta * nvirt_beta;
141
142 auto [kappa_alpha_trial, kappa_beta_trial] = UnflattenCoupledRotation(
143 v, nao_alpha, nocclevels_alpha, nao_beta, nocclevels_beta);
144 kappa_alpha_trial *= finite_diff_step;
145 kappa_beta_trial *= finite_diff_step;
146
147 // Same linearized-exponential + Lowdin-reorthonormalization approach
148 // as BuildSigmaVector's own EvaluateGradientAt -- but rotates BOTH
149 // channels together and builds BOTH new Fock matrices together in
150 // ONE coupled_fock_builder call, rather than one channel at a time
151 // with the other held fixed. This is what actually captures the
152 // alpha-beta coupling: the shared Coulomb potential and the XC
153 // kernel's cross-spin terms naturally see both perturbed densities
154 // together inside coupled_fock_builder, rather than needing this
155 // function to construct any cross-coupling term explicitly itself.
156 auto EvaluateBothGradientsAt = [&](const Eigen::MatrixXd& kappa_alpha,
157 const Eigen::MatrixXd& kappa_beta)
158 -> std::pair<Eigen::MatrixXd, Eigen::MatrixXd> {
159 Eigen::MatrixXd C_alpha_rot =
160 C_alpha *
161 (Eigen::MatrixXd::Identity(nao_alpha, nao_alpha) + kappa_alpha);
162 Eigen::MatrixXd nonortho_alpha =
163 C_alpha_rot.transpose() * S_->Matrix() * C_alpha_rot;
164 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es_alpha(nonortho_alpha);
165 C_alpha_rot = C_alpha_rot * es_alpha.operatorInverseSqrt();
166
167 Eigen::MatrixXd C_beta_rot =
168 C_beta * (Eigen::MatrixXd::Identity(nao_beta, nao_beta) + kappa_beta);
169 Eigen::MatrixXd nonortho_beta =
170 C_beta_rot.transpose() * S_->Matrix() * C_beta_rot;
171 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es_beta(nonortho_beta);
172 C_beta_rot = C_beta_rot * es_beta.operatorInverseSqrt();
173
174 Eigen::MatrixXd C_alpha_occ_rot = C_alpha_rot.leftCols(nocclevels_alpha);
175 Eigen::MatrixXd D_alpha_rot = C_alpha_occ_rot * C_alpha_occ_rot.transpose();
176 Eigen::MatrixXd C_beta_occ_rot = C_beta_rot.leftCols(nocclevels_beta);
177 Eigen::MatrixXd D_beta_rot = C_beta_occ_rot * C_beta_occ_rot.transpose();
178
179 SpinFock H_rot = coupled_fock_builder(D_alpha_rot, D_beta_rot);
180 Eigen::MatrixXd F_MO_alpha_rot =
181 C_alpha_rot.transpose() * H_rot.alpha * C_alpha_rot;
182 Eigen::MatrixXd F_MO_beta_rot =
183 C_beta_rot.transpose() * H_rot.beta * C_beta_rot;
184 return {F_MO_alpha_rot, F_MO_beta_rot};
185 };
186
187 auto [F_MO_alpha_plus, F_MO_beta_plus] =
188 EvaluateBothGradientsAt(kappa_alpha_trial, kappa_beta_trial);
189 auto [F_MO_alpha_minus, F_MO_beta_minus] =
190 EvaluateBothGradientsAt(-kappa_alpha_trial, -kappa_beta_trial);
191
192 Eigen::VectorXd sigma(n_ov_alpha + n_ov_beta);
193 for (Index i = 0; i < nocclevels_alpha; ++i) {
194 for (Index a = 0; a < nvirt_alpha; ++a) {
195 double g_plus_ia = F_MO_alpha_plus(i, nocclevels_alpha + a);
196 double g_minus_ia = F_MO_alpha_minus(i, nocclevels_alpha + a);
197 sigma(i * nvirt_alpha + a) =
198 (g_plus_ia - g_minus_ia) / (2.0 * finite_diff_step);
199 }
200 }
201 for (Index i = 0; i < nocclevels_beta; ++i) {
202 for (Index a = 0; a < nvirt_beta; ++a) {
203 double g_plus_ia = F_MO_beta_plus(i, nocclevels_beta + a);
204 double g_minus_ia = F_MO_beta_minus(i, nocclevels_beta + a);
205 sigma(n_ov_alpha + i * nvirt_beta + a) =
206 (g_plus_ia - g_minus_ia) / (2.0 * finite_diff_step);
207 }
208 }
209 return sigma;
210}
211
213 const Eigen::MatrixXd& H_AO, const tools::EigenSystem& MOs,
214 Index nocclevels, double& predicted_energy_change) const {
215 Index nao = MOs.eigenvectors().rows();
216 const Eigen::MatrixXd& C = MOs.eigenvectors();
217 const Eigen::VectorXd& eps = MOs.eigenvalues();
218
219 // Orbital gradient: the occ-virt block of the MO-basis Fock matrix,
220 // which vanishes exactly at self-consistency (F_MO would be block-
221 // diagonal, occ-occ and virt-virt only).
222 Eigen::MatrixXd F_MO = C.transpose() * H_AO * C;
223
224 // Antisymmetric rotation generator: nonzero only in the occ-virt (and
225 // virt-occ, by antisymmetry) blocks -- occ-occ/virt-virt rotations
226 // would leave the density matrix (and therefore the energy) entirely
227 // unchanged, so there is nothing to gain from including them.
228 Eigen::MatrixXd kappa = Eigen::MatrixXd::Zero(nao, nao);
229 // g_h_ratio(i,a) below stores kappa_ia's own g_ia/h_ia BEFORE the
230 // whole-matrix trust-radius scaling further down -- needed again
231 // afterward to compute the quadratic model's predicted energy change
232 // using the FINAL (per-element- and trust-radius-clamped) kappa,
233 // since g_ia and h_ia themselves do not change under that later,
234 // uniform rescaling, only kappa does.
235 Eigen::MatrixXd h_matrix = Eigen::MatrixXd::Zero(nao, nao);
236 // Guards near-degenerate occ-virt orbital pairs from producing a
237 // wildly oversized step for THAT pair specifically -- confirmed
238 // directly against an independent ORCA reference run on this exact
239 // system, whose own TRAH implementation repeatedly hit BOTH
240 // genuinely negative gaps (occupied/virtual character swapping
241 // during iteration, since occupation here is defined by column
242 // index, not current energy ordering -- down to -0.635 Ha) and
243 // gaps numerically indistinguishable from zero (as small as
244 // 0.000002 Ha), needing its own explicit warnings and special
245 // handling for both. abs() here is essential, not cosmetic: a
246 // signed gap of -0.6 would otherwise pass std::max(-0.6, kMinGap)
247 // as if it were the tiny POSITIVE value kMinGap, producing an
248 // enormous, wrong-direction step for exactly that pair -- the
249 // Hessian approximation must be treated as positive-definite
250 // (a legitimate descent direction) regardless of the true, possibly
251 // negative or near-zero curvature this diagonal approximation
252 // cannot itself represent.
253 constexpr double kMinGap = 1e-3;
254 // Per-PAIR step cap, independent of and in addition to the overall,
255 // whole-matrix trust-radius bound below -- confirmed necessary
256 // because a single pathological pair (as above) could otherwise
257 // dominate the entire rotation DIRECTION even after the whole
258 // matrix is normalized to the trust radius, silently scaling every
259 // other, well-behaved pair down to near-nothing while that one bad
260 // pair still controls where the step actually points.
261 constexpr double kMaxKappaElement = 0.1;
262 for (Index i = 0; i < nocclevels; ++i) {
263 for (Index a = nocclevels; a < nao; ++a) {
264 double gap = std::max(std::abs(eps(a) - eps(i)), kMinGap);
265 // Approximate, diagonal Hessian: orbital energy differences --
266 // the same cheap starting approximation used by most quasi-
267 // Newton SCF methods (e.g. SOSCF's own initial Hessian guess).
268 double h_ia = 2.0 * gap;
269 double kappa_ia = -F_MO(i, a) / h_ia;
270 kappa_ia = std::clamp(kappa_ia, -kMaxKappaElement, kMaxKappaElement);
271 kappa(i, a) = kappa_ia;
272 kappa(a, i) = -kappa_ia;
273 h_matrix(i, a) = h_ia;
274 }
275 }
276
277 double knorm = kappa.norm();
278 if (knorm > trust_radius_current_) {
279 kappa *= (trust_radius_current_ / knorm);
280 }
281
282 // Quadratic model's own predicted energy change, Sum_ia[g_ia*kappa_ia
283 // + 0.5*h_ia*kappa_ia^2], using the FINAL kappa (after both the
284 // per-element clamp and the whole-matrix trust-radius scaling above)
285 // -- needed by Iterate's own Fletcher-style accept/reject logic.
286 // g_ia = F_MO(i,a) directly, matching the sign convention kappa_ia =
287 // -F_MO(i,a)/h_ia was itself built from above.
288 predicted_energy_change = 0.0;
289 for (Index i = 0; i < nocclevels; ++i) {
290 for (Index a = nocclevels; a < nao; ++a) {
291 double kappa_ia = kappa(i, a);
292 predicted_energy_change +=
293 F_MO(i, a) * kappa_ia + 0.5 * h_matrix(i, a) * kappa_ia * kappa_ia;
294 }
295 }
296
297 // exp(kappa) ~= I + kappa is valid specifically because the trust-
298 // radius bound above keeps kappa small -- re-orthonormalize
299 // afterward since I+kappa is only approximately unitary, using the
300 // same Lowdin (symmetric orthogonalization) approach as
301 // DFTEngine::OrthogonalizeGuess.
302 Eigen::MatrixXd C_new = C * (Eigen::MatrixXd::Identity(nao, nao) + kappa);
303 Eigen::MatrixXd nonortho = C_new.transpose() * S_->Matrix() * C_new;
304 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es_ortho(nonortho);
305 return C_new * es_ortho.operatorInverseSqrt();
306}
307
308namespace {
309// Local operator matching DavidsonSolver's own MatrixReplacement
310// template interface (.rows(), .diagonal(), operator*(MatrixXd)) for
311// the augmented Hessian over the COMBINED (alpha+beta) rotation space
312// -- g and diag_h are the concatenated, both-channel vectors, and each
313// operator* call rotates BOTH channels together via
314// UKSConvergenceAcc::BuildCoupledSigmaVector, capturing the genuine
315// alpha-beta coupling that treating the two channels independently
316// would not.
317struct CoupledAugmentedHessianOperator {
318 const Eigen::VectorXd& g;
319 const Eigen::MatrixXd& C_alpha;
320 Index nocclevels_alpha;
321 const Eigen::MatrixXd& C_beta;
322 Index nocclevels_beta;
323 double alpha_scale;
324 const UKSConvergenceAcc::CoupledFockBuilder& coupled_fock_builder;
325 const UKSConvergenceAcc* self;
326 const Eigen::VectorXd& diag_h;
327
328 Index rows() const { return 1 + g.size(); }
329
330 Eigen::VectorXd diagonal() const {
331 Eigen::VectorXd d(1 + g.size());
332 d(0) = 0.0;
333 d.tail(g.size()) = diag_h;
334 return d;
335 }
336
337 Eigen::MatrixXd operator*(const Eigen::MatrixXd& V) const {
338 Eigen::MatrixXd AV = Eigen::MatrixXd::Zero(V.rows(), V.cols());
339 for (Index col = 0; col < V.cols(); ++col) {
340 double v0 = V(0, col);
341 Eigen::VectorXd v_ov = V.block(1, col, g.size(), 1);
342 AV(0, col) = alpha_scale * g.dot(v_ov);
343 Eigen::VectorXd sigma =
344 self->BuildCoupledSigmaVector(v_ov, C_alpha, nocclevels_alpha, C_beta,
345 nocclevels_beta, coupled_fock_builder);
346 AV.block(1, col, g.size(), 1) = alpha_scale * g * v0 + sigma;
347 }
348 return AV;
349 }
350};
351} // namespace
352
353std::pair<Eigen::MatrixXd, Eigen::MatrixXd>
355 const Eigen::MatrixXd& H_AO_alpha, const tools::EigenSystem& MOs_alpha,
356 Index nocclevels_alpha, const Eigen::MatrixXd& H_AO_beta,
357 const tools::EigenSystem& MOs_beta, Index nocclevels_beta,
358 const CoupledFockBuilder& coupled_fock_builder, double trust_radius,
359 double& predicted_energy_change) const {
360 Index nao_alpha = MOs_alpha.eigenvectors().rows();
361 Index nvirt_alpha = nao_alpha - nocclevels_alpha;
362 Index n_ov_alpha = nocclevels_alpha * nvirt_alpha;
363 const Eigen::MatrixXd& C_alpha = MOs_alpha.eigenvectors();
364 const Eigen::VectorXd& eps_alpha = MOs_alpha.eigenvalues();
365
366 Index nao_beta = MOs_beta.eigenvectors().rows();
367 Index nvirt_beta = nao_beta - nocclevels_beta;
368 Index n_ov_beta = nocclevels_beta * nvirt_beta;
369 const Eigen::MatrixXd& C_beta = MOs_beta.eigenvectors();
370 const Eigen::VectorXd& eps_beta = MOs_beta.eigenvalues();
371
372 Index n_ov = n_ov_alpha + n_ov_beta;
373
374 Eigen::MatrixXd F_MO_alpha = C_alpha.transpose() * H_AO_alpha * C_alpha;
375 Eigen::MatrixXd F_MO_beta = C_beta.transpose() * H_AO_beta * C_beta;
376
377 // Combined gradient and diagonal-Hessian preconditioner: alpha's own
378 // block first, beta's immediately after -- matching
379 // UnflattenCoupledRotation/BuildCoupledSigmaVector's own layout
380 // convention. The diagonal preconditioner itself is still built
381 // per-channel from each channel's OWN orbital-energy gaps (same
382 // formula as an earlier, decoupled AugmentedHessianStep
383 // implementation's own diag_h) -- only the actual
384 // Hessian-VECTOR product (via BuildCoupledSigmaVector, inside
385 // CoupledAugmentedHessianOperator) captures the real cross-channel
386 // coupling; the preconditioner is only ever an approximate guide for
387 // the Davidson iteration, not the step itself, so this
388 // simplification (no explicit alpha-beta cross term in the
389 // preconditioner) does not undermine what this whole undertaking is
390 // actually meant to fix.
391 Eigen::VectorXd g(n_ov);
392 Eigen::VectorXd diag_h(n_ov);
393 constexpr double kMinGap = 1e-3;
394 for (Index i = 0; i < nocclevels_alpha; ++i) {
395 for (Index a = 0; a < nvirt_alpha; ++a) {
396 g(i * nvirt_alpha + a) = F_MO_alpha(i, nocclevels_alpha + a);
397 double gap = std::max(
398 std::abs(eps_alpha(nocclevels_alpha + a) - eps_alpha(i)), kMinGap);
399 diag_h(i * nvirt_alpha + a) = 2.0 * gap;
400 }
401 }
402 for (Index i = 0; i < nocclevels_beta; ++i) {
403 for (Index a = 0; a < nvirt_beta; ++a) {
404 g(n_ov_alpha + i * nvirt_beta + a) = F_MO_beta(i, nocclevels_beta + a);
405 double gap = std::max(
406 std::abs(eps_beta(nocclevels_beta + a) - eps_beta(i)), kMinGap);
407 diag_h(n_ov_alpha + i * nvirt_beta + a) = 2.0 * gap;
408 }
409 }
410
411 double alpha_min = 1.0;
412 double alpha_max = 1000.0;
413 Eigen::VectorXd best_kappa_flat = Eigen::VectorXd::Zero(n_ov);
414 double best_mu = 0.0;
415
416 Eigen::MatrixXd initial_guess = Eigen::MatrixXd::Zero(1 + n_ov, 2);
417 initial_guess(0, 0) = 1.0;
418 double gnorm = g.norm();
419 if (gnorm > 1e-12) {
420 initial_guess.block(1, 1, n_ov, 1) = g / gnorm;
421 } else {
422 initial_guess(1, 1) = 1.0;
423 }
424
425 // Returns whether the Davidson solve itself actually converged
426 // (solver.info() == Eigen::ComputationInfo::Success) within its own
427 // iter_max_ budget -- confirmed necessary directly, from a real run
428 // on a genuinely difficult system (a -1 charge CDFT-constrained onto
429 // a 28-atom fragment): the solver's own diagnostic output showed
430 // "0.00% converged" repeated across dozens of iterations and several
431 // restarts, yet the ORIGINAL version of this lambda still
432 // unconditionally extracted eigenvectors()/eigenvalues() and used
433 // them as if the solve had succeeded -- there was no way for the
434 // caller to ever detect this had happened at all.
435 auto SolveForAlpha = [&](double alpha_try, Eigen::VectorXd& kappa_flat_out,
436 double& mu_out) -> bool {
437 CoupledAugmentedHessianOperator op{g,
438 C_alpha,
439 nocclevels_alpha,
440 C_beta,
441 nocclevels_beta,
442 alpha_try,
443 coupled_fock_builder,
444 this,
445 diag_h};
446 DavidsonSolver solver(*log_);
447 solver.set_matrix_type("SYMM");
448 solver.set_tolerance("loose");
449 solver.set_iter_max(opt_alpha_.davidson_max_iter);
450 solver.set_max_search_space(40);
451 solver.solve(op, 1, initial_guess);
452 if (solver.info() != Eigen::ComputationInfo::Success) {
453 return false;
454 }
455 Eigen::VectorXd eigvec = solver.eigenvectors().col(0);
456 mu_out = solver.eigenvalues()(0);
457 double v0 = eigvec(0);
458 if (std::abs(v0) < 1e-8) {
459 kappa_flat_out = Eigen::VectorXd::Zero(g.size());
460 return true;
461 }
462 kappa_flat_out = eigvec.tail(g.size()) / v0;
463 return true;
464 };
465
466 double alpha_try = alpha_min;
467 constexpr int kMaxBisectionIters = 20;
468 bool have_converged_once = false;
469 for (int bisection_iter = 0; bisection_iter < kMaxBisectionIters;
470 ++bisection_iter) {
471 Eigen::VectorXd kappa_flat;
472 double mu;
473 bool converged = SolveForAlpha(alpha_try, kappa_flat, mu);
474 if (!converged) {
475 // Never trust/store a failed solve's own kappa/mu at all -- see
476 // this function's own header comment on SolveForAlpha above for
477 // why the ORIGINAL version of this loop did exactly that, and
478 // the real, confirmed consequence. Treated the same way as
479 // step_norm > trust_radius: alpha_min moves up, since a LARGER
480 // alpha generally makes the augmented Hessian's own lowest
481 // eigenvalue more distinct from the rest of its spectrum --
482 // easier for Davidson to isolate, not harder -- so retrying at
483 // a larger alpha is a reasonable, physically-motivated response
484 // to a failed solve, not an arbitrary guess.
485 alpha_min = alpha_try;
486 alpha_try = 0.5 * (alpha_min + alpha_max);
487 continue;
488 }
489 have_converged_once = true;
490 double step_norm = kappa_flat.norm() / alpha_try;
491 best_kappa_flat = kappa_flat;
492 best_mu = mu;
493 if (std::abs(step_norm - trust_radius) < 0.01 * trust_radius) {
494 break;
495 }
496 if (step_norm > trust_radius) {
497 alpha_min = alpha_try;
498 } else {
499 alpha_max = alpha_try;
500 }
501 alpha_try = 0.5 * (alpha_min + alpha_max);
502 }
503
504 if (!have_converged_once) {
505 // Every single bisection attempt's own Davidson solve failed to
506 // converge -- confirmed directly, from a real run, that silently
507 // proceeding here (with best_kappa_flat/best_mu left at whatever
508 // they were default-initialized to, never actually set by a
509 // genuine solve) is the wrong thing to do: there is no
510 // meaningful step to take at all, and the caller deserves to know
511 // this failed rather than silently receiving a zero/garbage step.
512 throw std::runtime_error(
513 "CoupledAugmentedHessianStep: DavidsonSolver failed to converge "
514 "for every bisection trial (all " +
515 std::to_string(kMaxBisectionIters) +
516 " attempts) -- no genuine augmented-Hessian step could be "
517 "computed at all.");
518 }
519
521 << TimeStamp()
522 << " CoupledAugmentedHessianStep bisection diagnostic: "
523 "final alpha_try="
524 << alpha_try
525 << ", achieved step_norm=" << (best_kappa_flat.norm() / alpha_try)
526 << ", requested trust_radius=" << trust_radius << std::flush;
527
528 auto [kappa_alpha, kappa_beta] = UnflattenCoupledRotation(
529 best_kappa_flat, nao_alpha, nocclevels_alpha, nao_beta, nocclevels_beta);
530
531 // ONE, combined predicted energy change for the whole, coupled step
532 // -- same formula as an earlier, decoupled AugmentedHessianStep
533 // implementation's own (Q(kappa)-E0 =
534 // 0.5*(g^T*kappa + mu*||kappa||^2)), but now naturally a single
535 // number for both channels together, rather than needing to be
536 // summed from two separate calls the way that earlier, decoupled
537 // implementation's own path did.
538 predicted_energy_change =
539 0.5 * (g.dot(best_kappa_flat) + best_mu * best_kappa_flat.squaredNorm());
540
541 Eigen::MatrixXd C_alpha_new =
542 C_alpha * (Eigen::MatrixXd::Identity(nao_alpha, nao_alpha) + kappa_alpha);
543 Eigen::MatrixXd nonortho_alpha =
544 C_alpha_new.transpose() * S_->Matrix() * C_alpha_new;
545 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es_alpha(nonortho_alpha);
546 C_alpha_new = C_alpha_new * es_alpha.operatorInverseSqrt();
547
548 Eigen::MatrixXd C_beta_new =
549 C_beta * (Eigen::MatrixXd::Identity(nao_beta, nao_beta) + kappa_beta);
550 Eigen::MatrixXd nonortho_beta =
551 C_beta_new.transpose() * S_->Matrix() * C_beta_new;
552 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es_beta(nonortho_beta);
553 C_beta_new = C_beta_new * es_beta.operatorInverseSqrt();
554
555 return {C_alpha_new, C_beta_new};
556}
557
559 const Eigen::MatrixXd& MOs, Index nocclevels) const {
560 if (nocclevels == 0) {
561 return Eigen::MatrixXd::Zero(MOs.rows(), MOs.rows());
562 }
563 Eigen::MatrixXd occstates = MOs.leftCols(nocclevels);
564 return occstates * occstates.transpose();
565}
566
568 const tools::EigenSystem& MOs_alpha,
569 const tools::EigenSystem& MOs_beta) const {
570 SpinDensity result;
573 result.beta =
575 return result;
576}
577
578void UKSConvergenceAcc::Levelshift(Eigen::MatrixXd& H,
579 const Eigen::MatrixXd& MOs_old,
580 const options& opt, Index nocclevels) const {
581 if (opt.levelshift < 1e-9) {
582 return;
583 }
584 Eigen::VectorXd virt = Eigen::VectorXd::Zero(H.rows());
585 for (Index i = nocclevels; i < H.rows(); ++i) {
586 virt(i) = opt.levelshift;
587 }
588
590 << TimeStamp() << " Using levelshift:" << opt.levelshift << " Hartree"
591 << std::flush;
592
593 Eigen::MatrixXd vir = S_->Matrix() * MOs_old * virt.asDiagonal() *
594 MOs_old.transpose() * S_->Matrix();
595 H += vir;
596}
597
599 const Eigen::MatrixXd& dmat, const Eigen::MatrixXd& H) const {
600 const Eigen::MatrixXd& S = S_->Matrix();
601 return Sminusahalf.transpose() * (H * dmat * S - S * dmat * H) * Sminusahalf;
602}
603
604double UKSConvergenceAcc::CombinedError(const Eigen::MatrixXd& err_alpha,
605 const Eigen::MatrixXd& err_beta) const {
606 return std::max(err_alpha.cwiseAbs().maxCoeff(),
607 err_beta.cwiseAbs().maxCoeff());
608}
609
611 const SpinDensity& dmat, SpinFock& H, tools::EigenSystem& MOs_alpha,
612 tools::EigenSystem& MOs_beta, double totE) {
613
614 // Fletcher's trust-radius update (Helmich-Paris, J. Chem. Phys. 154,
615 // 164104 (2021), Sec. II D -- confirmed directly by reading that
616 // paper, not reconstructed from memory): verify whatever
617 // DirectMinimizationRotation step was taken last call, now that its
618 // actual effect on the energy (totE, just passed in -- computed by
619 // the caller from a real Fock build on that step's own density) is
620 // finally available. This CANNOT be checked within the same
621 // Iterate() call that took the step, since that call has no way to
622 // know what energy its own returned density will produce until the
623 // caller has built a new Fock matrix from it and come back around.
625 double actual_change = totE - direct_min_pre_energy_;
626 double r = (std::abs(direct_min_predicted_change_) > 1e-14)
627 ? actual_change / direct_min_predicted_change_
628 : -1.0; // treat a degenerate (~zero) predicted change
629 // as an outright reject, same as r<0 below --
630 // the model gave no useful information about
631 // this step at all.
633 << TimeStamp()
634 << " Direct-minimization step check: actual dE=" << actual_change
635 << ", predicted dE=" << direct_min_predicted_change_ << ", r=" << r
636 << ", trust radius=" << trust_radius_current_ << std::flush;
637 if (r < 0.0) {
638 // Reject: the quadratic model was not applicable within the
639 // given trust region (either the energy rose while predicted to
640 // fall, or vice versa). Revert to the pre-step MOs/energy and
641 // shrink the trust radius -- the NEXT call's own
642 // consecutive_adiis_failures_ check will naturally retry
643 // DirectMinimizationRotation from this reverted point with the
644 // smaller radius, since nothing here has changed the underlying
645 // (A)DIIS behavior that triggered it in the first place.
647 // Floor tied to BuildSigmaVector's own finite-difference step
648 // (kFiniteDiffStep = 1e-3, defined there): a real run showed
649 // trust_radius shrinking past 1e-6 while the step, predicted
650 // change, and actual change stayed EXACTLY identical every
651 // time -- the bisection's own alpha_min=1 floor means the
652 // gentlest achievable step cannot shrink further once alpha_min
653 // itself is the binding constraint, so continuing to request an
654 // even smaller trust radius changes nothing and the reject loop
655 // can never resolve on its own (confirmed directly: it only
656 // ended when the outer SCF's own 100-iteration budget ran out).
657 // More fundamentally, a trust radius below the sigma vector's
658 // own probing resolution is asking for precision the underlying
659 // finite-difference model was never built to provide -- its
660 // accuracy does not improve as the requested step shrinks, only
661 // the requested step size does. Once hit, give up on direct
662 // minimization for this SCF call rather than loop toward
663 // ever-smaller radii that cannot change the outcome.
667 << TimeStamp()
668 << " Direct-minimization trust radius fell "
669 "below its own finite-difference resolution floor ("
671 << ") without an accepted step -- "
672 "falling back to mixing instead of continuing to shrink."
673 << std::flush;
674 }
679 totE_.push_back(direct_min_pre_energy_);
680 direct_min_pending_ = false;
681 usedmixing_ = false;
682 return DensityMatrix(MOs_alpha, MOs_beta);
683 } else if (r <= 0.25) {
684 // Accepted, but the step was too long -- shrink for next time.
686 } else if (r > 0.75) {
687 // Accepted, and the model was a good fit -- grow for next time.
689 }
690 // 0.25 < r <= 0.75: accepted, trust radius left unchanged.
691 direct_min_pending_ = false;
692 }
693
694 // History, trimmed at ONE index for the Fock, density and error
695 // histories and for DIIS's own error history: the oldest entry, or with
696 // DIIS_maxout the entry with the largest error. Previously the index was
697 // a stale position (never re-based after an erase, maxerror_ never
698 // reset), and DIIS::Update could trim at a different index than the
699 // Fock history, pairing coefficients with the wrong matrices. totE_ is
700 // not trimmed: it is the chronological energy list getDeltaE reads.
701 Index drop = 0;
702 if (int(mathist_alpha_.size()) == opt_alpha_.histlength) {
703 if (opt_alpha_.maxout) {
704 drop = Index(std::max_element(errhist_.begin(), errhist_.end()) -
705 errhist_.begin());
706 }
707 mathist_alpha_.erase(mathist_alpha_.begin() + drop);
708 mathist_beta_.erase(mathist_beta_.begin() + drop);
709 dmatHist_alpha_.erase(dmatHist_alpha_.begin() + drop);
710 dmatHist_beta_.erase(dmatHist_beta_.begin() + drop);
711 errhist_.erase(errhist_.begin() + drop);
712 }
713
714 totE_.push_back(totE);
715
716 if (nocclevels_alpha_ > 0 &&
717 nocclevels_alpha_ < MOs_alpha.eigenvalues().size()) {
718 double gap_alpha = MOs_alpha.eigenvalues()(nocclevels_alpha_) -
719 MOs_alpha.eigenvalues()(nocclevels_alpha_ - 1);
720 // consecutive_adiis_failures_ < kMaxConsecutiveADIISFailures added
721 // deliberately: a real run showed level shift catastrophically
722 // breaking the Davidson solve this class's own direct-minimization
723 // fallback depends on (residual climbing past 395, far beyond any
724 // prior failure mode) once direct-minimization engaged (originally
725 // discovered against an earlier, decoupled AugmentedHessianStep
726 // implementation; the same mechanism still applies to
727 // CoupledAugmentedHessianStep's own diagonal preconditioner below,
728 // which is built the same way). Root cause: Levelshift() modifies H
729 // in place, BEFORE CoupledAugmentedHessianStep/DirectMinimizationRotation
730 // ever see it -- and that preconditioner (diag_h) is built directly
731 // from eps(a)-eps(i), the raw orbital energy gap, which becomes
732 // systematically inflated by the shift for virtual orbitals.
733 // ADIIS/DIIS do not
734 // have this problem (they operate on the Fock/density matrices
735 // themselves, never deriving a separate quantity like an orbital
736 // gap from the shifted eigenvalues), so level shift stays active
737 // for them -- this guard only disables it once direct-minimization
738 // is about to take over, since level shift's own purpose
739 // (discouraging occ-virt mixing during diagonalization) does not
740 // even apply to a method that never diagonalizes H at all.
741 if ((diiserror_ > opt_alpha_.levelshiftend &&
742 opt_alpha_.levelshift > 0.0) ||
743 gap_alpha < 1e-6) {
744 // "- 1" margin: consecutive_adiis_failures_ is only incremented
745 // LATER in this same Iterate() call (after this check runs), so
746 // without the margin this would still see the PREVIOUS
747 // iteration's count on the exact iteration where the threshold
748 // is reached and direct-minimization actually fires -- letting
749 // level shift through on precisely the iteration it needs to be
750 // blocked, one iteration too late.
752 Levelshift(H.alpha, MOs_alpha.eigenvectors(), opt_alpha_,
754 }
755 }
756 }
757
758 if (nocclevels_beta_ > 0 &&
759 nocclevels_beta_ < MOs_beta.eigenvalues().size()) {
760 double gap_beta = MOs_beta.eigenvalues()(nocclevels_beta_) -
761 MOs_beta.eigenvalues()(nocclevels_beta_ - 1);
762 if ((diiserror_ > opt_beta_.levelshiftend && opt_beta_.levelshift > 0.0) ||
763 gap_beta < 1e-6) {
765 Levelshift(H.beta, MOs_beta.eigenvectors(), opt_beta_,
767 }
768 }
769 }
770
771 Eigen::MatrixXd err_alpha = BuildErrorMatrix(dmat.alpha, H.alpha);
772 Eigen::MatrixXd err_beta = BuildErrorMatrix(dmat.beta, H.beta);
773
774 diiserror_ = CombinedError(err_alpha, err_beta);
775
776 // Trailing-average trigger bookkeeping (see this class's own header
777 // comment on diiserror_history_ for the full ORCA-derived reasoning)
778 // -- tracked unconditionally, every iteration, regardless of what
779 // this iteration goes on to do (ADIIS/DIIS/mixing/direct-
780 // minimization), since the whole point is to observe the genuine,
781 // realized trajectory of diiserror_ itself.
786 }
787
788 mathist_alpha_.push_back(H.alpha);
789 mathist_beta_.push_back(H.beta);
790 dmatHist_alpha_.push_back(dmat.alpha);
791 dmatHist_beta_.push_back(dmat.beta);
792
793 errhist_.push_back(diiserror_);
794
795 // crucial: one shared error matrix = alpha + beta contribution
796 diis_.Update(drop, err_alpha, err_beta);
797
798 bool diis_error = false;
800 << TimeStamp() << " DIIs error " << diiserror_ << std::flush;
802 << TimeStamp() << " Delta Etot " << getDeltaE() << std::flush;
803
804 Eigen::MatrixXd H_guess_alpha = H.alpha;
805 Eigen::MatrixXd H_guess_beta = H.beta;
806
807 // As in ConvergenceAcc::Iterate: below DIIS_start (e.g. after a warm
808 // start) no damping, a plain first step and DIIS from two entries on.
809 const bool near_convergence = diiserror_ < opt_alpha_.diis_start;
810 const std::size_t min_history = near_convergence ? 1 : 2;
811 if ((diiserror_ < opt_alpha_.adiis_start ||
812 diiserror_ < opt_alpha_.diis_start) &&
813 opt_alpha_.usediis && mathist_alpha_.size() > min_history) {
814
815 Eigen::VectorXd coeffs;
816
817 if (diiserror_ > opt_alpha_.diis_start ||
818 totE_.back() - totE_[totE_.size() - 2] > kEnergyRiseForADIIS) {
821 diis_error = !adiis_.Info() || coeffs.size() == 0;
823 << TimeStamp() << " Using ADIIS for next UKS guess" << std::flush;
824 } else {
825 coeffs = diis_.CalcCoeff();
826 diis_error = !diis_.Info() || coeffs.size() == 0;
828 << TimeStamp() << " Using DIIS for next UKS guess" << std::flush;
829 }
830
831 if (diis_error) {
833 // Trailing-average check (see this class's own header comment on
834 // diiserror_history_): true once enough iterations have
835 // happened AND diiserror_ has, on average, failed to shrink by
836 // more than kMeanRatioTolerance's own margin over the trailing
837 // window -- an ADDITIONAL, independent way to detect "genuinely
838 // stalled, not just occasionally failing," alongside (not
839 // instead of) the consecutive-failures count. A system that
840 // fails ADIIS's own tail-coefficient check occasionally, while
841 // still making real progress overall, would not trip this;
842 // ORCA's own AutoTRAH design (confirmed directly from a real
843 // ORCA log's own resolved SCF settings) is built the same way,
844 // reacting to the genuine RATE of improvement rather than
845 // isolated pass/fail outcomes alone.
846 bool trailing_average_stalled = false;
849 double mean_ratio = 0.0;
850 Index ratio_count = 0;
851 for (Index i = 1; i < Index(diiserror_history_.size()); ++i) {
852 if (diiserror_history_[i - 1] > 1e-12) {
853 mean_ratio += diiserror_history_[i] / diiserror_history_[i - 1];
854 ++ratio_count;
855 }
856 }
857 if (ratio_count > 0) {
858 mean_ratio /= double(ratio_count);
859 // mean_ratio here is new/old (< 1 means genuine improvement,
860 // matching diiserror_history_'s own index order). ORCA's own
861 // manual is not fully explicit about which direction its own
862 // "mean grad ratio" convention uses -- this specific
863 // 1.0/kMeanRatioTolerance threshold was inferred from the
864 // manual's own single, concrete worked example ("decreased
865 // on average only by a factor 0.9" triggering the warning
866 // with tolerance=1.125): 0.9 > 1/1.125 (~=0.889) is
867 // consistent with THAT example specifically triggering, but
868 // this has not been independently verified against ORCA's
869 // own source code or a second example.
870 trailing_average_stalled = mean_ratio > (1.0 / kMeanRatioTolerance);
871 }
872 }
873 // OR of both criteria, restored after a direct comparison: a
874 // real run confirmed both this OR-based combination and a
875 // trailing-average-only variant converge to the IDENTICAL,
876 // correct energy on the same water dimer geometry
877 // (-152.36718769 Hrt, matching ORCA's own converged value to
878 // ~0.03 mHa) -- but the OR combination needed fewer total SCF
879 // iterations to get there (71, actually fewer than ORCA's own 84
880 // cycles on this system) than the trailing-average-only variant
881 // did (94), since the trailing-average criterion alone triggers
882 // earlier and more often (it is not gated on ADIIS technically
883 // "failing" at all, only on diiserror_ itself failing to
884 // genuinely improve), invoking the expensive coupled machinery
885 // more times than strictly needed. Kept as an OR going forward:
886 // the fast-firing consecutive-count trigger handles the common
887 // case efficiently, with the trailing-average criterion
888 // available as an additional safety net for the specific,
889 // rarer failure mode it is built to catch (genuine, slow stall
890 // without ADIIS ever outright "failing").
892 trailing_average_stalled) &&
894 // Mirrors ORCA's own AutoTRAH trigger, via two independent
895 // conditions -- see this class's own header comment on
896 // DirectMinimizationRotation and diiserror_history_ for the
897 // full reasoning and the ORCA log this was validated against
898 // directly.
900 << TimeStamp() << " (A)DIIS failed " << consecutive_adiis_failures_
901 << " times in a row"
902 << (trailing_average_stalled ? " (or trailing average stalled)"
903 : "")
904 << ", switching to direct-minimization step" << std::flush;
905 // Save the pre-step state so this step's actual effect can be
906 // verified (and, if necessary, reverted) once its own energy
907 // becomes available on the NEXT Iterate() call -- see the
908 // Fletcher accept/reject check at the top of this function.
914
915 double predicted_change_alpha = 0.0;
916 double predicted_change_beta = 0.0;
917 Eigen::MatrixXd C_new_alpha;
918 Eigen::MatrixXd C_new_beta;
919 // Two-tier fallback: CoupledAugmentedHessianStep (captures the
920 // real alpha-beta coupling -- see the conversation this grew
921 // out of: a direct ORCA comparison on an identical geometry
922 // showed ORCA's own, fully-coupled TRAH converging where an
923 // earlier, decoupled AugmentedHessianStep implementation did
924 // not) if coupled_fock_builder_ has been injected (the only
925 // caller that ever does, DFTEngine::EvaluateUKS, always injects
926 // it); else the simplest, diagonal-Hessian-only
927 // DirectMinimizationRotation for any caller that has not (e.g.
928 // DFTEngine::RunAtomicDFT_unrestricted, which injects neither
929 // this nor a per-channel callback at all). Previously a
930 // three-tier fallback with a decoupled AugmentedHessianStep in
931 // between -- removed after confirming directly (Codecov's own
932 // patch-coverage report, and a direct grep across every caller)
933 // that no caller anywhere ever set the per-channel callbacks
934 // without also setting the coupled one, making that middle tier
935 // permanently unreachable dead code, not a genuine fallback.
937 double predicted_change_combined = 0.0;
938 std::tie(C_new_alpha, C_new_beta) = CoupledAugmentedHessianStep(
939 H.alpha, MOs_alpha, nocclevels_alpha_, H.beta, MOs_beta,
941 predicted_change_combined);
942 // Split evenly between the two channels purely so the
943 // existing direct_min_predicted_change_ bookkeeping below
944 // (predicted_change_alpha + predicted_change_beta) keeps
945 // working unchanged -- the coupled step itself only ever
946 // produces ONE, already-combined value; this split has no
947 // physical meaning of its own, it is bookkeeping
948 // convenience only.
949 predicted_change_alpha = 0.5 * predicted_change_combined;
950 predicted_change_beta = 0.5 * predicted_change_combined;
951 } else {
952 C_new_alpha = DirectMinimizationRotation(
953 H.alpha, MOs_alpha, nocclevels_alpha_, predicted_change_alpha);
954 C_new_beta = DirectMinimizationRotation(
955 H.beta, MOs_beta, nocclevels_beta_, predicted_change_beta);
956 }
958 predicted_change_alpha + predicted_change_beta;
959 direct_min_pending_ = true;
960
961 MOs_alpha.eigenvectors() = C_new_alpha;
962 MOs_beta.eigenvectors() = C_new_beta;
963 // Approximate orbital energies from the (only approximately
964 // diagonal, post-rotation) MO-basis Fock matrix -- consistent
965 // with the rotated orbitals themselves, and cheap to obtain;
966 // used only for level-shift gap checks and diagnostics until
967 // the next full diagonalization naturally supersedes them.
968 MOs_alpha.eigenvalues() =
969 (C_new_alpha.transpose() * H.alpha * C_new_alpha).diagonal();
970 MOs_beta.eigenvalues() =
971 (C_new_beta.transpose() * H.beta * C_new_beta).diagonal();
972
973 SpinDensity dmatout_direct = DensityMatrix(MOs_alpha, MOs_beta);
974 usedmixing_ = false;
975 return dmatout_direct;
976 }
978 << TimeStamp() << " (A)DIIS failed using mixing instead"
979 << std::flush;
980 H_guess_alpha = H.alpha;
981 H_guess_beta = H.beta;
982 } else {
984 H_guess_alpha.setZero();
985 H_guess_beta.setZero();
986 for (Index i = 0; i < coeffs.size(); ++i) {
987 if (std::abs(coeffs(i)) < 1e-8) {
988 continue;
989 }
990 H_guess_alpha += coeffs(i) * mathist_alpha_[i];
991 H_guess_beta += coeffs(i) * mathist_beta_[i];
992 }
993 }
994 }
995
996 MOs_alpha = SolveFockmatrix(H_guess_alpha);
997 MOs_beta = SolveFockmatrix(H_guess_beta);
998
999 SpinDensity dmatout = DensityMatrix(MOs_alpha, MOs_beta);
1000
1001 if (diiserror_ > opt_alpha_.mixingend || !opt_alpha_.usediis || diis_error ||
1002 (mathist_alpha_.size() <= 2 && !near_convergence)) {
1003 // mixingend, not adiis_start -- deliberately decoupled (see the
1004 // options struct's own comment in convergenceacc.h): ORCA keeps
1005 // DampErr fully independent of DIISStart, and recommends making
1006 // DampErr much SMALLER for difficult systems specifically so
1007 // damping stays active well past the point where DIIS itself
1008 // starts being tried -- reusing adiis_start here could never
1009 // represent that independently, since it would tie "when does
1010 // mixing turn off" to the same value as "when does ADIIS/DIIS
1011 // engage at all", which are conceptually separate questions.
1012 usedmixing_ = true;
1013 // Adaptive damping (matches ORCA's own DampFac/DampMax design,
1014 // confirmed directly from a real ORCA log's own resolved SCF
1015 // settings -- see the options struct's own comment in
1016 // convergenceacc.h for the full reasoning): ramp LINEARLY from
1017 // mixingparameter (the base, e.g. 0.7) toward mixingmax (the
1018 // ceiling, e.g. 0.98) as consecutive_adiis_failures_ increases
1019 // toward kMaxConsecutiveADIISFailures, rather than applying the
1020 // ceiling value for the entire run regardless of whether the SCF
1021 // is actually struggling. Ties the ramp to the SAME signal already
1022 // driving the direct-minimization trigger itself, rather than
1023 // introducing a separate struggle metric -- consecutive_adiis_
1024 // failures_ resets to 0 on any successful ADIIS/DIIS step, so the
1025 // ramp relaxes back toward the base value just as readily as it
1026 // climbed.
1027 double ramp_fraction =
1028 std::min(1.0, double(consecutive_adiis_failures_) /
1030 double mixingparameter_alpha_current =
1031 opt_alpha_.mixingparameter +
1032 ramp_fraction * (opt_alpha_.mixingmax - opt_alpha_.mixingparameter);
1033 double mixingparameter_beta_current =
1034 opt_beta_.mixingparameter +
1035 ramp_fraction * (opt_beta_.mixingmax - opt_beta_.mixingparameter);
1036 dmatout.alpha = mixingparameter_alpha_current * dmat.alpha +
1037 (1.0 - mixingparameter_alpha_current) * dmatout.alpha;
1038 dmatout.beta = mixingparameter_beta_current * dmat.beta +
1039 (1.0 - mixingparameter_beta_current) * dmatout.beta;
1041 << TimeStamp() << " Using coupled UKS mixing with adaptive alpha="
1042 << mixingparameter_alpha_current
1043 << " (base=" << opt_alpha_.mixingparameter
1044 << ", ceiling=" << opt_alpha_.mixingmax
1045 << ", ramp fraction=" << ramp_fraction << ")" << std::flush;
1046 } else {
1047 usedmixing_ = false;
1048 }
1049
1050 return dmatout;
1051}
1052
1054 if (totE_.size() < 2) {
1055 return 100.0;
1056 }
1057 return std::abs(totE_.back() - totE_[totE_.size() - 2]);
1058}
1059
1061 return (getDeltaE() < opt_alpha_.Econverged &&
1062 diiserror_ < opt_alpha_.error_converged);
1063}
1064
1065} // namespace xtp
1066} // namespace votca
const Eigen::VectorXd & eigenvalues() const
Definition eigensystem.h:30
const Eigen::MatrixXd & eigenvectors() const
Definition eigensystem.h:33
Use Davidson algorithm to solve A*V=E*V.
void set_max_search_space(Index N)
Eigen::VectorXd eigenvalues() const
void solve(const MatrixReplacement &A, Index neigen, Index size_initial_guess=0)
void set_matrix_type(std::string mt)
Eigen::ComputationInfo info() const
void set_tolerance(std::string tol)
Eigen::MatrixXd eigenvectors() const
Logger is used for thread-safe output of messages.
Definition logger.h:164
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
double CombinedError(const Eigen::MatrixXd &err_alpha, const Eigen::MatrixXd &err_beta) const
tools::EigenSystem SolveFockmatrix(const Eigen::MatrixXd &H) const
std::pair< Eigen::MatrixXd, Eigen::MatrixXd > UnflattenCoupledRotation(const Eigen::VectorXd &v, Index nao_alpha, Index nocclevels_alpha, Index nao_beta, Index nocclevels_beta) const
static constexpr Index kAutoStartIteration
SpinDensity DensityMatrix(const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
void Levelshift(Eigen::MatrixXd &H, const Eigen::MatrixXd &MOs_old, const options &opt, Index nocclevels) const
std::vector< Eigen::MatrixXd > mathist_beta_
static constexpr double kMinTrustRadius
static constexpr Index kMaxConsecutiveADIISFailures
void setOverlap(AOOverlap &S, double etol)
void Configure(const options &opt_alpha, const options &opt_beta)
Eigen::VectorXd BuildCoupledSigmaVector(const Eigen::VectorXd &v, const Eigen::MatrixXd &C_alpha, Index nocclevels_alpha, const Eigen::MatrixXd &C_beta, Index nocclevels_beta, const CoupledFockBuilder &coupled_fock_builder, double finite_diff_step=1e-3) const
static constexpr double kEnergyRiseForADIIS
Eigen::MatrixXd BuildErrorMatrix(const Eigen::MatrixXd &dmat, const Eigen::MatrixXd &H) const
ConvergenceAcc::options options
SpinDensity Iterate(const SpinDensity &dmat, SpinFock &H, tools::EigenSystem &MOs_alpha, tools::EigenSystem &MOs_beta, double totE)
std::vector< Eigen::MatrixXd > dmatHist_alpha_
Eigen::MatrixXd DirectMinimizationRotation(const Eigen::MatrixXd &H_AO, const tools::EigenSystem &MOs, Index nocclevels, double &predicted_energy_change) const
Eigen::MatrixXd UnflattenRotation(const Eigen::VectorXd &v_ov, Index nao, Index nocclevels) const
Eigen::VectorXd direct_min_pre_MOs_beta_energies_
std::vector< double > diiserror_history_
std::vector< Eigen::MatrixXd > dmatHist_beta_
CoupledFockBuilder coupled_fock_builder_
Eigen::VectorXd direct_min_pre_MOs_alpha_energies_
Eigen::MatrixXd DensityMatrixGroundState_unres(const Eigen::MatrixXd &MOs, Index nocclevels) const
static constexpr Index kTrailingWindowSize
static constexpr double kMeanRatioTolerance
std::function< SpinFock(const Eigen::MatrixXd &, const Eigen::MatrixXd &)> CoupledFockBuilder
std::vector< Eigen::MatrixXd > mathist_alpha_
static constexpr double kRemovedShift
std::pair< Eigen::MatrixXd, Eigen::MatrixXd > CoupledAugmentedHessianStep(const Eigen::MatrixXd &H_AO_alpha, const tools::EigenSystem &MOs_alpha, Index nocclevels_alpha, const Eigen::MatrixXd &H_AO_beta, const tools::EigenSystem &MOs_beta, Index nocclevels_beta, const CoupledFockBuilder &coupled_fock_builder, double trust_radius, double &predicted_energy_change) const
#define XTP_LOG(level, log)
Definition logger.h:40
Charge transport classes.
Definition ERIs.h:28
SymmetricEigenSystem SymmetricEigen(const Eigen::MatrixXd &A, bool vectors=true)
With vectors = false only the eigenvalues are computed.
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26