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