votca 2026-dev
Loading...
Searching...
No Matches
dftengine.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// Third party includes
21#include <algorithm>
22#include <boost/filesystem.hpp>
23#include <boost/format.hpp>
24#include <iomanip>
25#include <iostream>
26#include <map>
27#include <optional>
28#include <sstream>
29#include <string>
30
31// VOTCA includes
34
35// Local VOTCA includes
39#include "votca/xtp/aomatrix.h"
42#include "votca/xtp/dftengine.h"
46#include "votca/xtp/logger.h"
47#include "votca/xtp/mmregion.h"
48#include "votca/xtp/orbitals.h"
51
52namespace votca {
53namespace xtp {
54
55namespace {
56
57void CanonicalizeOrbitalPhases(Eigen::MatrixXd& coeffs) {
58 constexpr double tol = 1e-14;
59
60 for (Index col = 0; col < coeffs.cols(); ++col) {
61 Eigen::Index pivot = 0;
62 const double maxabs = coeffs.col(col).cwiseAbs().maxCoeff(&pivot);
63
64 if (maxabs <= tol) {
65 continue;
66 }
67
68 if (coeffs(pivot, col) < 0.0) {
69 coeffs.col(col) *= -1.0;
70 }
71 }
72}
73
74void CanonicalizeOrbitalPhases(tools::EigenSystem& mos) {
75 CanonicalizeOrbitalPhases(mos.eigenvectors());
76}
77
78} // namespace
79
80// Defined in libint2_derivative_calls.cc -- forward declared here
81// (rather than only near ComputeAndStoreForces further down, where the
82// other libint2_derivative_calls.cc forward declarations live) because
83// Initialize() below needs it too, to warn early -- at options-parsing
84// time, before any SCF work at all -- if compute_forces=true was
85// requested on a build that cannot actually do it. See that file's own
86// compile-time guard (around LIBINT2_MAX_DERIV_ORDER) for the full
87// explanation of why this check exists.
89
102
104
105 const std::string key_xtpdft = "xtpdft";
106 dftbasis_name_ = options.get(".basisset").as<std::string>();
107
108 if (options.exists(".auxbasisset")) {
109 auxbasis_name_ = options.get(".auxbasisset").as<std::string>();
110 }
111
112 if (!auxbasis_name_.empty()) {
113 screening_eps_ = options.get(key_xtpdft + ".screening_eps").as<double>();
115 options.get(key_xtpdft + ".fock_matrix_reset").as<Index>();
116 }
117 if (options.exists(".ecp")) {
118 ecp_name_ = options.get(".ecp").as<std::string>();
119 }
120
121 if (options.exists(key_xtpdft + ".force_uks_path")) {
122 force_uks_path_ = options.get(key_xtpdft + ".force_uks_path").as<bool>();
123 }
124
125 if (options.exists(key_xtpdft + ".compute_forces")) {
126 compute_forces_ = options.get(key_xtpdft + ".compute_forces").as<bool>();
127 }
129 // Fail fast, at options-parsing time, rather than only discovering
130 // this after a full (potentially expensive) SCF has already
131 // converged. Genuinely throws now (previously only printed a
132 // std::cerr WARNING and let Initialize() return normally, so the
133 // full SCF still ran to completion regardless, wasting real
134 // compute on a calculation that could never produce forces) --
135 // throwing std::runtime_error here for an invalid/impossible
136 // options combination matches this file's own, already-established
137 // convention (see e.g. "Spin multiplicity must be >= 1." and
138 // several other throw std::runtime_error(...) calls elsewhere in
139 // this same function), not a new pattern.
140 throw std::runtime_error(
141 "compute_forces=true was requested, but the libint2 this was "
142 "built against does not support derivative integrals for one "
143 "or more operator categories it needs (one-body, the two-center "
144 "Coulomb metric, or three-center RI -- see "
145 "libint2_derivative_calls.cc's own compile guards for exactly "
146 "which). Many pre-packaged libint2 builds (Homebrew, Ubuntu "
147 "apt, etc.) do not enable derivative-integral support for all "
148 "of these by default; rebuild libint2 with "
149 "--enable-1body/--enable-eri2/--enable-eri3 to use this "
150 "feature.");
151 }
152 if (compute_forces_ && !ecp_name_.empty()) {
153 // A real, previously-unguarded gap: the SCF's own Hamiltonian
154 // genuinely includes the ECP contribution (H0 = T + V_nuc + V_ECP
155 // + V_ext, see the comment on that further down in this file, and
156 // dftAOECP.FillPotential(dftbasis_, ecp_) actually called during
157 // the SCF itself) -- but ComputeAndStoreForces/ComputeAndStoreForcesUKS
158 // have no d(V_ECP)/dR term at all (confirmed directly: neither
159 // function references ecp_ or ecp_name_ anywhere). Computing
160 // forces anyway in this case would not fail cleanly the way the
161 // libint2-support case above does -- it would silently produce a
162 // physically INCOMPLETE result (missing the ECP contribution to
163 // the force entirely) that looks like a normal, valid force
164 // output, which is worse than refusing outright. Refuse instead.
165 throw std::runtime_error(
166 "compute_forces=true was requested together with an ECP ('" +
167 ecp_name_ +
168 "'), but analytic nuclear forces do not yet include the ECP "
169 "contribution to the force (d(V_ECP)/dR) -- computing forces "
170 "in this configuration would silently omit that term rather "
171 "than fail visibly. Either drop the ECP or do not request "
172 "compute_forces until this is implemented.");
173 }
174
175 if (options.exists(key_xtpdft + ".cdft.enabled")) {
176 cdft_enabled_ = options.get(key_xtpdft + ".cdft.enabled").as<bool>();
177 }
178 if (cdft_enabled_) {
179 // Deliberately parsed into a CDFTConstraintSpec (atom indices +
180 // charge, both directly from the options tree) here, at
181 // Initialize() time, rather than building the actual
182 // HirshfeldPartition::Constraint (which needs the reference
183 // densities and weight matrix) right away -- those need the
184 // molecule and basis, neither of which exist yet at this point;
185 // BuildCDFTConstraint (elsewhere in this file) does that
186 // conversion later, once Evaluate() actually has an Orbitals
187 // object with real QMAtoms to work with.
188 std::string indices_str =
189 options.get(key_xtpdft + ".cdft.indices").as<std::string>();
190 if (indices_str.empty()) {
191 throw std::runtime_error(
192 "cdft.enabled=true was requested, but cdft.indices is empty -- "
193 "specify which atoms (0-based, e.g. '1 3 13:17', same syntax "
194 "already used for diabatization.xml's own fragment indices) "
195 "make up the constrained fragment.");
196 }
197 cdft_constraint_spec_.atom_indices =
198 IndexParser().CreateIndexVector(indices_str);
199 cdft_constraint_spec_.target_charge =
200 options.get(key_xtpdft + ".cdft.charge").as<double>();
201 cdft_constraint_spec_.initial_lambda =
202 options.get(key_xtpdft + ".cdft.initial_lambda").as<double>();
204 options.get(key_xtpdft + ".cdft.max_iterations").as<Index>();
206 options.get(key_xtpdft + ".cdft.population_tolerance").as<double>();
207 cdft_constraint_spec_.guess_strategy =
208 options.get(key_xtpdft + ".cdft.guess_strategy").as<std::string>();
209 // Note: CDFT itself needs no derivative integral at all (it only
210 // ever builds ENERGY-level quantities -- reference densities,
211 // weight matrices, Fock-matrix potentials -- never the
212 // deriv_order=1 machinery compute_forces needs), so there is no
213 // separate HasLibint2DerivativeSupport() check needed here; if
214 // compute_forces were ALSO left enabled alongside cdft.enabled,
215 // the earlier compute_forces-specific check above already covers
216 // that combination.
217 }
218
219 initial_guess_ = options.get(".initial_guess").as<std::string>();
220
221 if (initial_guess_ == "dimer_guess") {
222 dimer_guess_orbA_name_ = options.get(".dimer_guess_orbA").as<std::string>();
223 dimer_guess_orbB_name_ = options.get(".dimer_guess_orbB").as<std::string>();
224 if (dimer_guess_orbA_name_.empty() || dimer_guess_orbB_name_.empty()) {
225 throw std::runtime_error(
226 "initial_guess=dimer_guess requires both dimer_guess_orbA and "
227 "dimer_guess_orbB to be set to real monomer .orb file paths.");
228 }
229 }
230
231 grid_name_ = options.get(key_xtpdft + ".integration_grid").as<std::string>();
232 xc_functional_name_ = options.get(".functional").as<std::string>();
233
234 if (options.exists(key_xtpdft + ".externaldensity")) {
237 options.get(key_xtpdft + ".externaldensity.orbfile").as<std::string>();
238 gridquality_ = options.get(key_xtpdft + ".externaldensity.gridquality")
239 .as<std::string>();
240 state_ =
241 options.get(key_xtpdft + ".externaldensity.state").as<std::string>();
242 }
243
244 if (options.exists(".externalfield")) {
246 extfield_ = options.get(".externalfield").as<Eigen::Vector3d>();
247 }
248
249 conv_opt_.Econverged =
250 options.get(key_xtpdft + ".convergence.energy").as<double>();
251 conv_opt_.error_converged =
252 options.get(key_xtpdft + ".convergence.error").as<double>();
253 max_iter_ =
254 options.get(key_xtpdft + ".convergence.max_iterations").as<Index>();
255
256 std::string method =
257 options.get(key_xtpdft + ".convergence.method").as<std::string>();
258 if (method == "DIIS") {
259 conv_opt_.usediis = true;
260 } else if (method == "mixing") {
261 conv_opt_.usediis = false;
262 }
263 if (!conv_opt_.usediis) {
264 conv_opt_.histlength = 1;
265 conv_opt_.maxout = false;
266 }
267 conv_opt_.mixingparameter =
268 options.get(key_xtpdft + ".convergence.mixing").as<double>();
269 // Ceiling for adaptive damping -- see the options struct's own
270 // comment (convergenceacc.h) for the full ORCA-derived reasoning.
271 conv_opt_.mixingmax =
272 options.get(key_xtpdft + ".convergence.mixing_max").as<double>();
273 conv_opt_.levelshift =
274 options.get(key_xtpdft + ".convergence.levelshift").as<double>();
275 conv_opt_.levelshiftend =
276 options.get(key_xtpdft + ".convergence.levelshift_end").as<double>();
277 // Independent from adiis_start -- see the options struct's own
278 // comment (convergenceacc.h) and UKSConvergenceAcc::Iterate's own
279 // mixing-trigger comment for the full reasoning.
280 conv_opt_.mixingend =
281 options.get(key_xtpdft + ".convergence.mixing_end").as<double>();
282 conv_opt_.maxout =
283 options.get(key_xtpdft + ".convergence.DIIS_maxout").as<bool>();
284 conv_opt_.histlength =
285 options.get(key_xtpdft + ".convergence.DIIS_length").as<Index>();
286 conv_opt_.diis_start =
287 options.get(key_xtpdft + ".convergence.DIIS_start").as<double>();
288 conv_opt_.adiis_start =
289 options.get(key_xtpdft + ".convergence.ADIIS_start").as<double>();
290 conv_opt_.davidson_max_iter =
291 options.get(key_xtpdft + ".convergence.davidson_max_iter").as<Index>();
292 conv_opt_.energy_reset = options.ifExistsReturnElseReturnDefault<double>(
293 key_xtpdft + ".convergence.energy_reset", 1.0);
295 key_xtpdft + ".overlap_tolerance", 1e-8);
297 key_xtpdft + ".ri_pair_threshold", 1e-10);
298
299 if (options.exists(key_xtpdft + ".dft_in_dft.activeatoms")) {
301 options.get(key_xtpdft + ".dft_in_dft.activeatoms").as<std::string>();
303 options.get(key_xtpdft + ".dft_in_dft.threshold").as<double>();
305 options.get(key_xtpdft + ".dft_in_dft.levelshift").as<double>();
306 truncate_ =
307 options.get(key_xtpdft + ".dft_in_dft.truncate_basis").as<bool>();
308 if (truncate_) {
310 options.get(key_xtpdft + ".dft_in_dft.truncation_threshold")
311 .as<double>();
312 }
313 }
314}
315
316void DFTEngine::PrintMOs(const Eigen::VectorXd& MOEnergies, Log::Level level) {
317 XTP_LOG(level, *pLog_) << " Orbital energies: " << std::flush;
318 XTP_LOG(level, *pLog_) << " index occupation energy(Hartree) " << std::flush;
319
320 for (Index i = 0; i < MOEnergies.size(); ++i) {
321 Index occupancy = 0;
322 if (i < num_docc_) {
323 occupancy = 2;
324 } else if (i < num_docc_ + num_socc_alpha_) {
325 occupancy = 1;
326 }
327
328 XTP_LOG(level, *pLog_) << (boost::format(" %1$5d %2$1d %3$+1.10f") %
329 i % occupancy % MOEnergies(i))
330 .str()
331 << std::flush;
332 }
333 return;
334}
335
336void DFTEngine::PrintMOsUKS(const Eigen::VectorXd& alpha_energies,
337 const Eigen::VectorXd& beta_energies,
338 Log::Level level) const {
339 XTP_LOG(level, *pLog_) << " UKS orbital energies:" << std::flush;
340 XTP_LOG(level, *pLog_) << " index occ eps_a(Ha) eps_b(Ha)"
341 << std::flush;
342
343 const Index nrows =
344 std::max<Index>(alpha_energies.size(), beta_energies.size());
345
346 for (Index i = 0; i < nrows; ++i) {
347 const bool occ_a = (i < num_alpha_electrons_);
348 const bool occ_b = (i < num_beta_electrons_);
349
350 std::string occ = "0";
351 if (occ_a && occ_b) {
352 occ = "2";
353 } else if (occ_a) {
354 occ = "a";
355 } else if (occ_b) {
356 occ = "b";
357 }
358
359 std::string eps_a = " -";
360 std::string eps_b = " -";
361
362 if (i < alpha_energies.size()) {
363 eps_a = (boost::format("%+1.10f") % alpha_energies(i)).str();
364 }
365 if (i < beta_energies.size()) {
366 eps_b = (boost::format("%+1.10f") % beta_energies(i)).str();
367 }
368
369 XTP_LOG(level, *pLog_) << (boost::format(
370 " %1$5d %2$1s %3$15s %4$15s") %
371 i % occ % eps_a % eps_b)
372 .str()
373 << std::flush;
374 }
375
376 if (num_alpha_electrons_ > 0 &&
377 num_alpha_electrons_ < alpha_energies.size()) {
378 XTP_LOG(level, *pLog_) << (boost::format(
379 " alpha HOMO-LUMO gap: %+1.10f Ha") %
380 (alpha_energies(num_alpha_electrons_) -
381 alpha_energies(num_alpha_electrons_ - 1)))
382 .str()
383 << std::flush;
384 }
385
386 if (num_beta_electrons_ > 0 && num_beta_electrons_ < beta_energies.size()) {
387 XTP_LOG(level, *pLog_) << (boost::format(
388 " beta HOMO-LUMO gap: %+1.10f Ha") %
389 (beta_energies(num_beta_electrons_) -
390 beta_energies(num_beta_electrons_ - 1)))
391 .str()
392 << std::flush;
393 }
394}
395
396void DFTEngine::CalcElDipole(const Orbitals& orb) const {
397 QMState state = QMState("n");
398 Eigen::Vector3d result = orb.CalcElDipole(state);
400 << TimeStamp() << " Electric Dipole is[e*bohr]:\n\t\t dx=" << result[0]
401 << "\n\t\t dy=" << result[1] << "\n\t\t dz=" << result[2] << std::flush;
402 return;
403}
404
405// Assembles the total ground-state gradient (nuclear repulsion + RI-J
406// Coulomb + XC, LDA or GGA) from the converged density matrix, negates it
407// to the physical force convention, and stores it via Orbitals::setForces().
408// See the detailed SCOPE note on the declaration in dftengine.h for exactly
409// which cases this does and does not support, and why.
410//
411// Every individual term here (NuclearRepulsionDerivative, RIJGradient,
412// PulayGradient, GridWeightGradient) was separately derived and validated
413// via finite-difference tests earlier in this branch (see
414// test_dftgradient.cc and test_xcgradient.cc) -- this function's own new
415// content is just the SUMMATION and the sign convention, not any new
416// derivative math.
417// Defined in libint2_derivative_calls.cc, not yet in any header (same
418// STATUS noted throughout that file) -- forward declared here. Unlike
419// DFTGradient::RIJGradient/PulayGradient/etc., these return RAW AO-matrix
420// derivatives (d(matrix_munu)/dR), not already-contracted energy
421// gradients -- the contraction with Dmat is done explicitly below.
422using AOMatrixDerivative = std::array<Eigen::MatrixXd, 3>;
423std::vector<AOMatrixDerivative> ComputeOverlapDerivatives(
424 const AOBasis& aobasis);
425std::vector<AOMatrixDerivative> ComputeKineticDerivatives(
426 const AOBasis& aobasis);
427std::vector<AOMatrixDerivative> ComputeNuclearAttractionDerivatives(
428 const AOBasis& aobasis, const QMMolecule& mol);
429// HasLibint2DerivativeSupport() (used below in both ComputeAndStoreForces
430// and ComputeAndStoreForcesUKS) is already forward declared earlier in
431// this file, near Initialize() -- see that declaration's own comment
432// for why it needed to be that early.
433
435 Orbitals& orb, const Eigen::MatrixXd& Dmat,
436 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
437 if (auxbasis_name_.empty()) {
439 << TimeStamp()
440 << " Skipping force calculation: RI-J gradient (DFTGradient::"
441 "RIJGradient) only implements the RI path, but this SCF ran "
442 "without an auxiliary basis (conventional 4-center ERIs)."
443 << std::flush;
444 return;
445 }
446
449 << TimeStamp()
450 << " Skipping force calculation: the libint2 this was built "
451 "against does not support derivative integrals for one or "
452 "more operator categories it needs. Many pre-packaged "
453 "libint2 builds (Homebrew, Ubuntu apt, etc.) do not enable "
454 "this by default -- rebuild libint2 with "
455 "--enable-1body/--enable-eri2/--enable-eri3 to use analytic "
456 "forces."
457 << std::flush;
458 return;
459 }
460
461 if (!ecp_name_.empty()) {
462 // Same reasoning as Initialize()'s own, earlier check (which
463 // should already have caught this before any SCF work even
464 // started) -- this is a defense-in-depth repeat, matching the
465 // existing HasLibint2DerivativeSupport() re-check just above,
466 // in case compute_forces_/ecp_name_ were ever set some other way
467 // than through Initialize()'s own options parsing. Skips cleanly
468 // (log + return) rather than throwing here, matching this
469 // function's own existing style for the libint2-support case
470 // above -- by the time SCF has already converged this far,
471 // throwing would be a less graceful failure than simply not
472 // storing forces, though Initialize()'s own check is the
473 // preferred, much earlier place for this to actually be caught.
475 << TimeStamp() << " Skipping force calculation: an ECP ('" << ecp_name_
476 << "') was used for this SCF, but analytic nuclear forces do "
477 "not yet include the ECP contribution to the force "
478 "(d(V_ECP)/dR) -- computing forces in this configuration "
479 "would silently omit that term rather than fail visibly."
480 << std::flush;
481 return;
482 }
483
484 const QMMolecule& mol = orb.QMAtoms();
485 Index natoms = mol.size();
486
487 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Starting force calculation ("
488 << natoms << " atoms)" << std::flush;
489
490 // One-electron (kinetic + nuclear attraction) contribution --
491 // dEone/dR_A = Tr[Dmat . d(T+V_ne)/dR_A]. This was the piece
492 // discovered MISSING from the total gradient by the first genuine
493 // end-to-end SCF+forces test (test_dftengine_forces.cc): kinetic
494 // derivatives were validated at the very start of this whole branch
495 // and then never actually wired into any gradient assembly, and
496 // nuclear attraction derivatives were never implemented at all until
497 // that gap was found. See ComputeNuclearAttractionDerivatives in
498 // libint2_derivative_calls.cc for the detailed derivation (sign
499 // convention checked directly against AOMultipole's own,
500 // already-validated energy-level code, not assumed).
502 << " Computing one-electron (kinetic + nuclear "
503 "attraction) derivatives"
504 << std::flush;
505 std::vector<AOMatrixDerivative> dT = ComputeKineticDerivatives(dftbasis_);
506 std::vector<AOMatrixDerivative> dVne =
508 Eigen::MatrixXd eone_grad = Eigen::MatrixXd::Zero(natoms, 3);
509 for (Index a = 0; a < natoms; ++a) {
510 for (Index xyz = 0; xyz < 3; ++xyz) {
511 eone_grad(a, xyz) = Dmat.cwiseProduct(dT[a][xyz] + dVne[a][xyz]).sum();
512 }
513 }
515 << TimeStamp() << " One-electron derivatives done" << std::flush;
516
517 // Overlap "Pulay force" -- a SECOND, genuinely distinct missing term,
518 // found after the kinetic+nuclear-attraction fix improved but did not
519 // fully resolve the discrepancy against the end-to-end finite-difference
520 // test (magnitude dropped ~7x in the right direction, but still wrong
521 // by roughly the size of a real missing term, not noise).
522 //
523 // Distinct from the earlier "PulayGradient" naming (which is about
524 // basis functions inside the XC integral) -- this is the CLASSICAL
525 // SCF Pulay/overlap force, present in essentially any Gaussian-basis
526 // HF/DFT gradient: the MO coefficients C are only implicitly
527 // R-independent because they satisfy the orthonormality constraint
528 // C^T S C = I, and S itself depends on R (basis functions move). At
529 // the SCF stationary point, the Lagrange multipliers for this
530 // constraint are exactly the orbital energies (canonical MOs), giving
531 // an extra term dE/dR_A|_overlap = -Tr[W . dS/dR_A], where
532 // W = 2 * C_occ * diag(eps_occ) * C_occ^T (the "energy-weighted
533 // density matrix", factor of 2 matching the same doubled convention
534 // Dmat already uses for closed-shell restricted). Confirmed as a
535 // standard, expected term by libint2's own reference SCF-gradient
536 // example (compute_1body_ints_deriv<Operator::overlap> combined with
537 // exactly this W construction, in
538 // libint2/include/libint2/lcao/1body.h) -- not a novel derivation.
539 //
540 // ComputeOverlapDerivatives itself was validated (finite-difference
541 // tested) at the very start of this whole branch and then never
542 // actually used in any gradient assembly until now, same as kinetic.
544 Eigen::MatrixXd C_occ = orb.MOs().eigenvectors().leftCols(n_occ);
545 Eigen::VectorXd eps_occ = orb.MOs().eigenvalues().head(n_occ);
546 Eigen::MatrixXd W = 2.0 * C_occ * eps_occ.asDiagonal() * C_occ.transpose();
547
549 << TimeStamp() << " Computing overlap (Pulay) derivatives"
550 << std::flush;
551 std::vector<AOMatrixDerivative> dS = ComputeOverlapDerivatives(dftbasis_);
552 Eigen::MatrixXd overlap_pulay_grad = Eigen::MatrixXd::Zero(natoms, 3);
553 for (Index a = 0; a < natoms; ++a) {
554 for (Index xyz = 0; xyz < 3; ++xyz) {
555 overlap_pulay_grad(a, xyz) = -W.cwiseProduct(dS[a][xyz]).sum();
556 }
557 }
559 << TimeStamp() << " Overlap derivatives done" << std::flush;
560
562 << TimeStamp() << " Computing RI-J (Coulomb) gradient" << std::flush;
563 Eigen::MatrixXd rij_term =
566 << TimeStamp() << " RI-J gradient done" << std::flush;
567
569 << TimeStamp() << " Computing XC grid (Pulay + weight) gradient terms"
570 << std::flush;
571 Eigen::MatrixXd pulay_term = vxcpotential.PulayGradient(Dmat, dftbasis_);
572 Eigen::MatrixXd weight_term = vxcpotential.GridWeightGradient(Dmat, mol);
573 Eigen::MatrixXd nucrep_term = DFTGradient::NuclearRepulsionDerivative(mol);
575 << TimeStamp() << " XC grid gradient terms done" << std::flush;
576
577 Eigen::MatrixXd grad = nucrep_term + eone_grad + overlap_pulay_grad +
578 rij_term + pulay_term + weight_term;
579
580 // Exact-exchange (RI-K) gradient -- hybrid functionals only. Skipped
581 // entirely (not just multiplied by a zero ScaHFX_) when not needed,
582 // since RIKGradient is genuinely expensive (O(nocc^2 * naux) linear
583 // solves) unlike the GGA sigma terms, which are cheap enough to
584 // compute unconditionally.
585 //
586 // RIKGradient's own energy convention (E_K = -sum_ij c_ij.d_ij) was
587 // confirmed, via direct numerical simulation of
588 // ERIs::CalculateEXX_mos's real algorithm and then a real C++
589 // finite-difference test against that same production function
590 // (test_dftgradient.cc), to equal EXACTLY 0.25*Dmat.cwiseProduct(K).sum()
591 // at ScaHFX_=1 -- so for general ScaHFX_, the contribution is
592 // ScaHFX_ * RIKGradient(...), a direct scaling, matching exactly how
593 // the real SCF energy scales its own exx term
594 // (exx = 0.25*ScaHFX_*Dmat.cwiseProduct(K).sum()).
595 //
596 // This removes what was previously an explicit, logged SCOPE
597 // limitation (hybrid functionals skipped entirely) -- see git history
598 // for the full derivation/verification that led to this.
599 if (ScaHFX_ > 0.0) {
601 << TimeStamp() << " Computing RI-K (exact exchange) gradient"
602 << std::flush;
605 << TimeStamp() << " RI-K gradient done" << std::flush;
606 }
607
608 // Sanity check independent of the finite-difference tests already done
609 // per-term: translational invariance means the TOTAL gradient must sum
610 // to zero across all atoms. Logged rather than asserted/thrown --
611 // deliberately not blocking a real SCF run over a force-only sanity
612 // check, but worth knowing about if it ever fires.
613 Eigen::Vector3d sum = grad.colwise().sum();
614 if (sum.cwiseAbs().maxCoeff() > 1e-4) {
616 << TimeStamp()
617 << " WARNING: computed forces do not sum to zero across atoms "
618 "(translational invariance check failed, max component="
619 << sum.cwiseAbs().maxCoeff()
620 << ") -- treat these forces with "
621 "caution."
622 << std::flush;
623 }
624
625 // Physical force = -dE/dR, matching the convention external tools
626 // (e.g. ASE's Calculator.get_forces()) expect -- NuclearRepulsionDerivative/
627 // RIJGradient/PulayGradient/GridWeightGradient all return dE/dR directly
628 // (the gradient, not the force), consistent with each other throughout
629 // this branch; negating once here, at the point of storage, rather than
630 // in each individual term, keeps that internal convention consistent
631 // and puts the physical-force sign flip in exactly one place.
632 Eigen::MatrixXd force = -grad;
633 orb.setForces(force);
634
636 << TimeStamp() << " Computed and stored ground-state nuclear forces."
637 << std::flush;
638 // Atomic units (Hartree/Bohr) -- deliberately NOT converted here, same
639 // convention as what gets stored via setForces()/WriteToCpt above and
640 // what the rest of this file's own log output uses for energies
641 // (Hartree throughout; only the earlier "Molecule Coordinates" section
642 // converts to Angstrom, for readability, and that conversion is
643 // unrelated to this).
644 XTP_LOG(Log::error, *pLog_) << " Forces [Ha/Bohr]" << std::flush;
645 for (Index a = 0; a < natoms; ++a) {
646 std::string output =
647 (boost::format(" %1$s"
648 " %2$+1.6f %3$+1.6f %4$+1.6f") %
649 mol[a].getElement() % force(a, 0) % force(a, 1) % force(a, 2))
650 .str();
651 XTP_LOG(Log::error, *pLog_) << output << std::flush;
652 }
653}
654
656 const QMMolecule& mol, const tools::EigenSystem& MOs_alpha,
657 const tools::EigenSystem& MOs_beta) const {
658 // W = W_alpha + W_beta, each WITHOUT the factor of 2 RKS uses -- UKS
659 // spin densities/MO occupations are not pre-doubled (each spin
660 // channel already corresponds to its own electron count).
661 //
662 // NOTE ON VALIDATION: this term is NOT checkable against a
663 // fixed-C finite difference the way the other UKS gradient terms are
664 // -- confirmed directly by a failed attempt to do exactly that (see
665 // git history). The overlap Pulay force specifically corrects for C's
666 // IMPLICIT R-dependence through the orthonormality constraint
667 // C^T S(R) C = I, valid only at a genuine variational stationary
668 // point (the Lagrange-multiplier argument requires C to actually be a
669 // converged SCF solution) -- a fixed, arbitrary C held constant across
670 // displaced geometries never satisfies that constraint
671 // self-consistently, so there is no fixed-C energy this term is
672 // supposed to match. This mirrors exactly why the RKS version of this
673 // term was only ever validated by a genuine, self-consistent
674 // end-to-end SCF test (test_dftengine_forces.cc), never a
675 // fixed-density-matrix unit test. See
676 // compute_non_xc_gradient_uks_finite_difference and
677 // overlap_pulay_gradient_uks_reduces_to_rks in
678 // test_dftengine_private.cc for how this piece is actually checked
679 // instead: the other four terms against a fixed-C finite difference,
680 // and this term separately against the already-validated RKS formula
681 // in the alpha==beta limit.
682 Index n_occ_alpha = num_alpha_electrons_;
683 Index n_occ_beta = num_beta_electrons_;
684 Eigen::MatrixXd C_alpha_occ = MOs_alpha.eigenvectors().leftCols(n_occ_alpha);
685 Eigen::MatrixXd C_beta_occ = MOs_beta.eigenvectors().leftCols(n_occ_beta);
686 Eigen::VectorXd eps_alpha_occ = MOs_alpha.eigenvalues().head(n_occ_alpha);
687 Eigen::VectorXd eps_beta_occ = MOs_beta.eigenvalues().head(n_occ_beta);
688 Eigen::MatrixXd W =
689 C_alpha_occ * eps_alpha_occ.asDiagonal() * C_alpha_occ.transpose() +
690 C_beta_occ * eps_beta_occ.asDiagonal() * C_beta_occ.transpose();
691
692 Index natoms = mol.size();
693 std::vector<AOMatrixDerivative> dS = ComputeOverlapDerivatives(dftbasis_);
694 Eigen::MatrixXd overlap_pulay_grad = Eigen::MatrixXd::Zero(natoms, 3);
695 for (Index a = 0; a < natoms; ++a) {
696 for (Index xyz = 0; xyz < 3; ++xyz) {
697 overlap_pulay_grad(a, xyz) = -W.cwiseProduct(dS[a][xyz]).sum();
698 }
699 }
700 return overlap_pulay_grad;
701}
702
704 const QMMolecule& mol, const UKSConvergenceAcc::SpinDensity& Dspin,
705 const tools::EigenSystem& MOs_alpha,
706 const tools::EigenSystem& MOs_beta) const {
707 Index natoms = mol.size();
708 const Eigen::MatrixXd D_total = Dspin.total();
709
710 // One-electron and RI-J: identical formulas/conventions to the RKS
711 // case, just built from D_total = Dspin.alpha + Dspin.beta -- matches
712 // exactly how RKS's own Dmat is already alpha+beta (E_one and E_coul
713 // in EvaluateUKS use D_total the same way EvaluateClosedShell's Eone/
714 // Etwo use Dmat), confirmed directly by reading EvaluateUKS rather
715 // than assumed.
716 std::vector<AOMatrixDerivative> dT = ComputeKineticDerivatives(dftbasis_);
717 std::vector<AOMatrixDerivative> dVne =
719 Eigen::MatrixXd eone_grad = Eigen::MatrixXd::Zero(natoms, 3);
720 for (Index a = 0; a < natoms; ++a) {
721 for (Index xyz = 0; xyz < 3; ++xyz) {
722 eone_grad(a, xyz) = D_total.cwiseProduct(dT[a][xyz] + dVne[a][xyz]).sum();
723 }
724 }
725
726 Eigen::MatrixXd overlap_pulay_grad =
727 ComputeOverlapPulayGradientUKS(mol, MOs_alpha, MOs_beta);
728
729 Eigen::MatrixXd grad =
731 overlap_pulay_grad +
733
734 // Exact exchange (RI-K), hybrids only. Factor of 0.5*ScaHFX_ (not
735 // ScaHFX_ alone) -- confirmed both algebraically and numerically
736 // (Python, to ~1e-14) that ERIs::CalculateEXX_dmat(P) ==
737 // 0.5*ERIs::CalculateEXX_mos(C) when P=C*C^T, and UKS's own exact
738 // exchange goes through CalculateEXX_dmat (a DIFFERENT code path than
739 // RIKGradient was validated against, which uses CalculateEXX_mos
740 // directly) -- tracing that factor of 0.5 through both spin channels'
741 // energy expressions gives dE_exx/dR =
742 // 0.5*ScaHFX_*[RIKGradient(C_alpha_occ)+RIKGradient(C_beta_occ)], not
743 // the naive ScaHFX_*(...) that would be a factor-of-2 error.
744 if (ScaHFX_ > 0.0) {
745 Eigen::MatrixXd C_alpha_occ =
746 MOs_alpha.eigenvectors().leftCols(num_alpha_electrons_);
747 Eigen::MatrixXd C_beta_occ =
748 MOs_beta.eigenvectors().leftCols(num_beta_electrons_);
749 grad += 0.5 * ScaHFX_ *
752 }
753 return grad;
754}
755
757 Orbitals& orb, const UKSConvergenceAcc::SpinDensity& Dspin,
758 const tools::EigenSystem& MOs_alpha, const tools::EigenSystem& MOs_beta,
759 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
760 if (auxbasis_name_.empty()) {
762 << TimeStamp()
763 << " Skipping UKS force calculation: RI-J gradient only "
764 "implements the RI path, but this SCF ran without an "
765 "auxiliary basis."
766 << std::flush;
767 return;
768 }
769
772 << TimeStamp()
773 << " Skipping UKS force calculation: the libint2 this was "
774 "built against does not support derivative integrals for "
775 "one or more operator categories it needs. Many "
776 "pre-packaged libint2 builds (Homebrew, Ubuntu apt, etc.) "
777 "do not enable this by default -- rebuild libint2 with "
778 "--enable-1body/--enable-eri2/--enable-eri3 to use "
779 "analytic forces."
780 << std::flush;
781 return;
782 }
783
784 if (!ecp_name_.empty()) {
785 // Same reasoning as the RKS ComputeAndStoreForces' own, identical
786 // check just above (and Initialize()'s own, earlier, preferred
787 // check) -- ECP forces are not implemented in either spin
788 // channel's gradient assembly.
790 << TimeStamp() << " Skipping UKS force calculation: an ECP ('"
791 << ecp_name_
792 << "') was used for this SCF, but analytic nuclear forces do "
793 "not yet include the ECP contribution to the force "
794 "(d(V_ECP)/dR) -- computing forces in this configuration "
795 "would silently omit that term rather than fail visibly."
796 << std::flush;
797 return;
798 }
799
800 Eigen::MatrixXd grad =
801 ComputeNonXCGradientUKS(orb.QMAtoms(), Dspin, MOs_alpha, MOs_beta);
802
803 // XC gradient (LDA and GGA both supported -- see the detailed
804 // derivation/validation history on this function's declaration in
805 // dftengine.h and on PulayGradientUKS/GridWeightGradientUKS in
806 // vxc_potential.h).
807 grad += vxcpotential.PulayGradientUKS(Dspin.alpha, Dspin.beta, dftbasis_);
808 grad += vxcpotential.GridWeightGradientUKS(Dspin.alpha, Dspin.beta,
809 orb.QMAtoms());
810
811 // Sanity check independent of the finite-difference tests already
812 // done per-term: translational invariance means the TOTAL gradient
813 // must sum to zero across all atoms. Logged rather than asserted/
814 // thrown, same as the RKS path -- deliberately not blocking a real
815 // SCF run over a force-only sanity check.
816 Eigen::Vector3d sum = grad.colwise().sum();
817 if (sum.cwiseAbs().maxCoeff() > 1e-4) {
819 << TimeStamp()
820 << " WARNING: computed UKS forces do not sum to zero across "
821 "atoms (translational invariance check failed, max "
822 "component="
823 << sum.cwiseAbs().maxCoeff()
824 << ") -- treat these forces with "
825 "caution."
826 << std::flush;
827 }
828
829 // Physical force = -dE/dR, matching the RKS ComputeAndStoreForces
830 // convention exactly -- all pieces above return dE/dR directly (the
831 // gradient, not the force), negated once here at the point of
832 // storage.
833 Eigen::MatrixXd force = -grad;
834 orb.setForces(force);
835
837 << TimeStamp() << " Computed and stored ground-state UKS nuclear forces."
838 << std::flush;
839 // Same convention as ComputeAndStoreForces (RKS): atomic units
840 // (Hartree/Bohr), matching what gets stored via setForces() above.
841 const QMMolecule& mol_for_print = orb.QMAtoms();
842 XTP_LOG(Log::error, *pLog_) << " Forces [Ha/Bohr]" << std::flush;
843 for (Index a = 0; a < force.rows(); ++a) {
844 std::string output = (boost::format(" %1$s"
845 " %2$+1.6f %3$+1.6f %4$+1.6f") %
846 mol_for_print[a].getElement() % force(a, 0) %
847 force(a, 1) % force(a, 2))
848 .str();
849 XTP_LOG(Log::error, *pLog_) << output << std::flush;
850 }
851}
852
853// Build the Coulomb and exact-exchange contributions generated by the current
854// density matrix. The returned pair is conventionally interpreted as
855//
856// (J[P], -K[P]),
857//
858// so that the hybrid Fock update becomes F = H0 + J[P] + a_x (-K[P]) + V_xc.
859// For RI/3c builds the occupied MO block is supplied when available to avoid an
860// unnecessary reconstruction of exchange intermediates.
861std::array<Eigen::MatrixXd, 2> DFTEngine::CalcERIs_EXX(
862 const Eigen::MatrixXd& MOCoeff, const Eigen::MatrixXd& Dmat,
863 double error) const {
864 if (!auxbasis_name_.empty()) {
865 std::array<Eigen::MatrixXd, 2> result;
866 {
867 auto t = timings_.Measure("J (RI)");
868 result[0] = ERIs_.CalculateERIs_3c(Dmat);
869 }
870 if (conv_accelerator_.getUseMixing() || MOCoeff.rows() == 0) {
871 auto t = timings_.Measure("K (RI, from density matrix)");
872 result[1] = ERIs_.CalculateEXX_3c(Eigen::MatrixXd::Zero(0, 0), Dmat);
873 } else {
874 auto t = timings_.Measure("K (RI, from occupied MOs)");
875 Eigen::MatrixXd occblock = MOCoeff.leftCols(num_docc_ + num_socc_alpha_);
876 result[1] = ERIs_.CalculateEXX_3c(occblock, Dmat);
877 }
878 return result;
879 } else {
880 auto t = timings_.Measure("J+K (4c)");
881 return ERIs_.CalculateERIs_EXX_4c(Dmat, error);
882 }
883}
884
885// Pure Coulomb contribution J[P] from the current AO density matrix. The code
886// dispatches to either RI/3c or conventional 4-center integral evaluation.
887Eigen::MatrixXd DFTEngine::CalcERIs(const Eigen::MatrixXd& Dmat,
888 double error) const {
889 if (!auxbasis_name_.empty()) {
890 auto t = timings_.Measure("J (RI)");
891 return ERIs_.CalculateERIs_3c(Dmat);
892 } else {
893 auto t = timings_.Measure("J (4c)");
894 return ERIs_.CalculateERIs_4c(Dmat, error);
895 }
896}
897
899 const double gb = 1024.0 * 1024.0 * 1024.0;
900 const double n = double(dftbasis_.AOBasisSize());
901 const double threads = double(OPENMP::getMaxThreads());
903 << TimeStamp() << " DFT dimensions: " << dftbasis_.AOBasisSize()
904 << " basis functions, " << OPENMP::getMaxThreads() << " threads"
905 << std::flush;
906 if (!auxbasis_name_.empty()) {
907 const double naux = double(ERIs_.AuxSize());
908 // the stored basis-function pairs for every aux function
909 const double tensor =
910 naux * double(ERIs_.StoredPairs()) * sizeof(double) / gb;
911 const double kept = double(ERIs_.StoredPairs()) /
912 double(std::max<Index>(1, ERIs_.AllPairs()));
913 // per thread: the OpenMP reduction copy of J or K, and for K the
914 // unpacked 3c slice plus the product temporaries
915 const double scratch = threads * 4 * n * n * sizeof(double) / gb;
917 << TimeStamp() << " RI: " << ERIs_.AuxSize() << " aux functions ("
918 << ERIs_.Removedfunctions()
919 << " removed from the metric); stored 3c tensor "
920 << std::setprecision(3) << tensor << " GB (" << 100.0 * kept
921 << "% of the pairs at ri_pair_threshold " << ri_pair_threshold_
922 << "); J/K scratch up to about " << scratch << " GB" << std::flush;
923 }
924 double rss = DFTTimings::ResidentMemoryGB(false);
925 if (rss >= 0) {
927 << TimeStamp()
928 << " Resident memory after setup: " << std::setprecision(3) << rss
929 << " GB" << std::flush;
930 }
931}
932
934 const Mat_p_Energy& H0) const {
935 return conv_accelerator_.SolveFockmatrix(H0.matrix());
936}
937
938// Construct a self-consistent model-potential guess by starting from an
939// atomic density P^(0), evaluating
940//
941// F[P^(0)] = H0 + J[P^(0)] + a_x (-K[P^(0)]) + V_xc[P^(0)],
942//
943// and diagonalizing the resulting Fock matrix once.
945 const Mat_p_Energy& H0, const QMMolecule& mol,
946 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
947 Eigen::MatrixXd Dmat = [&]() {
948 auto t = timings_.Measure("guess: atomic densities");
949 return AtomicGuess(mol);
950 }();
951 Mat_p_Energy e_vxc = [&]() {
952 auto t = timings_.Measure("Vxc");
953 return vxcpotential.IntegrateVXC(Dmat);
954 }();
956 << TimeStamp() << " Filled DFT Vxc matrix " << std::flush;
957
958 Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
959
960 if (ScaHFX_ > 0) {
961 std::array<Eigen::MatrixXd, 2> both =
962 CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dmat, 1e-12);
963 H += both[0];
964 H += ScaHFX_ * both[1];
965 } else {
966 H += CalcERIs(Dmat, 1e-12);
967 }
968 return conv_accelerator_.SolveFockmatrix(H);
969}
970
972 timings_.Reset();
973 bool success = EvaluateAndTime(orb);
974 timings_.Report(*pLog_, Log::error);
975 double peak = DFTTimings::ResidentMemoryGB(true);
976 if (peak >= 0) {
978 << TimeStamp() << " Peak resident memory of this process so far: "
979 << std::setprecision(3) << peak << " GB" << std::flush;
980 }
981 return success;
982}
983
985 if (cdft_enabled_) {
986 // Deliberately dispatched here, BEFORE any of the normal
987 // Prepare/SetupH0/SetupVxc/ConfigOrbfile setup below -- RunCDFT
988 // does that same setup internally itself (matching this
989 // function's own structure exactly), so doing it here too would
990 // just duplicate the work. BuildCDFTConstraint needs orb.QMAtoms()
991 // to already be set (the same requirement Evaluate() itself has,
992 // via SetupH0(orb.QMAtoms()) below), so this is not adding any new
993 // requirement on the caller.
996
997 // Suppresses the ordinary DFT force calculation during EVERY
998 // intermediate lambda-bisection trial inside RunCDFT (each of
999 // which internally calls EvaluateUKS, which would otherwise
1000 // trigger the full, expensive ComputeAndStoreForcesUKS on each one
1001 // -- confirmed directly, via a real run, to be a genuine,
1002 // substantial waste: only the FINAL, converged lambda's own force
1003 // is ever actually used). Restored unconditionally below,
1004 // regardless of whether RunCDFT converges, so this never leaks a
1005 // suppressed value back to the caller.
1006 bool original_compute_forces = compute_forces_;
1007 compute_forces_ = false;
1008 bool converged = RunCDFT(orb, constraint);
1009 compute_forces_ = original_compute_forces;
1010
1011 if (converged && original_compute_forces) {
1012 // ONE, explicit, final UKS evaluation to compute the ordinary
1013 // DFT force for the now-converged, fixed CDFT density -- orb
1014 // already holds the converged MOs from RunCDFT's own, final
1015 // internal EvaluateUKS call, so this re-run starts from (and
1016 // should remain at) that same fixed point, converging
1017 // essentially immediately rather than as a fresh, cold SCF.
1018 // Deliberately re-runs Prepare/SetupH0/SetupVxc/ConfigOrbfile
1019 // (the same setup RunCDFT already did once, internally, at its
1020 // own start) rather than threading H0/vxcpotential through
1021 // RunCDFT's own signature to avoid this -- a real, accepted
1022 // cost (this setup, not the SCF itself, is what gets redone),
1023 // chosen specifically to avoid touching RunCDFT's own,
1024 // already-validated signature/control flow at all.
1026 << TimeStamp()
1027 << " CDFT converged -- computing the ordinary DFT force once, "
1028 "for the final, converged density only"
1029 << std::flush;
1030 Prepare(orb);
1031 Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
1032 Vxc_Potential<Vxc_Grid> vxcpotential = SetupVxc(orb.QMAtoms());
1033 ConfigOrbfile(orb);
1034 EvaluateUKS(orb, H0, vxcpotential);
1035 }
1036
1037 if (converged && orb.hasForces()) {
1038 // The explicit, final EvaluateUKS call just above (not RunCDFT's
1039 // own, internal ones, which now have forces suppressed -- see
1040 // the comment on original_compute_forces above) computed and
1041 // stored the ordinary DFT force; this adds the CDFT-specific
1042 // correction on top of it. Done HERE, once, after RunCDFT's
1043 // outer loop has fully converged --
1044 // deliberately NOT inside ComputeAndStoreForcesUKS itself (which
1045 // would otherwise redo this work, wastefully and riskily, at
1046 // EVERY outer CDFT iteration, since RunCDFT calls EvaluateUKS
1047 // repeatedly) and deliberately NOT by changing RunCDFT's own
1048 // signature to pass through the original per-atom fragment
1049 // indices (constraint only carries the already-SUMMED
1050 // weight_matrix, not which atoms went into it -- rebuilding here
1051 // instead, via cdft_constraint_spec_'s own atom_indices, avoids
1052 // touching RunCDFT's own, already-validated signature/behavior
1053 // at all).
1054 //
1055 // Rebuilds the same reference densities/atomic references/
1056 // basis/grid BuildCDFTConstraint itself already built internally
1057 // -- a redundant but cheap recomputation (no SCF involved),
1058 // accepted deliberately for this reason.
1059 std::map<std::string, Eigen::MatrixXd> reference_densities =
1061 AOBasis full_dftbasis;
1062 {
1063 BasisSet basisset;
1064 basisset.Load(dftbasis_name_);
1065 full_dftbasis.Fill(basisset, orb.QMAtoms());
1066 }
1067 Vxc_Grid grid;
1068 grid.GridSetup(grid_name_, orb.QMAtoms(), full_dftbasis);
1069 std::vector<HirshfeldPartition::AtomicReference> atoms =
1071 orb.QMAtoms(), dftbasis_name_, reference_densities);
1072
1073 std::array<Eigen::MatrixXd, 2> Dspin =
1075 // Total (alpha+beta) density -- matches the charge constraint's
1076 // own spin_alpha_coefficient=spin_beta_coefficient=+1.0
1077 // convention exactly (Tr[(D_alpha+D_beta)*W] = the same
1078 // population EvaluateMismatch itself computes inside RunCDFT).
1079 Eigen::MatrixXd density_total = Dspin[0] + Dspin[1];
1080
1081 Eigen::MatrixXd cdft_gradient_correction =
1082 Eigen::MatrixXd::Zero(static_cast<Index>(orb.QMAtoms().size()), 3);
1083 for (Index atom_index : cdft_constraint_spec_.atom_indices) {
1084 cdft_gradient_correction +=
1086 atoms, atom_index, density_total, orb.QMAtoms(), full_dftbasis,
1087 grid);
1088 }
1089 // Physical force = -dE/dR (ComputeAndStoreForcesUKS's own,
1090 // already-established convention): the CDFT correction to the
1091 // GRADIENT is +lambda*d(Tr[D*W_c])/dR (added directly, matching
1092 // ComputeCDFTForceContribution's own gradient-convention
1093 // return), so the correction to the FORCE is -lambda times this
1094 // same quantity.
1095 orb.setForces(orb.getForces() -
1096 constraint.lambda * cdft_gradient_correction);
1097 }
1098 return converged;
1099 }
1100
1101 // Prepare replaces orb's basis, so record which basis its MOs belong to.
1102 const std::string previous_basis =
1103 orb.hasDFTbasisName() ? orb.getDFTbasisName() : "";
1104 Prepare(orb);
1106
1107 const std::string configured_guess = initial_guess_;
1108 warm_started_ = false;
1109 if (warm_start_ && initial_guess_ != "orbfile") {
1110 std::string reason;
1111 if (UsableAsWarmStart(orb, previous_basis, reason)) {
1112 initial_guess_ = "orbfile";
1113 warm_started_ = true;
1115 << TimeStamp()
1116 << " Starting from the orbitals of the previous QM/MM iteration"
1117 << std::flush;
1118 } else {
1120 << TimeStamp() << " Previous orbitals not usable as guess (" << reason
1121 << "); using " << initial_guess_ << std::flush;
1122 }
1123 }
1124
1125 Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
1126 Vxc_Potential<Vxc_Grid> vxcpotential = [&]() {
1127 auto t = timings_.Measure("setup: XC grid");
1128 return SetupVxc(orb.QMAtoms());
1129 }();
1130 ConfigOrbfile(orb);
1131
1132 bool success = false;
1136 << TimeStamp()
1137 << " Forcing closed-shell singlet through UKS development path."
1138 << std::flush;
1139 }
1140 success = EvaluateUKS(orb, H0, vxcpotential);
1141 } else {
1142 success = EvaluateClosedShell(orb, H0, vxcpotential);
1143 }
1144 initial_guess_ = configured_guess;
1145 warm_started_ = false;
1146 return success;
1147}
1148
1150 const std::string& previous_basis,
1151 std::string& reason) const {
1152 if (!orb.hasMOs()) {
1153 reason = "no MOs";
1154 return false;
1155 }
1156 const Index n = dftbasis_.AOBasisSize();
1157 if (orb.MOs().eigenvectors().rows() != n ||
1158 orb.MOs().eigenvectors().cols() != n) {
1159 reason = "basis size differs";
1160 return false;
1161 }
1162 if (!previous_basis.empty() && previous_basis != orb.getDFTbasisName()) {
1163 reason = "basis set differs";
1164 return false;
1165 }
1168 reason = "electron count differs";
1169 return false;
1170 }
1171 return true;
1172}
1173
1175 HirshfeldPartition::Constraint& constraint) {
1176 Prepare(orb);
1177 Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
1178 Vxc_Potential<Vxc_Grid> vxcpotential = SetupVxc(orb.QMAtoms());
1179 ConfigOrbfile(orb);
1180
1181 // Restored on every exit path (converged or not) -- RunCDFT
1182 // deliberately overrides this member's own value between outer
1183 // iterations (to force the warm-start "orbfile" guess from the
1184 // second iteration onward), so it must not leak whatever value the
1185 // caller's own options actually specified.
1186 std::string saved_initial_guess = initial_guess_;
1187
1188 constraints_ = {constraint};
1189
1190 // Bisection bracket for lambda -- deliberately not Newton's method:
1191 // bisection needs only that the population is monotonic in lambda
1192 // (true for a well-behaved CDFT problem: increasing lambda always
1193 // pushes more density toward -- or away from, depending on sign --
1194 // the constrained region), never an explicit dN/dlambda derivative,
1195 // making this the more robust choice for a first implementation.
1196 // Starts centered on the caller's own initial guess (constraint.lambda,
1197 // 0.0 by default) and expands outward, doubling each time, until the
1198 // mismatch changes sign across the bracket or a hard iteration limit
1199 // is hit -- rather than assuming any single fixed bracket width is
1200 // always wide enough for every system.
1201 double lambda_lo = constraint.lambda - 0.1;
1202 double lambda_hi = constraint.lambda + 0.1;
1203
1204 auto EvaluateMismatch = [&](double lambda) -> double {
1205 constraints_[0].lambda = lambda;
1207 << TimeStamp() << " CDFT: starting inner SCF at lambda=" << lambda
1208 << std::flush;
1209 bool scf_converged = EvaluateUKS(orb, H0, vxcpotential);
1210 if (!scf_converged) {
1211 throw std::runtime_error(
1212 "RunCDFT: inner SCF did not converge at lambda=" +
1213 std::to_string(lambda));
1214 }
1215 if (cdft_constraint_spec_.guess_strategy == "warmstart") {
1216 initial_guess_ = "orbfile"; // warm start every subsequent call
1217 }
1218 // "fresh": deliberately leave initial_guess_ untouched here, so it
1219 // stays at whatever the calculation's own, original, top-level
1220 // setting was for every trial -- see this option's own XML help
1221 // text (dftpackage.xml) for why this can matter: if consecutive
1222 // lambda trials correspond to substantially different electronic
1223 // structures, warm-starting from the immediately preceding trial's
1224 // own converged density could be a worse starting point than a
1225 // fresh guess, not a better one.
1226 std::array<Eigen::MatrixXd, 2> Dspin =
1228 double population =
1229 constraint.spin_alpha_coefficient *
1230 Dspin[0].cwiseProduct(constraint.weight_matrix).sum() +
1231 constraint.spin_beta_coefficient *
1232 Dspin[1].cwiseProduct(constraint.weight_matrix).sum();
1233 return population - constraint.target_population;
1234 };
1235
1236 try {
1237 double mismatch_lo = EvaluateMismatch(lambda_lo);
1238 double mismatch_hi = EvaluateMismatch(lambda_hi);
1239
1240 Index bracket_attempts = 0;
1241 constexpr Index kMaxBracketAttempts = 10;
1242 while (mismatch_lo * mismatch_hi > 0.0 &&
1243 bracket_attempts < kMaxBracketAttempts) {
1244 double width = lambda_hi - lambda_lo;
1245 lambda_lo -= 0.5 * width;
1246 lambda_hi += 0.5 * width;
1247 mismatch_lo = EvaluateMismatch(lambda_lo);
1248 mismatch_hi = EvaluateMismatch(lambda_hi);
1249 ++bracket_attempts;
1250 }
1251 if (mismatch_lo * mismatch_hi > 0.0) {
1253 << TimeStamp()
1254 << " RunCDFT: could not bracket a root for the population "
1255 "mismatch after "
1256 << kMaxBracketAttempts
1257 << " bracket-expansion attempts -- the target population may "
1258 "be unreachable for this system, or the initial "
1259 "lambda guess may be far from the actual root."
1260 << std::flush;
1261 initial_guess_ = saved_initial_guess;
1262 constraints_.clear();
1263 return false;
1264 }
1265
1266 for (Index outer_iter = 0; outer_iter < max_cdft_iterations_;
1267 ++outer_iter) {
1268 double lambda_mid = 0.5 * (lambda_lo + lambda_hi);
1269 double mismatch_mid = EvaluateMismatch(lambda_mid);
1270
1272 << TimeStamp() << " CDFT outer iteration " << outer_iter + 1 << " of "
1273 << max_cdft_iterations_ << ": lambda=" << lambda_mid
1274 << " population mismatch=" << mismatch_mid << std::flush;
1275
1276 if (std::abs(mismatch_mid) < cdft_population_tolerance_) {
1277 constraint.lambda = lambda_mid;
1278 initial_guess_ = saved_initial_guess;
1280 << TimeStamp() << " CDFT converged after " << outer_iter + 1
1281 << " outer iterations, lambda=" << lambda_mid << std::flush;
1282 return true;
1283 }
1284
1285 if (mismatch_mid * mismatch_lo < 0.0) {
1286 lambda_hi = lambda_mid;
1287 mismatch_hi = mismatch_mid;
1288 } else {
1289 lambda_lo = lambda_mid;
1290 mismatch_lo = mismatch_mid;
1291 }
1292 }
1293 } catch (const std::runtime_error&) {
1294 initial_guess_ = saved_initial_guess;
1295 constraints_.clear();
1296 throw;
1297 }
1298
1300 << TimeStamp()
1301 << " RunCDFT: outer bisection loop did not converge "
1302 "within "
1303 << max_cdft_iterations_ << " iterations." << std::flush;
1304 constraint.lambda = 0.5 * (lambda_lo + lambda_hi);
1305 initial_guess_ = saved_initial_guess;
1306 return false;
1307}
1308
1309// Restricted SCF loop. The total energy is assembled as
1310//
1311// E = Tr[P H0] + E_nuc + E_coul + E_xc + E_exx,
1312//
1313// with P = 2 C_occ C_occ^T. DIIS or mixing updates the density until both
1314// the energy change and the commutator error are converged.
1316 Orbitals& orb, const Mat_p_Energy& H0,
1317 const Vxc_Potential<Vxc_Grid>& vxcpotential) {
1318
1320 MOs.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
1321 MOs.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
1322
1323 if (initial_guess_ == "orbfile") {
1325 << TimeStamp() << " Reading guess from orbitals object/file"
1326 << std::flush;
1327 MOs = orb.MOs();
1329 } else {
1331 << TimeStamp() << " Setup Initial Guess using: " << initial_guess_
1332 << std::flush;
1333 if (initial_guess_ == "independent") {
1334 MOs = IndependentElectronGuess(H0);
1335 } else if (initial_guess_ == "atom") {
1336 MOs = ModelPotentialGuess(H0, orb.QMAtoms(), vxcpotential);
1337 } else if (initial_guess_ == "huckel") {
1338 MOs = ExtendedHuckelGuess(orb.QMAtoms());
1339 } else if (initial_guess_ == "huckel_dft") {
1340 MOs = ExtendedHuckelDFTGuess(H0, orb.QMAtoms(), vxcpotential);
1341 } else if (initial_guess_ == "dimer_guess") {
1342 // Closed-shell dimer: the block-diagonal guess is restricted as long
1343 // as both monomers carry identical alpha and beta MOs (restricted,
1344 // closed-shell monomers). Open-shell monomers need the UKS path.
1345 Orbitals dimer_guess_orb = BuildDimerGuessFromMonomerFiles(orb.QMAtoms());
1346 if (dimer_guess_orb.getNumberOfAlphaElectrons() !=
1347 dimer_guess_orb.getNumberOfBetaElectrons() ||
1348 !(dimer_guess_orb.MOs().eigenvectors() ==
1349 dimer_guess_orb.MOs_beta().eigenvectors())) {
1350 throw std::runtime_error(
1351 "initial_guess=dimer_guess: this is a restricted (closed-shell) "
1352 "calculation, but at least one monomer .orb file is open-shell "
1353 "or unrestricted. Use force_uks_path to run the dimer "
1354 "unrestricted with this guess.");
1355 }
1356 if (dimer_guess_orb.getNumberOfAlphaElectrons() != num_alpha_electrons_) {
1357 throw std::runtime_error(
1358 "initial_guess=dimer_guess: the monomers have " +
1359 std::to_string(2 * dimer_guess_orb.getNumberOfAlphaElectrons()) +
1360 " electrons in total, but this calculation has " +
1361 std::to_string(2 * num_alpha_electrons_) +
1362 ". Check the monomer charges against the dimer charge.");
1363 }
1364 MOs = dimer_guess_orb.MOs();
1366 } else {
1367 throw std::runtime_error("Initial guess method not known/implemented");
1368 }
1369 }
1370
1371 ConvergenceAcc::SpinDensity spin_dmat =
1372 conv_accelerator_.DensityMatrixSpinResolved(MOs);
1373 Eigen::MatrixXd Dmat = spin_dmat.total();
1374
1376 << TimeStamp() << " Guess Matrix gives N=" << std::setprecision(9)
1377 << Dmat.cwiseProduct(dftAOoverlap_.Matrix()).sum() << " electrons."
1378 << std::flush;
1379
1381 << TimeStamp() << " STARTING SCF cycle" << std::flush;
1383 << " ----------------------------------------------"
1384 "----------------------------"
1385 << std::flush;
1386
1387 Eigen::MatrixXd J = Eigen::MatrixXd::Zero(Dmat.rows(), Dmat.cols());
1388 Eigen::MatrixXd K;
1389 if (ScaHFX_ > 0) {
1390 K = Eigen::MatrixXd::Zero(Dmat.rows(), Dmat.cols());
1391 }
1392
1393 double start_incremental_F_threshold = 1e-4;
1394 if (!auxbasis_name_.empty()) {
1395 start_incremental_F_threshold = 0.0; // Disable if RI is used
1396 }
1397 IncrementalFockBuilder incremental_fock(*pLog_, start_incremental_F_threshold,
1399 incremental_fock.Configure(Dmat);
1400 conv_accelerator_.StartNewSCF();
1401
1402 for (Index this_iter = 0; this_iter < max_iter_; this_iter++) {
1403 XTP_LOG(Log::error, *pLog_) << std::flush;
1404 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Iteration " << this_iter + 1
1405 << " of " << max_iter_ << std::flush;
1406
1407 Mat_p_Energy e_vxc = [&]() {
1408 auto t = timings_.Measure("Vxc");
1409 return vxcpotential.IntegrateVXC(Dmat);
1410 }();
1412 << TimeStamp() << " Filled DFT Vxc matrix " << std::flush;
1413
1414 Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
1415 double Eone = Dmat.cwiseProduct(H0.matrix()).sum();
1416 double Etwo = e_vxc.energy();
1417 double exx = 0.0;
1418
1419 incremental_fock.Start(this_iter, conv_accelerator_.getDIIsError());
1420 incremental_fock.resetMatrices(J, K, Dmat);
1421 incremental_fock.UpdateCriteria(conv_accelerator_.getDIIsError(),
1422 this_iter);
1423
1424 double integral_error =
1425 std::min(conv_accelerator_.getDIIsError() * 1e-5, 1e-5);
1426
1427 if (ScaHFX_ > 0) {
1428 std::array<Eigen::MatrixXd, 2> both = CalcERIs_EXX(
1429 MOs.eigenvectors(), incremental_fock.getDmat_diff(), integral_error);
1430 J += both[0];
1431 H += J;
1432 Etwo += 0.5 * Dmat.cwiseProduct(J).sum();
1433 K += both[1];
1434 H += 0.5 * ScaHFX_ * K;
1435 exx = 0.25 * ScaHFX_ * Dmat.cwiseProduct(K).sum();
1437 << TimeStamp() << " Filled F+K matrix " << std::flush;
1438 } else {
1439 J += CalcERIs(incremental_fock.getDmat_diff(), integral_error);
1441 << TimeStamp() << " Filled F matrix " << std::flush;
1442 H += J;
1443 Etwo += 0.5 * Dmat.cwiseProduct(J).sum();
1444 }
1445
1446 Etwo += exx;
1447 double totenergy = Eone + H0.energy() + Etwo;
1448
1449 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Single particle energy "
1450 << std::setprecision(12) << Eone << std::flush;
1451 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Two particle energy "
1452 << std::setprecision(12) << Etwo << std::flush;
1454 << TimeStamp() << std::setprecision(12) << " Local Exc contribution "
1455 << e_vxc.energy() << std::flush;
1456 if (ScaHFX_ > 0) {
1458 << TimeStamp() << std::setprecision(12)
1459 << " Non local Ex contribution " << exx << std::flush;
1460 }
1462 << TimeStamp() << " Total Energy " << std::setprecision(12) << totenergy
1463 << std::flush;
1464
1465 {
1466 auto t = timings_.Measure("DIIS/ADIIS + diagonalisation");
1467 Dmat = conv_accelerator_.Iterate(Dmat, H, MOs, totenergy);
1468 }
1469 incremental_fock.UpdateDmats(Dmat, conv_accelerator_.getDIIsError(),
1470 this_iter);
1471
1473
1474 if (num_docc_ + num_socc_alpha_ > 0 &&
1475 num_docc_ + num_socc_alpha_ < MOs.eigenvalues().size()) {
1477 << "\t\tGAP "
1480 << std::flush;
1481 }
1482
1483 if (conv_accelerator_.isConverged()) {
1485 << TimeStamp() << " Total Energy has converged to "
1486 << std::setprecision(9) << conv_accelerator_.getDeltaE()
1487 << "[Ha] after " << this_iter + 1
1488 << " iterations. DIIS error is converged up to "
1489 << conv_accelerator_.getDIIsError() << std::flush;
1491 << TimeStamp() << " Final Single Point Energy "
1492 << std::setprecision(12) << totenergy << " Ha" << std::flush;
1493 XTP_LOG(Log::error, *pLog_) << TimeStamp() << std::setprecision(12)
1494 << " Final Local Exc contribution "
1495 << e_vxc.energy() << " Ha" << std::flush;
1496 if (ScaHFX_ > 0) {
1497 XTP_LOG(Log::error, *pLog_) << TimeStamp() << std::setprecision(12)
1498 << " Final Non Local Ex contribution "
1499 << exx << " Ha" << std::flush;
1500 }
1501
1503
1504 Index nuclear_charge = 0;
1505 for (const QMAtom& atom : orb.QMAtoms()) {
1506 nuclear_charge += atom.getNuccharge();
1507 }
1508
1509 orb.setQMEnergy(totenergy);
1510 orb.MOs() = MOs;
1514 orb.setChargeAndSpin(
1515 nuclear_charge - numofelectrons_,
1517
1518 if (compute_forces_) {
1519 auto t = timings_.Measure("forces");
1520 ComputeAndStoreForces(orb, Dmat, vxcpotential);
1521 }
1522
1523 CalcElDipole(orb);
1524 return true;
1525 } else if (this_iter == max_iter_ - 1) {
1527 << TimeStamp() << " DFT calculation has not converged after "
1528 << max_iter_
1529 << " iterations. Use more iterations or another convergence "
1530 "acceleration scheme."
1531 << std::flush;
1532 return false;
1533 }
1534 }
1535
1536 return true;
1537}
1538
1539// Unrestricted SCF loop. The alpha and beta channels are iterated through
1540// separate Fock matrices
1541//
1542// F^alpha = H0 + J[P^alpha + P^beta] + V_xc^alpha + K^alpha
1543// F^beta = H0 + J[P^alpha + P^beta] + V_xc^beta + K^beta,
1544//
1545// while the total energy uses the spin-summed one-electron and Coulomb terms
1546// together with spin-resolved XC and exact-exchange contributions.
1548 const Vxc_Potential<Vxc_Grid>& vxcpotential) {
1549 tools::EigenSystem MOs_alpha;
1550 tools::EigenSystem MOs_beta;
1551
1552 MOs_alpha.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
1553 MOs_alpha.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
1554 MOs_beta.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
1555 MOs_beta.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
1556
1557 UKSConvergenceAcc conv_uks;
1558
1562
1566
1567 conv_uks.Configure(opt_alpha, opt_beta);
1568 conv_uks.setLogger(pLog_);
1570
1571 if (initial_guess_ == "orbfile") {
1573 << TimeStamp() << " Reading UKS guess from orbitals object/file"
1574 << std::flush;
1575
1576 MOs_alpha = orb.MOs();
1577 MOs_alpha.eigenvectors() = OrthogonalizeGuess(MOs_alpha.eigenvectors());
1578
1579 if (orb.hasBetaMOs()) {
1580 MOs_beta = orb.MOs_beta();
1581 MOs_beta.eigenvectors() = OrthogonalizeGuess(MOs_beta.eigenvectors());
1582 } else {
1584 << TimeStamp()
1585 << " Orbital file has no beta MOs, using alpha guess for beta."
1586 << std::flush;
1587 MOs_beta = MOs_alpha;
1588 }
1589 } else if (initial_guess_ == "dimer_guess") {
1591 << TimeStamp()
1592 << " Building UKS guess from two monomer .orb files (dimer_guess)"
1593 << std::flush;
1594 Orbitals dimer_guess_orb = BuildDimerGuessFromMonomerFiles(orb.QMAtoms());
1595 MOs_alpha = dimer_guess_orb.MOs();
1596 MOs_alpha.eigenvectors() = OrthogonalizeGuess(MOs_alpha.eigenvectors());
1597 MOs_beta = dimer_guess_orb.MOs_beta();
1598 MOs_beta.eigenvectors() = OrthogonalizeGuess(MOs_beta.eigenvectors());
1599 } else {
1601 << TimeStamp() << " Setup UKS Initial Guess using: " << initial_guess_
1602 << std::flush;
1603
1604 tools::EigenSystem guess;
1605 if (initial_guess_ == "independent") {
1606 guess = IndependentElectronGuess(H0);
1607 } else if (initial_guess_ == "atom") {
1608 guess = ModelPotentialGuess(H0, orb.QMAtoms(), vxcpotential);
1609 } else if (initial_guess_ == "huckel") {
1610 guess = ExtendedHuckelGuess(orb.QMAtoms());
1611 } else if (initial_guess_ == "huckel_dft") {
1612 guess = ExtendedHuckelDFTGuess(H0, orb.QMAtoms(), vxcpotential);
1613 } else {
1614 throw std::runtime_error("Initial guess method not known/implemented");
1615 }
1616
1617 MOs_alpha = guess;
1618 MOs_beta = guess;
1619 }
1620
1621 // Build the initial spin densities P^alpha and P^beta from the chosen
1622 // starting orbitals before entering the coupled UKS iterations.
1624 conv_uks.DensityMatrix(MOs_alpha, MOs_beta);
1625
1627 << TimeStamp() << " UKS guess gives Nalpha="
1628 << Dspin.alpha.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1629 << " Nbeta=" << Dspin.beta.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1630 << " Ntot=" << Dspin.total().cwiseProduct(dftAOoverlap_.Matrix()).sum()
1631 << std::flush;
1632
1634 << TimeStamp() << " STARTING UKS SCF cycle" << std::flush;
1636 << " ------------------------------------------------------------"
1637 << std::flush;
1638
1639 for (Index this_iter = 0; this_iter < max_iter_; ++this_iter) {
1640 XTP_LOG(Log::error, *pLog_) << std::flush;
1641 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Iteration " << this_iter + 1
1642 << " of " << max_iter_ << std::flush;
1643
1644 Eigen::MatrixXd H_alpha = H0.matrix();
1645 Eigen::MatrixXd H_beta = H0.matrix();
1646
1647 // The Coulomb contribution depends only on the total density
1648 // P = P^alpha + P^beta, while exchange and XC remain spin resolved.
1649 const Eigen::MatrixXd D_total = Dspin.total();
1650
1651 double E_one = Dspin.alpha.cwiseProduct(H0.matrix()).sum() +
1652 Dspin.beta.cwiseProduct(H0.matrix()).sum();
1653
1654 double E_coul = 0.0;
1655 double E_xc = 0.0;
1656 double E_exx = 0.0;
1657
1658 double integral_error = std::min(conv_uks.getDIIsError() * 1e-5, 1e-5);
1659
1660 if (ScaHFX_ > 0) {
1661 std::array<Eigen::MatrixXd, 2> both_alpha = CalcERIs_EXX(
1662 Eigen::MatrixXd::Zero(0, 0), Dspin.alpha, integral_error);
1663 std::array<Eigen::MatrixXd, 2> both_beta =
1664 CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dspin.beta, integral_error);
1665
1666 Eigen::MatrixXd J = both_alpha[0] + both_beta[0];
1667 Eigen::MatrixXd K_alpha = both_alpha[1];
1668 Eigen::MatrixXd K_beta = both_beta[1];
1669
1670 H_alpha += J + ScaHFX_ * K_alpha;
1671 H_beta += J + ScaHFX_ * K_beta;
1672
1673 E_coul = 0.5 * D_total.cwiseProduct(J).sum();
1674 E_exx = 0.5 * ScaHFX_ *
1675 (Dspin.alpha.cwiseProduct(K_alpha).sum() +
1676 Dspin.beta.cwiseProduct(K_beta).sum());
1677 } else {
1678 Eigen::MatrixXd J = CalcERIs(D_total, integral_error);
1679 H_alpha += J;
1680 H_beta += J;
1681 E_coul = 0.5 * D_total.cwiseProduct(J).sum();
1682 }
1683
1684 auto vxc = [&]() {
1685 auto t = timings_.Measure("Vxc");
1686 return vxcpotential.IntegrateVXCSpin(Dspin.alpha, Dspin.beta);
1687 }();
1688 H_alpha += vxc.vxc_alpha;
1689 H_beta += vxc.vxc_beta;
1690 E_xc = vxc.energy;
1691
1692 double totenergy = H0.energy() + E_one + E_coul + E_xc + E_exx;
1693
1694 // CDFT constraint potential -- deliberately the LAST term added to
1695 // either Fock matrix, and gated by a single, cheap .empty() check:
1696 // for any standard, non-CDFT run (constraints_ left at its default,
1697 // empty state), this entire block is skipped, and both H_alpha and
1698 // H_beta are built exactly as they always were -- no measurable
1699 // overhead, no behavior change whatsoever. Adds
1700 // lambda_c * spin_alpha/beta_coefficient * W_c to the respective
1701 // Fock matrix for every active constraint c (a charge constraint
1702 // uses +1/+1, adding the identical potential to both channels; a
1703 // future spin constraint would use +1/-1 -- see Constraint's own
1704 // comment in hirshfeldpartition.h for why these are stored
1705 // separately rather than this code assuming "charge" specifically),
1706 // and the corresponding correction term to the reported total
1707 // energy: E_CDFT = E_KS + sum_c lambda_c * (N_c^computed -
1708 // N_c^target), the standard Wu-Van Voorhis Lagrangian.
1709 if (!constraints_.empty()) {
1711 H_alpha += (c.lambda * c.spin_alpha_coefficient) * c.weight_matrix;
1712 H_beta += (c.lambda * c.spin_beta_coefficient) * c.weight_matrix;
1713 double population =
1714 c.spin_alpha_coefficient *
1715 Dspin.alpha.cwiseProduct(c.weight_matrix).sum() +
1716 c.spin_beta_coefficient *
1717 Dspin.beta.cwiseProduct(c.weight_matrix).sum();
1718 totenergy += c.lambda * (population - c.target_population);
1719 }
1720 }
1721
1722 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " One particle energy "
1723 << std::setprecision(12) << E_one << std::flush;
1724 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Coulomb contribution "
1725 << std::setprecision(12) << E_coul << std::flush;
1726 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " XC contribution "
1727 << std::setprecision(12) << E_xc << std::flush;
1728 if (ScaHFX_ > 0) {
1730 << TimeStamp() << " EXX contribution " << std::setprecision(12)
1731 << E_exx << std::flush;
1732 }
1734 << TimeStamp() << " Total Energy " << std::setprecision(12) << totenergy
1735 << std::flush;
1736
1737 UKSConvergenceAcc::SpinFock Hspin{H_alpha, H_beta};
1738
1739 // Coupled Fock builder: both new densities are used together, for
1740 // BOTH the Coulomb/exchange terms and the XC potential, in a single
1741 // call. This is what lets CoupledAugmentedHessianStep/
1742 // BuildCoupledSigmaVector capture the real alpha-beta coupling
1743 // (through the shared Coulomb potential and the XC kernel's
1744 // cross-spin terms). Mirrors the exact same H0 + Coulomb/exchange +
1745 // XC sequence already used to build H_alpha/H_beta themselves just
1746 // above, so a perturbed density that happens to equal the current
1747 // one reproduces the identical Fock matrix.
1748 conv_uks.setCoupledFockBuilder(
1749 [this, &H0, &vxcpotential](
1750 const Eigen::MatrixXd& alpha_new,
1751 const Eigen::MatrixXd& beta_new) -> UKSConvergenceAcc::SpinFock {
1753 H_new.alpha = H0.matrix();
1754 H_new.beta = H0.matrix();
1755 constexpr double kIntegralError = 1e-8;
1756 if (ScaHFX_ > 0) {
1757 std::array<Eigen::MatrixXd, 2> both_alpha_new = CalcERIs_EXX(
1758 Eigen::MatrixXd::Zero(0, 0), alpha_new, kIntegralError);
1759 std::array<Eigen::MatrixXd, 2> both_beta_new = CalcERIs_EXX(
1760 Eigen::MatrixXd::Zero(0, 0), beta_new, kIntegralError);
1761 Eigen::MatrixXd J_new = both_alpha_new[0] + both_beta_new[0];
1762 H_new.alpha += J_new + ScaHFX_ * both_alpha_new[1];
1763 H_new.beta += J_new + ScaHFX_ * both_beta_new[1];
1764 } else {
1765 Eigen::MatrixXd D_total_new = alpha_new + beta_new;
1766 Eigen::MatrixXd J_new = CalcERIs(D_total_new, kIntegralError);
1767 H_new.alpha += J_new;
1768 H_new.beta += J_new;
1769 }
1770 auto vxc_new = [&]() {
1771 auto t = timings_.Measure("Vxc");
1772 return vxcpotential.IntegrateVXCSpin(alpha_new, beta_new);
1773 }();
1774 H_new.alpha += vxc_new.vxc_alpha;
1775 H_new.beta += vxc_new.vxc_beta;
1776 return H_new;
1777 });
1778
1779 {
1780 auto t = timings_.Measure("DIIS/ADIIS + diagonalisation");
1781 Dspin = conv_uks.Iterate(Dspin, Hspin, MOs_alpha, MOs_beta, totenergy);
1782 }
1784 MOs_beta = MOs_alpha;
1785 Dspin.beta = Dspin.alpha;
1786 }
1787
1789 << TimeStamp()
1790 << " Nalpha=" << Dspin.alpha.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1791 << " Nbeta=" << Dspin.beta.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1792 << std::flush;
1793
1795 << TimeStamp() << " <Sz> = "
1796 << 0.5 * double(num_alpha_electrons_ - num_beta_electrons_)
1797 << std::flush;
1798
1799 PrintMOsUKS(MOs_alpha.eigenvalues(), MOs_beta.eigenvalues(), Log::info);
1800
1801 if (conv_uks.isConverged()) {
1802 Index nuclear_charge = 0;
1803 for (const QMAtom& atom : orb.QMAtoms()) {
1804 nuclear_charge += atom.getNuccharge();
1805 }
1806
1807 CanonicalizeOrbitalPhases(MOs_alpha);
1808 CanonicalizeOrbitalPhases(MOs_beta);
1809
1810 orb.setQMEnergy(totenergy);
1811 orb.MOs() = MOs_alpha;
1812 orb.MOs_beta() = MOs_beta;
1817 orb.setChargeAndSpin(
1818 nuclear_charge - numofelectrons_,
1820
1822 << TimeStamp() << " UKS converged after " << this_iter + 1
1823 << " iterations. Delta E=" << conv_uks.getDeltaE()
1824 << " DIIS error=" << conv_uks.getDIIsError() << std::flush;
1825
1827 << TimeStamp() << " Final Single Point Energy "
1828 << std::setprecision(12) << totenergy << " Ha" << std::flush;
1830 << TimeStamp() << std::setprecision(12) << " Final XC contribution "
1831 << E_xc << " Ha" << std::flush;
1832 if (ScaHFX_ > 0) {
1834 << TimeStamp() << std::setprecision(12)
1835 << " Final EXX contribution " << E_exx << " Ha" << std::flush;
1836 }
1837
1839 << TimeStamp() << " <Sz> = "
1840 << 0.5 * double(num_alpha_electrons_ - num_beta_electrons_)
1841 << std::flush;
1842
1843 PrintMOsUKS(MOs_alpha.eigenvalues(), MOs_beta.eigenvalues(), Log::error);
1844
1845 if (compute_forces_) {
1846 auto t = timings_.Measure("forces");
1847 ComputeAndStoreForcesUKS(orb, Dspin, MOs_alpha, MOs_beta, vxcpotential);
1848 }
1849
1850 CalcElDipole(orb);
1851 return true;
1852 }
1853
1854 if (this_iter == max_iter_ - 1) {
1856 << TimeStamp() << " UKS calculation has not converged after "
1857 << max_iter_ << " iterations." << std::flush;
1858 return false;
1859 }
1860 }
1861
1862 return false;
1863}
1864
1865// One-electron core Hamiltonian and its constant energy offset.
1866//
1867// The matrix part is
1868//
1869// H0 = T + V_nuc + V_ECP + V_ext,
1870//
1871// while the scalar energy collects all nucleus-nucleus and nucleus-external
1872// interaction terms that do not depend on the electronic density.
1874 auto h0_timer =
1875 std::make_unique<DFTTimings::Scope>(timings_, "setup: one-electron H0");
1876
1877 AOKinetic dftAOkinetic;
1878
1879 dftAOkinetic.Fill(dftbasis_);
1881 << TimeStamp() << " Filled DFT Kinetic energy matrix ." << std::flush;
1882
1883 AOMultipole dftAOESP;
1884 dftAOESP.FillPotential(dftbasis_, mol);
1886 << TimeStamp() << " Filled DFT nuclear potential matrix." << std::flush;
1887
1888 Eigen::MatrixXd H0 = dftAOkinetic.Matrix() + dftAOESP.Matrix();
1890 << TimeStamp() << " Constructed independent particle hamiltonian "
1891 << std::flush;
1892 double E0 = NuclearRepulsion(mol);
1893 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Nuclear Repulsion Energy is "
1894 << std::setprecision(9) << E0 << std::flush;
1895
1896 if (!ecp_name_.empty()) {
1897 AOECP dftAOECP;
1898 dftAOECP.FillPotential(dftbasis_, ecp_);
1899 H0 += dftAOECP.Matrix();
1901 << TimeStamp() << " Filled DFT ECP matrix" << std::flush;
1902 }
1903 h0_timer.reset();
1904
1905 if (externalsites_ != nullptr) {
1906 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " " << externalsites_->size()
1907 << " External sites" << std::flush;
1908 bool has_quadrupoles = std::any_of(
1909 externalsites_->begin(), externalsites_->end(),
1910 [](const std::unique_ptr<StaticSite>& s) { return s->getRank() == 2; });
1911 std::string header =
1912 " Name Coordinates[a0] charge[e] dipole[e*a0] ";
1913 if (has_quadrupoles) {
1914 header += " quadrupole[e*a0^2]";
1915 }
1916 XTP_LOG(Log::error, *pLog_) << header << std::flush;
1917 Index limit = 50;
1918 Index counter = 0;
1919 for (const std::unique_ptr<StaticSite>& site : *externalsites_) {
1920 if (counter == limit) {
1921 break;
1922 }
1923 std::string output =
1924 (boost::format(" %1$s"
1925 " %2$+1.4f %3$+1.4f %4$+1.4f"
1926 " %5$+1.4f") %
1927 site->getElement() % site->getPos()[0] % site->getPos()[1] %
1928 site->getPos()[2] % site->getCharge())
1929 .str();
1930 const Eigen::Vector3d& dipole = site->getDipole();
1931 output += (boost::format(" %1$+1.4f %2$+1.4f %3$+1.4f") % dipole[0] %
1932 dipole[1] % dipole[2])
1933 .str();
1934 if (site->getRank() > 1) {
1935 Eigen::VectorXd quadrupole = site->Q().tail<5>();
1936 output +=
1937 (boost::format(" %1$+1.4f %2$+1.4f %3$+1.4f %4$+1.4f %5$+1.4f") %
1938 quadrupole[0] % quadrupole[1] % quadrupole[2] % quadrupole[3] %
1939 quadrupole[4])
1940 .str();
1941 }
1942 XTP_LOG(Log::error, *pLog_) << output << std::flush;
1943 counter++;
1944 }
1945 if (counter == limit) {
1947 << " ... (" << externalsites_->size() - limit
1948 << " sites not displayed)\n"
1949 << std::flush;
1950 }
1951
1952 auto t = timings_.Measure("setup: external multipoles");
1953 Mat_p_Energy ext_multipoles =
1956 << TimeStamp() << " Nuclei-external site interaction energy "
1957 << std::setprecision(9) << ext_multipoles.energy() << std::flush;
1958 E0 += ext_multipoles.energy();
1959 H0 += ext_multipoles.matrix();
1960 }
1961
1963 Orbitals extdensity;
1964 extdensity.ReadFromCpt(orbfilename_);
1965 Mat_p_Energy extdensity_result = IntegrateExternalDensity(mol, extdensity);
1966 E0 += extdensity_result.energy();
1968 << TimeStamp() << " Nuclei-external density interaction energy "
1969 << std::setprecision(9) << extdensity_result.energy() << std::flush;
1970 H0 += extdensity_result.matrix();
1971 }
1972
1974
1976 << TimeStamp() << " Integrating external electric field with F[Hrt]="
1977 << extfield_.transpose() << std::flush;
1978 H0 += IntegrateExternalField(mol);
1979 }
1980
1981 if (has_ewaldgrid_) {
1983 << TimeStamp() << " Integrating external Ewald Potential" << std::flush;
1984 auto t = timings_.Measure("setup: Ewald potential on grid");
1985 Vxc_Grid ewaldgrid;
1986 ewaldgrid.GridSetup(grid_name_, mol, dftbasis_);
1987
1988 // The rebuild above is REQUIRED, not a convenience. external_ewaldgrid_
1989 // arrived by value from QMRegion, where it was built against an AOBasis
1990 // local to QMRegion::PrepareEwaldPotentialGrid, and a Vxc_Grid holds raw
1991 // `const AOShell*` into the basis it was built from. Those pointers
1992 // dangle by the time it gets here, so only its coordinates and its
1993 // potential values may be read -- integrating on it directly would be
1994 // undefined behaviour. See the comment on QMRegion::ewaldgrid_.
1995 //
1996 // The two grids agree only if both sides used the same name, molecule
1997 // and basis. Checked, because pairing potential values with the wrong
1998 // points is a wrong Hamiltonian that nothing downstream would flag.
1999 if (ewaldgrid.getBoxesSize() != external_ewaldgrid_.getBoxesSize()) {
2000 throw std::runtime_error(
2001 "DFTEngine: the external Ewald potential grid has " +
2002 std::to_string(external_ewaldgrid_.getBoxesSize()) +
2003 " boxes but this molecule's own grid has " +
2004 std::to_string(ewaldgrid.getBoxesSize()) +
2005 ". The potential was evaluated on a different grid than the one "
2006 "being integrated over.");
2007 }
2008 // make sure the Potential values are copied from the external ewald grid to
2009 // this one
2010 for (Index i = 0; i < ewaldgrid.getBoxesSize(); ++i) {
2011 GridBox& box = ewaldgrid[i];
2012 const std::vector<double>& source =
2013 external_ewaldgrid_[i].getPotentialValues();
2014 if (Index(source.size()) != box.size()) {
2015 throw std::runtime_error("DFTEngine: box " + std::to_string(i) +
2016 " of the external Ewald "
2017 "potential grid holds " +
2018 std::to_string(source.size()) +
2019 " values for " + std::to_string(box.size()) +
2020 " grid points.");
2021 }
2022 std::vector<double>& values = box.getPotentialValues();
2023 values = source;
2024 }
2025 // The AO matrix depends on the basis, the grid and the potential values;
2026 // reuse it from the previous run if all three are the same.
2027 std::string ewald_key;
2028 if (setup_cache_ != nullptr) {
2029 double sum = 0.0;
2030 double sum2 = 0.0;
2031 Index points = 0;
2032 for (Index i = 0; i < ewaldgrid.getBoxesSize(); ++i) {
2033 for (double v : ewaldgrid[i].getPotentialValues()) {
2034 sum += v;
2035 sum2 += v * v;
2036 ++points;
2037 }
2038 }
2039 std::ostringstream key;
2040 key << std::setprecision(17) << RISetupKey() << "|" << grid_name_ << "|"
2041 << points << "|" << sum << "|" << sum2;
2042 ewald_key = key.str();
2043 }
2044 if (setup_cache_ != nullptr && setup_cache_->ewald_key == ewald_key &&
2045 setup_cache_->ewald_matrix.rows() == dftbasis_.AOBasisSize()) {
2046 H0 += setup_cache_->ewald_matrix;
2048 << TimeStamp()
2049 << " Reusing the Ewald potential matrix of the previous run"
2050 << std::flush;
2051 } else {
2052 Ewald_Potential<Vxc_Grid> EwaldIntegration(ewaldgrid);
2053 const Eigen::MatrixXd ewald_matrix =
2054 EwaldIntegration.IntegrateEwald(dftbasis_.AOBasisSize()).matrix();
2055 H0 += ewald_matrix;
2056 if (setup_cache_ != nullptr) {
2057 setup_cache_->ewald_key = ewald_key;
2058 setup_cache_->ewald_matrix = ewald_matrix;
2059 }
2060 }
2061
2062 // The grid reaches the electron density only. The nuclei sit in the
2063 // same potential, and their share arrives as a scalar -- the same
2064 // pairing IntegrateExternalMultipoles has with ExternalRepulsion.
2066 << TimeStamp() << " Nuclei-external Ewald potential energy "
2067 << std::setprecision(9) << ewald_nuclear_energy_ << std::flush;
2069 }
2070
2071 return Mat_p_Energy(E0, H0);
2072}
2073
2074std::string DFTEngine::RISetupKey() const {
2075 std::ostringstream key;
2076 key << std::setprecision(17) << dftbasis_name_ << "|" << auxbasis_name_ << "|"
2078 for (const AOBasis* basis : {&dftbasis_, &auxbasis_}) {
2079 key << "|";
2080 for (const AOShell& shell : *basis) {
2081 key << static_cast<int>(shell.getL()) << "," << shell.getSize() << ","
2082 << shell.getPos().x() << "," << shell.getPos().y() << ","
2083 << shell.getPos().z() << ";";
2084 }
2085 }
2086 return key.str();
2087}
2088
2089void DFTEngine::setSCFToleranceFloor(double energy, double error) {
2090 if (energy <= conv_opt_.Econverged && error <= conv_opt_.error_converged) {
2091 return;
2092 }
2093 conv_opt_.Econverged = std::max(conv_opt_.Econverged, energy);
2094 conv_opt_.error_converged = std::max(conv_opt_.error_converged, error);
2096 << TimeStamp() << " SCF thresholds for this run: Delta E "
2097 << conv_opt_.Econverged << " Ha, DIIS error " << conv_opt_.error_converged
2098 << " (set by the caller)" << std::flush;
2099}
2100
2102 if (setup_cache_ == nullptr || auxbasis_name_.empty() ||
2103 ERIs_.AuxSize() == 0) {
2104 return;
2105 }
2106 setup_cache_->eris = std::move(ERIs_);
2107 setup_cache_->eris_key = eris_key_;
2108 setup_cache_->has_eris = true;
2109}
2110
2111// Precompute SCF-invariant matrices: overlap for the generalized eigenvalue
2112// problem and the RI/4c electron-repulsion backend that later yields J[P] and
2113// K[P].
2115 auto overlap_timer =
2116 std::make_unique<DFTTimings::Scope>(timings_, "setup: overlap, S^-1/2");
2119 << TimeStamp() << " Filled DFT Overlap matrix." << std::flush;
2120
2121 conv_opt_.numberofelectrons = numofelectrons_;
2122 conv_opt_.number_alpha_electrons = num_alpha_electrons_;
2123 conv_opt_.number_beta_electrons = num_beta_electrons_;
2127 conv_accelerator_.Configure(conv_opt_);
2128 conv_accelerator_.setLogger(pLog_);
2130 conv_accelerator_.PrintConfigOptions();
2131 overlap_timer.reset();
2132
2133 if (!auxbasis_name_.empty() && setup_cache_ != nullptr &&
2134 setup_cache_->has_eris && setup_cache_->eris_key == RISetupKey()) {
2135 // same basis sets and geometry as the previous run (QM/MM iteration)
2136 ERIs_ = std::move(setup_cache_->eris);
2137 setup_cache_->has_eris = false;
2138 eris_key_ = setup_cache_->eris_key;
2140 << TimeStamp()
2141 << " Reusing the RI integrals of the previous run (same basis sets "
2142 "and geometry)"
2143 << std::flush;
2144 } else if (!auxbasis_name_.empty()) {
2145 // prepare invariant part of electron repulsion integrals
2146 auto ri_start = DFTTimings::Clock::now();
2147 if (setup_cache_ != nullptr) {
2148 setup_cache_->has_eris = false;
2149 setup_cache_->eris = ERIs(); // free a stale tensor before building
2151 }
2153 double ri_seconds =
2154 std::chrono::duration<double>(DFTTimings::Clock::now() - ri_start)
2155 .count();
2156 timings_.Add("setup: RI metric V^-1/2", ERIs_.MetricSeconds());
2157 timings_.Add("setup: RI 3c integrals", ri_seconds - ERIs_.MetricSeconds());
2159 << TimeStamp() << " Inverted AUX Coulomb matrix, removed "
2160 << ERIs_.Removedfunctions() << " functions from aux basis"
2161 << std::flush;
2163 << TimeStamp()
2164 << " Setup invariant parts of Electron Repulsion integrals "
2165 << std::flush;
2166 } else {
2168 << TimeStamp() << " Calculating 4c diagonals. " << std::flush;
2169 auto t = timings_.Measure("setup: 4c Schwarz screening");
2170 ERIs_.Initialize_4c(dftbasis_);
2172 << TimeStamp() << " Calculated 4c diagonals. " << std::flush;
2173 }
2174
2175 return;
2176}
2177
2178namespace {
2179// Spherically averaged atom SCF used for the atomic guess and the
2180// Hirshfeld reference densities.
2181//
2182// Averaged over rotations, an operator on one atom keeps, between two
2183// shells of equal l, (trace / (2l+1)) times the identity and nothing
2184// between different l. The averaged problem therefore separates into one
2185// small "radial" problem per l over the shells of that l, whose levels are
2186// (2l+1)-fold degenerate. Electrons are filled into these levels by aufbau;
2187// a partly filled level holds them spread evenly over its 2l+1 components.
2188// The density is spherical by construction, so there is no orientation of
2189// a partly filled shell to choose and no symmetry breaking to converge
2190// through (with integer occupation of the individual orbitals, an open p
2191// or d shell made the SCF stall or oscillate).
2192class SphericalAtomSCF {
2193 public:
2194 explicit SphericalAtomSCF(const AOBasis& basis) : n_(basis.AOBasisSize()) {
2195 for (const AOShell& shell : basis) {
2196 const Index l = static_cast<Index>(shell.getL());
2197 Index g = 0;
2198 while (g < Index(l_.size()) && l_[g] != l) {
2199 ++g;
2200 }
2201 if (g == Index(l_.size())) {
2202 l_.push_back(l);
2203 starts_.emplace_back();
2204 }
2205 starts_[g].push_back(shell.getStartIndex());
2206 }
2207 }
2208
2209 Index Groups() const { return Index(l_.size()); }
2210
2212 Eigen::MatrixXd Reduce(const Eigen::MatrixXd& full, Index g) const {
2213 const std::vector<Index>& st = starts_[g];
2214 const Index size = 2 * l_[g] + 1;
2215 Eigen::MatrixXd red(st.size(), st.size());
2216 for (Index i = 0; i < Index(st.size()); ++i) {
2217 for (Index j = 0; j < Index(st.size()); ++j) {
2218 red(i, j) = full.block(st[i], st[j], size, size).trace() / double(size);
2219 }
2220 }
2221 return red;
2222 }
2223
2226 Eigen::MatrixXd Expand(const std::vector<Eigen::MatrixXd>& red) const {
2227 Eigen::MatrixXd full = Eigen::MatrixXd::Zero(n_, n_);
2228 for (Index g = 0; g < Groups(); ++g) {
2229 const std::vector<Index>& st = starts_[g];
2230 const Index size = 2 * l_[g] + 1;
2231 for (Index i = 0; i < Index(st.size()); ++i) {
2232 for (Index j = 0; j < Index(st.size()); ++j) {
2233 full.block(st[i], st[j], size, size)
2234 .diagonal()
2235 .setConstant(red[g](i, j));
2236 }
2237 }
2238 }
2239 return full;
2240 }
2241
2244 std::vector<Eigen::MatrixXd> Density(
2245 const std::vector<Eigen::MatrixXd>& fock,
2246 const std::vector<Eigen::MatrixXd>& overlap,
2247 const std::vector<Eigen::VectorXd>& occupation) const {
2248 std::vector<Eigen::MatrixXd> dens(Groups());
2249 for (Index g = 0; g < Groups(); ++g) {
2250 Eigen::GeneralizedSelfAdjointEigenSolver<Eigen::MatrixXd> es(fock[g],
2251 overlap[g]);
2252 const Index nocc = occupation[g].size();
2253 const Eigen::MatrixXd c = es.eigenvectors().leftCols(nocc);
2254 dens[g] = c * (occupation[g] / double(2 * l_[g] + 1)).asDiagonal() *
2255 c.transpose();
2256 }
2257 return dens;
2258 }
2259
2262 std::vector<Eigen::VectorXd> AufbauOccupation(
2263 const std::vector<Eigen::MatrixXd>& fock,
2264 const std::vector<Eigen::MatrixXd>& overlap, double nelectrons) const {
2265 struct Level {
2266 double energy;
2267 Index group;
2268 Index index;
2269 };
2270 std::vector<Level> levels;
2271 std::vector<Eigen::VectorXd> occ(Groups());
2272 for (Index g = 0; g < Groups(); ++g) {
2273 Eigen::GeneralizedSelfAdjointEigenSolver<Eigen::MatrixXd> es(
2274 fock[g], overlap[g], Eigen::EigenvaluesOnly);
2275 occ[g] = Eigen::VectorXd::Zero(es.eigenvalues().size());
2276 for (Index k = 0; k < es.eigenvalues().size(); ++k) {
2277 levels.push_back({es.eigenvalues()(k), g, k});
2278 }
2279 }
2280 std::stable_sort(
2281 levels.begin(), levels.end(),
2282 [](const Level& a, const Level& b) { return a.energy < b.energy; });
2283 double left = nelectrons;
2284 for (const Level& lev : levels) {
2285 const double take = std::clamp(left, 0.0, double(2 * l_[lev.group] + 1));
2286 occ[lev.group](lev.index) = take;
2287 left -= take;
2288 }
2289 return occ;
2290 }
2291
2295 bool OccupationFromSubshells(
2296 const std::vector<std::array<double, 3>>& subshells,
2297 const std::vector<Eigen::MatrixXd>& overlap,
2298 std::vector<Eigen::VectorXd>& occ) const {
2299 occ.assign(Groups(), Eigen::VectorXd());
2300 for (Index g = 0; g < Groups(); ++g) {
2301 occ[g] = Eigen::VectorXd::Zero(overlap[g].rows());
2302 }
2303 for (const auto& sub : subshells) {
2304 const Index l = Index(sub[0]);
2305 const Index k = Index(sub[1]);
2306 Index g = 0;
2307 while (g < Groups() && l_[g] != l) {
2308 ++g;
2309 }
2310 if (g == Groups() || k >= occ[g].size()) {
2311 return false;
2312 }
2313 occ[g](k) += sub[2];
2314 }
2315 return true;
2316 }
2317
2318 private:
2319 Index n_;
2320 std::vector<Index> l_;
2321 std::vector<std::vector<Index>> starts_;
2322};
2323
2324// Pulay DIIS on a flattened Fock vector with a flattened error vector.
2325class SimpleDIIS {
2326 public:
2327 explicit SimpleDIIS(Index maxhist) : maxhist_(maxhist) {}
2328
2329 Eigen::VectorXd Extrapolate(const Eigen::VectorXd& fock,
2330 const Eigen::VectorXd& error) {
2331 focks_.push_back(fock);
2332 errors_.push_back(error);
2333 if (Index(focks_.size()) > maxhist_) {
2334 focks_.erase(focks_.begin());
2335 errors_.erase(errors_.begin());
2336 }
2337 const Index m = Index(focks_.size());
2338 if (m < 2) {
2339 return fock;
2340 }
2341 Eigen::MatrixXd b = Eigen::MatrixXd::Zero(m + 1, m + 1);
2342 for (Index i = 0; i < m; ++i) {
2343 for (Index j = 0; j <= i; ++j) {
2344 b(i, j) = b(j, i) = errors_[i].dot(errors_[j]);
2345 }
2346 b(i, m) = b(m, i) = -1.0;
2347 }
2348 // Scale the error overlaps to order one: near convergence they are
2349 // ~1e-12 next to the -1 constraint entries, and a rank-revealing solver
2350 // would treat the whole block as zero (the extrapolation then stalls).
2351 // The scale only changes the Lagrange multiplier, not the coefficients.
2352 const double scale = b.topLeftCorner(m, m).diagonal().maxCoeff();
2353 if (scale > 0.0) {
2354 b.topLeftCorner(m, m) /= scale;
2355 }
2356 Eigen::VectorXd rhs = Eigen::VectorXd::Zero(m + 1);
2357 rhs(m) = -1.0;
2358 const Eigen::VectorXd x = b.colPivHouseholderQr().solve(rhs);
2359 if (!x.allFinite()) {
2360 return fock;
2361 }
2362 Eigen::VectorXd result = Eigen::VectorXd::Zero(fock.size());
2363 for (Index i = 0; i < m; ++i) {
2364 result += x(i) * focks_[i];
2365 }
2366 return result;
2367 }
2368
2369 private:
2370 Index maxhist_;
2371 std::vector<Eigen::VectorXd> focks_;
2372 std::vector<Eigen::VectorXd> errors_;
2373};
2374
2375// Valence subshells of a neutral atom for the spherical atom SCF, as
2376// {l, k, alpha electrons, beta electrons}, k counting the subshells of that
2377// l from the lowest valence one. Subshells are filled by the Madelung (n+l,
2378// then n) rule; the ncore electrons an ECP replaces are taken from the
2379// lowest subshells in (n, l) order (def2: 28 = 1s-3d, 46, 60 = up to 4f,
2380// ...). Every subshell holds alpha and beta electrons equally except that
2381// alpha - beta = nalpha - nbeta is put into the open subshells, highest
2382// first. Empty if the ECP core does not end at a subshell boundary or the
2383// spin difference does not fit.
2384std::vector<std::array<double, 4>> ValenceConfiguration(Index z, Index ncore,
2385 Index nalpha,
2386 Index nbeta) {
2387 struct Sub {
2388 Index n;
2389 Index l;
2390 double electrons;
2391 };
2392 std::vector<Sub> subs;
2393 Index left = z;
2394 for (Index sum = 1; left > 0; ++sum) {
2395 for (Index l = (sum - 1) / 2; l >= 0 && left > 0; --l) {
2396 const Index take = std::min(left, 2 * (2 * l + 1));
2397 subs.push_back({sum - l, l, double(take)});
2398 left -= take;
2399 }
2400 }
2401 std::stable_sort(subs.begin(), subs.end(), [](const Sub& a, const Sub& b) {
2402 return a.n < b.n || (a.n == b.n && a.l < b.l);
2403 });
2404 Index removed = 0;
2405 std::size_t first = 0;
2406 while (first < subs.size() && removed < ncore) {
2407 removed += Index(subs[first].electrons);
2408 ++first;
2409 }
2410 if (removed != ncore) {
2411 return {};
2412 }
2413 std::vector<std::array<double, 4>> result;
2414 for (std::size_t i = first; i < subs.size(); ++i) {
2415 Index k = 0;
2416 for (std::size_t j = first; j < i; ++j) {
2417 k += (subs[j].l == subs[i].l) ? 1 : 0;
2418 }
2419 result.push_back({double(subs[i].l), double(k), 0.5 * subs[i].electrons,
2420 0.5 * subs[i].electrons});
2421 }
2422 // spin polarisation into the open subshells, last filled first
2423 double excess = 0.5 * double(nalpha - nbeta);
2424 for (auto it = result.rbegin(); it != result.rend() && excess > 0.0; ++it) {
2425 const double capacity = double(2 * Index((*it)[0]) + 1);
2426 const double move = std::min(excess, capacity - (*it)[2]);
2427 if (move > 0.0 && (*it)[3] >= move) {
2428 (*it)[2] += move;
2429 (*it)[3] -= move;
2430 excess -= move;
2431 }
2432 }
2433 if (excess > 1e-12) {
2434 return {};
2435 }
2436 return result;
2437}
2438
2439// Hund's-rule ground-state (alpha electrons, beta electrons) for the
2440// main-group (s/p-block) elements most relevant to organic systems --
2441// H through Kr, plus the heavier halogens (Br, I) via their own,
2442// separately-computed period-5 entries. Explicitly does NOT cover
2443// d-block (Sc-Zn, Y-Cd) or f-block elements: the d^n s^2 vs d^(n+1) s^1
2444// (and worse, f-block) ground-state competition is genuinely subtle
2445// and functional-dependent -- exactly why CP2K's own isolated-atom
2446// ("ATOM") program requires explicit, manual per-subshell occupation
2447// specification rather than trusting any automatic rule (confirmed
2448// directly: HORTON's own CP2K pro-atom documentation states "The ATOM
2449// program of CP2K does not simply follow the Aufbau rule to assign
2450// orbital occupations"). Returns std::nullopt for anything not
2451// explicitly covered, so callers can fall back to the existing,
2452// simpler parity-based logic with a clear warning rather than silently
2453// guessing.
2454//
2455// Method: standard Aufbau filling order (1s,2s,2p,3s,3p,4s,3d,4p,5s,
2456// 4d,5p) up to (but explicitly skipping) each d-block range, applying
2457// Hund's rule within any open p subshell (spread across all 3 p
2458// orbitals with parallel/majority spin first, only pairing once every
2459// orbital in that subshell already has one) -- for p^n, n<=3 gives n
2460// alpha/0 beta in that subshell; n>3 gives 3 alpha/(n-3) beta. Every
2461// entry below was computed by hand from this rule and can be checked
2462// against any standard table of atomic ground-state term symbols
2463// (all are unambiguous, textbook Hund's-rule cases for main-group
2464// atoms -- no functional-dependent ambiguity of the kind that affects
2465// d/f-block).
2466std::optional<std::pair<Index, Index>> HundsRuleAlphaBetaElectrons(
2467 Index nuclear_charge) {
2468 switch (nuclear_charge) {
2469 case 1:
2470 return std::make_pair(1, 0); // H: 1s1
2471 case 2:
2472 return std::make_pair(1, 1); // He: 1s2
2473 case 3:
2474 return std::make_pair(2, 1); // Li: [He] 2s1
2475 case 4:
2476 return std::make_pair(2, 2); // Be: 2s2
2477 case 5:
2478 return std::make_pair(3, 2); // B: 2p1
2479 case 6:
2480 return std::make_pair(4, 2); // C: 2p2 (2a)
2481 case 7:
2482 return std::make_pair(5, 2); // N: 2p3 (3a)
2483 case 8:
2484 return std::make_pair(5, 3); // O: 2p4 (3a+1b)
2485 case 9:
2486 return std::make_pair(5, 4); // F: 2p5 (3a+2b)
2487 case 10:
2488 return std::make_pair(5, 5); // Ne: 2p6
2489 case 11:
2490 return std::make_pair(6, 5); // Na: [Ne] 3s1
2491 case 12:
2492 return std::make_pair(6, 6); // Mg: 3s2
2493 case 13:
2494 return std::make_pair(7, 6); // Al: 3p1
2495 case 14:
2496 return std::make_pair(8, 6); // Si: 3p2 (2a)
2497 case 15:
2498 return std::make_pair(9, 6); // P: 3p3 (3a)
2499 case 16:
2500 return std::make_pair(9, 7); // S: 3p4 (3a+1b)
2501 case 17:
2502 return std::make_pair(9, 8); // Cl: 3p5 (3a+2b)
2503 case 18:
2504 return std::make_pair(9, 9); // Ar: 3p6
2505 case 19:
2506 return std::make_pair(10, 9); // K: [Ar] 4s1
2507 case 20:
2508 return std::make_pair(10, 10); // Ca: 4s2
2509 // 21-30 (Sc-Zn): 3d block -- deliberately NOT covered.
2510 case 31:
2511 return std::make_pair(16, 15); // Ga: [Zn] 4p1
2512 case 32:
2513 return std::make_pair(17, 15); // Ge: 4p2 (2a)
2514 case 33:
2515 return std::make_pair(18, 15); // As: 4p3 (3a)
2516 case 34:
2517 return std::make_pair(18, 16); // Se: 4p4 (3a+1b)
2518 case 35:
2519 return std::make_pair(18, 17); // Br: 4p5 (3a+2b)
2520 case 36:
2521 return std::make_pair(18, 18); // Kr: 4p6
2522 // 39-48 (Y-Cd): 4d block -- deliberately NOT covered.
2523 case 49:
2524 return std::make_pair(25, 24); // In: [Cd] 5p1
2525 case 50:
2526 return std::make_pair(26, 24); // Sn: 5p2 (2a)
2527 case 51:
2528 return std::make_pair(27, 24); // Sb: 5p3 (3a)
2529 case 52:
2530 return std::make_pair(27, 25); // Te: 5p4 (3a+1b)
2531 case 53:
2532 return std::make_pair(27, 26); // I: 5p5 (3a+2b)
2533 case 54:
2534 return std::make_pair(27, 27); // Xe: 5p6
2535 default:
2536 return std::nullopt;
2537 }
2538}
2539} // namespace
2540
2542 const QMAtom& uniqueAtom, bool use_hunds_rule_occupation) const {
2543 bool with_ecp = !ecp_name_.empty();
2544 if (uniqueAtom.getElement() == "H" || uniqueAtom.getElement() == "He") {
2545 with_ecp = false;
2546 }
2547
2548 QMMolecule atom = QMMolecule("individual_atom", 0);
2549 atom.push_back(uniqueAtom);
2550
2551 BasisSet basisset;
2552 basisset.Load(dftbasis_name_);
2553 AOBasis dftbasis;
2554 dftbasis.Fill(basisset, atom);
2555 Vxc_Grid grid;
2556 grid.GridSetup(grid_name_, atom, dftbasis);
2557 Vxc_Potential<Vxc_Grid> gridIntegration(grid);
2558 gridIntegration.setXCfunctional(xc_functional_name_);
2559
2560 ECPAOBasis ecp;
2561 if (with_ecp) {
2562 ECPBasisSet ecps;
2563 ecps.Load(ecp_name_);
2564 ecp.Fill(ecps, atom);
2565 }
2566
2567 // Electrons of the neutral atom that the basis describes: without the
2568 // core an ECP replaces (ecp.Fill sets it on the atom in `atom`, not on
2569 // uniqueAtom, which therefore must not be used here).
2570 const Index z = uniqueAtom.getElementNumber();
2571 const Index ncore = z - atom[0].getNuccharge();
2572 const Index numofelectrons = atom[0].getNuccharge();
2573 Index alpha_e = 0;
2574 Index beta_e = 0;
2575
2576 // Total alpha/beta split. The SAD guess (AtomicGuess) uses the
2577 // parity-based split; the Hirshfeld reference densities of CDFT ask for
2578 // the Hund's-rule ground state (use_hunds_rule_occupation). Either way
2579 // the atom is spherical and its open subshells fractionally occupied.
2580 if (use_hunds_rule_occupation) {
2581 auto hunds_rule = HundsRuleAlphaBetaElectrons(z);
2582 if (hunds_rule.has_value()) {
2583 // the ECP core is closed-shell
2584 alpha_e = hunds_rule->first - ncore / 2;
2585 beta_e = hunds_rule->second - ncore / 2;
2586 } else {
2588 << TimeStamp()
2589 << " No Hund's-rule ground-state occupation table "
2590 "entry for nuclear charge "
2591 << z
2592 << " (d/f-block elements are not covered -- see "
2593 "HundsRuleAlphaBetaElectrons's own comment for why) -- "
2594 "falling back to the simpler, parity-based alpha/beta split."
2595 << std::flush;
2596 use_hunds_rule_occupation = false;
2597 }
2598 }
2599 if (!use_hunds_rule_occupation) {
2600 if ((numofelectrons % 2) != 0) {
2601 alpha_e = numofelectrons / 2 + numofelectrons % 2;
2602 beta_e = numofelectrons / 2;
2603 } else {
2604 alpha_e = numofelectrons / 2;
2605 beta_e = alpha_e;
2606 }
2607 }
2608
2609 AOOverlap dftAOoverlap;
2610 AOKinetic dftAOkinetic;
2611 AOMultipole dftAOESP;
2612 AOECP dftAOECP;
2613 ERIs ERIs_atom;
2614
2615 dftAOoverlap.Fill(dftbasis);
2616 dftAOkinetic.Fill(dftbasis);
2617
2618 dftAOESP.FillPotential(dftbasis, atom);
2619 ERIs_atom.Initialize_4c(dftbasis);
2620
2621 Eigen::MatrixXd H0 = dftAOkinetic.Matrix() + dftAOESP.Matrix();
2622 if (with_ecp) {
2623 dftAOECP.FillPotential(dftbasis, ecp);
2624 H0 += dftAOECP.Matrix();
2625 }
2626
2627 if (uniqueAtom.getElement() == "H") {
2628 // One electron: the lowest orbital of H0, as before.
2629 Eigen::GeneralizedSelfAdjointEigenSolver<Eigen::MatrixXd> es(
2630 H0, dftAOoverlap.Matrix());
2631 const Eigen::VectorXd c = es.eigenvectors().col(0);
2632 return c * c.transpose();
2633 }
2634
2635 // Spherically averaged SCF with fractional occupation of open shells
2636 // (see SphericalAtomSCF).
2637 const SphericalAtomSCF sph(dftbasis);
2638 const Index ngroups = sph.Groups();
2639 using Radial = std::vector<Eigen::MatrixXd>;
2640 Radial S_red(ngroups);
2641 Radial H0_red(ngroups);
2642 // S^-1/2 of each group, to measure the error in an orthonormal basis
2643 Radial X_red(ngroups);
2644 for (Index g = 0; g < ngroups; ++g) {
2645 S_red[g] = sph.Reduce(dftAOoverlap.Matrix(), g);
2646 H0_red[g] = sph.Reduce(H0, g);
2647 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(S_red[g]);
2648 X_red[g] = es.eigenvectors() *
2649 es.eigenvalues().cwiseInverse().cwiseSqrt().asDiagonal() *
2650 es.eigenvectors().transpose();
2651 }
2652
2653 // Energy and averaged Fock matrices of the spin densities (radial)
2654 struct State {
2655 Radial Da, Db, Fa, Fb;
2656 double energy = 0.0;
2657 };
2658 // from the functional, not ScaHFX_: that member is only set in SetupVxc,
2659 // and this function is also called without it (Hirshfeld references, tests)
2660 const double scahfx =
2662 auto Build = [&](const Radial& Da, const Radial& Db) {
2663 const Eigen::MatrixXd D_alpha = sph.Expand(Da);
2664 const Eigen::MatrixXd D_beta = sph.Expand(Db);
2665 const Eigen::MatrixXd D_total = D_alpha + D_beta;
2666 Eigen::MatrixXd H_alpha = H0;
2667 Eigen::MatrixXd H_beta = H0;
2668 double e_two = 0.0;
2669 if (scahfx > 0) {
2670 std::array<Eigen::MatrixXd, 2> both_alpha =
2671 ERIs_atom.CalculateERIs_EXX_4c(D_alpha, 1e-12);
2672 std::array<Eigen::MatrixXd, 2> both_beta =
2673 ERIs_atom.CalculateERIs_EXX_4c(D_beta, 1e-12);
2674 const Eigen::MatrixXd hartree = both_alpha[0] + both_beta[0];
2675 H_alpha += hartree + scahfx * both_alpha[1];
2676 H_beta += hartree + scahfx * both_beta[1];
2677 e_two = 0.5 * D_total.cwiseProduct(hartree).sum() +
2678 0.5 * scahfx *
2679 (both_alpha[1].cwiseProduct(D_alpha).sum() +
2680 both_beta[1].cwiseProduct(D_beta).sum());
2681 } else {
2682 const Eigen::MatrixXd hartree =
2683 ERIs_atom.CalculateERIs_4c(D_total, 1e-12);
2684 H_alpha += hartree;
2685 H_beta += hartree;
2686 e_two = 0.5 * D_total.cwiseProduct(hartree).sum();
2687 }
2688 const auto vxc = gridIntegration.IntegrateVXCSpin(D_alpha, D_beta);
2689 H_alpha += vxc.vxc_alpha;
2690 H_beta += vxc.vxc_beta;
2691 State st;
2692 st.Da = Da;
2693 st.Db = Db;
2694 st.energy = D_total.cwiseProduct(H0).sum() + e_two + vxc.energy;
2695 st.Fa.resize(ngroups);
2696 st.Fb.resize(ngroups);
2697 for (Index g = 0; g < ngroups; ++g) {
2698 st.Fa[g] = sph.Reduce(H_alpha, g);
2699 st.Fb[g] = sph.Reduce(H_beta, g);
2700 }
2701 return st;
2702 };
2703 // Fixed occupation of the valence subshells (Madelung configuration); an
2704 // aufbau occupation that may change between iterations only if there is
2705 // none for this atom and basis.
2706 std::vector<Eigen::VectorXd> occ_alpha;
2707 std::vector<Eigen::VectorXd> occ_beta;
2708 bool fixed_occupation = false;
2709 {
2710 const std::vector<std::array<double, 4>> config =
2711 ValenceConfiguration(z, ncore, alpha_e, beta_e);
2712 std::vector<std::array<double, 3>> alpha_subshells;
2713 std::vector<std::array<double, 3>> beta_subshells;
2714 for (const auto& sub : config) {
2715 alpha_subshells.push_back({sub[0], sub[1], sub[2]});
2716 beta_subshells.push_back({sub[0], sub[1], sub[3]});
2717 }
2718 fixed_occupation =
2719 !config.empty() &&
2720 sph.OccupationFromSubshells(alpha_subshells, S_red, occ_alpha) &&
2721 sph.OccupationFromSubshells(beta_subshells, S_red, occ_beta);
2722 if (!fixed_occupation) {
2724 << TimeStamp() << " No subshell configuration for "
2725 << uniqueAtom.getElement()
2726 << " in this basis/ECP; atomic SCF uses aufbau occupation"
2727 << std::flush;
2728 }
2729 }
2730 auto Occupy = [&](const Radial& F, bool alpha) {
2731 if (fixed_occupation) {
2732 return sph.Density(F, S_red, alpha ? occ_alpha : occ_beta);
2733 }
2734 return sph.Density(
2735 F, S_red,
2736 sph.AufbauOccupation(F, S_red, double(alpha ? alpha_e : beta_e)));
2737 };
2738 // flattened Fock matrices and commutator errors F D S - S D F
2739 auto Flatten = [&](const State& st, Eigen::VectorXd& fock,
2740 Eigen::VectorXd& error) {
2741 Index total = 0;
2742 for (Index g = 0; g < ngroups; ++g) {
2743 total += 2 * st.Fa[g].size();
2744 }
2745 fock.resize(total);
2746 error.resize(total);
2747 Index pos = 0;
2748 for (Index spin = 0; spin < 2; ++spin) {
2749 const Radial& F = (spin == 0) ? st.Fa : st.Fb;
2750 const Radial& D = (spin == 0) ? st.Da : st.Db;
2751 for (Index g = 0; g < ngroups; ++g) {
2752 const Eigen::MatrixXd fds = F[g] * D[g] * S_red[g];
2753 const Eigen::MatrixXd err =
2754 X_red[g] * (fds - fds.transpose()) * X_red[g];
2755 const Index size = F[g].size();
2756 fock.segment(pos, size) =
2757 Eigen::Map<const Eigen::VectorXd>(F[g].data(), size);
2758 error.segment(pos, size) =
2759 Eigen::Map<const Eigen::VectorXd>(err.data(), size);
2760 pos += size;
2761 }
2762 }
2763 };
2764 auto Unflatten = [&](const Eigen::VectorXd& fock, Radial& Fa, Radial& Fb) {
2765 Index pos = 0;
2766 for (Index spin = 0; spin < 2; ++spin) {
2767 Radial& F = (spin == 0) ? Fa : Fb;
2768 for (Index g = 0; g < ngroups; ++g) {
2769 const Index rows = S_red[g].rows();
2770 F[g] = Eigen::Map<const Eigen::MatrixXd>(fock.data() + pos, rows, rows);
2771 pos += rows * rows;
2772 }
2773 }
2774 };
2775
2776 // Pulay DIIS on the averaged Fock matrices. With the occupation of each
2777 // subshell fixed, the density is a smooth function of the Fock matrix and
2778 // DIIS converges in about 5-15 iterations for H-Kr (including the 3d
2779 // metals, where aufbau occupation flips between 4s and 3d).
2780 State cur = Build(Occupy(H0_red, true), Occupy(H0_red, false));
2781 SimpleDIIS diis(8);
2782 const Index maxiter = 100;
2783 // The molecule's tolerances, but not tighter than the atom needs as a
2784 // starting density: 1e-7 is at the noise floor of the atomic integrals and
2785 // grid for the 3d metals, where DIIS then stalls.
2786 const double error_tolerance = std::max(conv_opt_.error_converged, 1e-6);
2787 const double energy_tolerance = std::max(conv_opt_.Econverged, 1e-8);
2788 double energy_old = cur.energy;
2789 bool converged = false;
2790 Index this_iter = 0;
2791 for (; this_iter < maxiter; this_iter++) {
2792 Eigen::VectorXd fock;
2793 Eigen::VectorXd error;
2794 Flatten(cur, fock, error);
2795 const double max_error = error.cwiseAbs().maxCoeff();
2797 << TimeStamp() << " Iter " << this_iter << " of " << maxiter << " Etot "
2798 << std::setprecision(12) << cur.energy << " error " << max_error
2799 << std::flush;
2800 if (this_iter > 0 && max_error < error_tolerance &&
2801 std::abs(cur.energy - energy_old) < energy_tolerance) {
2802 converged = true;
2803 break;
2804 }
2805 energy_old = cur.energy;
2806 Radial Fa(ngroups);
2807 Radial Fb(ngroups);
2808 Unflatten(diis.Extrapolate(fock, error), Fa, Fb);
2809 cur = Build(Occupy(Fa, true), Occupy(Fb, false));
2810 }
2811 if (converged) {
2813 << TimeStamp() << " Converged after " << this_iter + 1
2814 << " iterations, Etot=" << std::setprecision(12) << cur.energy
2815 << std::flush;
2816 } else {
2818 << TimeStamp() << " Not converged after " << maxiter
2819 << " iterations. Unconverged density." << std::flush;
2820 }
2821 const Radial& Da = cur.Da;
2822 const Radial& Db = cur.Db;
2823
2824 const Eigen::MatrixXd density = sph.Expand(Da) + sph.Expand(Db);
2826 << TimeStamp() << " Atomic density Matrix for " << uniqueAtom.getElement()
2827 << " gives N=" << std::setprecision(9)
2828 << density.cwiseProduct(dftAOoverlap.Matrix()).sum() << " electrons."
2829 << std::flush;
2830 return density;
2831}
2832
2833Eigen::MatrixXd DFTEngine::AtomicGuess(const QMMolecule& mol) const {
2834
2835 std::vector<std::string> elements = mol.FindUniqueElements();
2837 << TimeStamp() << " Scanning molecule of size " << mol.size()
2838 << " for unique elements" << std::flush;
2839 QMMolecule uniqueelements = QMMolecule("uniqueelements", 0);
2840 for (auto element : elements) {
2841 uniqueelements.push_back(QMAtom(0, element, Eigen::Vector3d::Zero()));
2842 }
2843
2844 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " " << uniqueelements.size()
2845 << " unique elements found" << std::flush;
2846 std::vector<Eigen::MatrixXd> uniqueatom_guesses;
2847 for (QMAtom& unique_atom : uniqueelements) {
2849 << TimeStamp() << " Calculating atom density for "
2850 << unique_atom.getElement() << std::flush;
2851 Eigen::MatrixXd dmat_unrestricted = RunAtomicDFT_unrestricted(unique_atom);
2852 uniqueatom_guesses.push_back(dmat_unrestricted);
2853 }
2854
2855 Eigen::MatrixXd guess =
2856 Eigen::MatrixXd::Zero(dftbasis_.AOBasisSize(), dftbasis_.AOBasisSize());
2857 Index start = 0;
2858 for (const QMAtom& atom : mol) {
2859 Index index = 0;
2860 for (; index < uniqueelements.size(); index++) {
2861 if (atom.getElement() == uniqueelements[index].getElement()) {
2862 break;
2863 }
2864 }
2865 Eigen::MatrixXd& dmat_unrestricted = uniqueatom_guesses[index];
2866 guess.block(start, start, dmat_unrestricted.rows(),
2867 dmat_unrestricted.cols()) = dmat_unrestricted;
2868 start += dmat_unrestricted.rows();
2869 }
2870
2871 return guess;
2872}
2873
2874std::map<std::string, Eigen::MatrixXd>
2876 std::vector<std::string> elements = mol.FindUniqueElements();
2878 << TimeStamp() << " Scanning molecule of size " << mol.size()
2879 << " for unique elements (Hirshfeld reference densities)" << std::flush;
2880
2881 std::map<std::string, Eigen::MatrixXd> reference_densities;
2882 for (const std::string& element : elements) {
2883 QMAtom unique_atom(0, element, Eigen::Vector3d::Zero());
2885 << TimeStamp() << " Calculating Hirshfeld reference density for "
2886 << element << std::flush;
2887 // use_hunds_rule_occupation=true unconditionally here -- this is
2888 // the one and only caller that should ever request it; AtomicGuess
2889 // just above, the pre-existing SAD-guess caller, never does.
2890 reference_densities[element] = RunAtomicDFT_unrestricted(
2891 unique_atom, /*use_hunds_rule_occupation=*/true);
2892 }
2893 return reference_densities;
2894}
2895
2897 const QMMolecule& mol, const CDFTConstraintSpec& spec) const {
2898 std::map<std::string, Eigen::MatrixXd> reference_densities =
2900
2901 AOBasis full_dftbasis;
2902 {
2903 BasisSet basisset;
2904 basisset.Load(dftbasis_name_);
2905 full_dftbasis.Fill(basisset, mol);
2906 }
2907
2908 Vxc_Grid grid;
2909 grid.GridSetup(grid_name_, mol, full_dftbasis);
2910
2911 std::vector<HirshfeldPartition::AtomicReference> atoms =
2913 reference_densities);
2914
2916 constraint.weight_matrix = Eigen::MatrixXd::Zero(full_dftbasis.AOBasisSize(),
2917 full_dftbasis.AOBasisSize());
2918 double neutral_reference_population = 0.0;
2919 for (Index atom_index : spec.atom_indices) {
2920 if (atom_index < 0 || atom_index >= static_cast<Index>(mol.size())) {
2921 throw std::runtime_error(
2922 "BuildCDFTConstraint: cdft.indices contains atom index " +
2923 std::to_string(atom_index) + ", but this molecule only has " +
2924 std::to_string(mol.size()) +
2925 " atoms (0-based indexing -- valid range is 0.." +
2926 std::to_string(mol.size() - 1) + ").");
2927 }
2928 // Hirshfeld weights are additive across atoms in a fragment --
2929 // w_fragment(r) = sum_{i in fragment} w_i(r) -- so the fragment's
2930 // own weight matrix is just the sum of each atom's own
2931 // BuildWeightMatrix result, and the neutral reference population
2932 // (needed to convert the options file's charge-relative target
2933 // into RunCDFT's own absolute-population convention) is just the
2934 // sum of the fragment atoms' own nuclear charges.
2936 atoms, atom_index, full_dftbasis, grid);
2937 neutral_reference_population +=
2938 static_cast<double>(mol[atom_index].getNuccharge());
2939 }
2940
2941 constraint.target_population =
2942 neutral_reference_population - spec.target_charge;
2943 constraint.lambda = spec.initial_lambda;
2944 constraint.spin_alpha_coefficient = 1.0;
2945 constraint.spin_beta_coefficient = 1.0;
2946
2948 << TimeStamp() << " CDFT constraint: " << spec.atom_indices.size()
2949 << " atom(s), neutral reference population="
2950 << neutral_reference_population
2951 << ", requested relative charge=" << spec.target_charge
2952 << ", absolute target population=" << constraint.target_population
2953 << std::flush;
2954
2955 return constraint;
2956}
2957
2959 // A warm start was checked in UsableAsWarmStart, against the basis the MOs
2960 // were computed in.
2961 if (initial_guess_ == "orbfile" && !warm_started_) {
2962
2963 if (orb.hasDFTbasisName()) {
2964 if (orb.getDFTbasisName() != dftbasis_name_) {
2965 throw std::runtime_error(
2966 (boost::format("Basisset Name in guess orb file "
2967 "and in dftengine option file differ %1% vs %2%") %
2969 .str());
2970 }
2971 } else {
2973 << TimeStamp()
2974 << " WARNING: "
2975 "Orbital file has no basisset information,"
2976 "using it as a guess might work or not for calculation with "
2977 << dftbasis_name_ << std::flush;
2978 }
2979 }
2980
2981 const Index target_charge = orb.getCharge();
2982 const Index multiplicity = orb.getSpin();
2983
2984 orb.setChargeAndSpin(target_charge, multiplicity);
2987
2989 orb.setXCGrid(grid_name_);
2990 orb.setScaHFX(ScaHFX_);
2991 if (!ecp_name_.empty()) {
2992 orb.setECPName(ecp_name_);
2993 }
2994 if (!auxbasis_name_.empty()) {
2996 }
2997
2998 if (initial_guess_ == "orbfile") {
2999 if (orb.hasECPName() || !ecp_name_.empty()) {
3000 if (orb.getECPName() != ecp_name_) {
3001 throw std::runtime_error(
3002 (boost::format("ECPs in orb file: %1% and options %2% differ") %
3003 orb.getECPName() % ecp_name_)
3004 .str());
3005 }
3006 }
3009 throw std::runtime_error(
3010 (boost::format("Number of electrons in guess orb file "
3011 "and in dftengine differ: "
3012 "alpha %1% vs %2%, beta %3% vs %4%.") %
3015 .str());
3016 }
3017 if (orb.getBasisSetSize() != dftbasis_.AOBasisSize()) {
3018 throw std::runtime_error(
3019 (boost::format("Number of levels in guess orb file: "
3020 "%1% and in dftengine: %2% differ.") %
3021 orb.getBasisSetSize() % dftbasis_.AOBasisSize())
3022 .str());
3023 }
3024 } else {
3027 }
3028 return;
3029}
3030
3031void DFTEngine::Prepare(Orbitals& orb, Index numofelectrons) {
3032 QMMolecule& mol = orb.QMAtoms();
3033
3035 << TimeStamp() << " Using " << OPENMP::getMaxThreads() << " threads"
3036 << std::flush;
3037
3038 if (XTP_HAS_MKL_OVERLOAD()) {
3040 << TimeStamp() << " Using MKL overload for Eigen " << std::flush;
3041 } else {
3043 << TimeStamp()
3044 << " Using native Eigen implementation, no BLAS overload "
3045 << std::flush;
3046 }
3047
3048 XTP_LOG(Log::error, *pLog_) << " Molecule Coordinates [A] " << std::flush;
3049 for (const QMAtom& atom : mol) {
3050 const Eigen::Vector3d pos = atom.getPos() * tools::conv::bohr2ang;
3051 std::string output = (boost::format(" %1$s"
3052 " %2$+1.4f %3$+1.4f %4$+1.4f") %
3053 atom.getElement() % pos[0] % pos[1] % pos[2])
3054 .str();
3055
3056 XTP_LOG(Log::error, *pLog_) << output << std::flush;
3057 }
3058
3060 dftbasis_ = orb.getDftBasis();
3061
3063 << TimeStamp() << " Loaded DFT Basis Set " << dftbasis_name_ << " with "
3064 << dftbasis_.AOBasisSize() << " functions" << std::flush;
3065
3066 if (!auxbasis_name_.empty()) {
3067 BasisSet auxbasisset;
3068 auxbasisset.Load(auxbasis_name_);
3069 auxbasis_.Fill(auxbasisset, mol);
3071 << TimeStamp() << " Loaded AUX Basis Set " << auxbasis_name_ << " with "
3072 << auxbasis_.AOBasisSize() << " functions" << std::flush;
3073 }
3074 if (!ecp_name_.empty()) {
3075 ECPBasisSet ecpbasisset;
3076 ecpbasisset.Load(ecp_name_);
3078 << TimeStamp() << " Loaded ECP library " << ecp_name_ << std::flush;
3079
3080 std::vector<std::string> results = ecp_.Fill(ecpbasisset, mol);
3082 << TimeStamp() << " Filled ECP Basis" << std::flush;
3083 if (results.size() > 0) {
3084 std::string message = "";
3085 for (const std::string& element : results) {
3086 message += " " + element;
3087 }
3089 << TimeStamp() << " Found no ECPs for elements" << message
3090 << std::flush;
3091 }
3092 }
3093
3094 numofelectrons_ = 0;
3097 num_docc_ = 0;
3098 num_socc_alpha_ = 0;
3099
3100 Index nuclear_charge = 0;
3101 for (const QMAtom& atom : mol) {
3102 nuclear_charge += atom.getNuccharge();
3103 }
3104
3105 Index target_charge = orb.getCharge();
3106 Index multiplicity = orb.getSpin();
3107
3108 if (multiplicity < 1) {
3109 throw std::runtime_error("Spin multiplicity must be >= 1.");
3110 }
3111
3112 if (numofelectrons >= 0) {
3113 numofelectrons_ = numofelectrons;
3114 } else {
3115 numofelectrons_ = nuclear_charge - target_charge;
3116 }
3117
3118 Index spin_excess = multiplicity - 1;
3119
3120 if (numofelectrons_ < 0) {
3121 throw std::runtime_error("Computed a negative number of electrons.");
3122 }
3123
3124 if (spin_excess > numofelectrons_) {
3125 throw std::runtime_error(
3126 "Spin multiplicity incompatible with total number of electrons.");
3127 }
3128
3129 if (((numofelectrons_ + spin_excess) % 2) != 0) {
3130 throw std::runtime_error(
3131 "Charge and spin multiplicity imply non-integer alpha/beta "
3132 "occupations.");
3133 }
3134
3135 num_alpha_electrons_ = (numofelectrons_ + spin_excess) / 2;
3136 num_beta_electrons_ = (numofelectrons_ - spin_excess) / 2;
3137
3140
3142 << TimeStamp() << " Total number of electrons: " << numofelectrons_
3143 << " (charge=" << target_charge << ", multiplicity=" << multiplicity
3144 << ", alpha=" << num_alpha_electrons_ << ", beta=" << num_beta_electrons_
3145 << ", docc=" << num_docc_ << ", socc=" << num_socc_alpha_ << ")"
3146 << std::flush;
3147
3149 return;
3150}
3151
3154 if (ScaHFX_ > 0) {
3156 << TimeStamp() << " Using hybrid functional with alpha=" << ScaHFX_
3157 << std::flush;
3158 }
3159 Vxc_Grid grid;
3160 grid.GridSetup(grid_name_, mol, dftbasis_);
3161 Vxc_Potential<Vxc_Grid> vxc(grid);
3164 << TimeStamp() << " Setup numerical integration grid " << grid_name_
3165 << " for vxc functional " << xc_functional_name_ << std::flush;
3167 << "\t\t " << " with " << grid.getGridSize() << " points"
3168 << " divided into " << grid.getBoxesSize() << " boxes" << std::flush;
3169 return vxc;
3170}
3171
3172double DFTEngine::NuclearRepulsion(const QMMolecule& mol) const {
3173 double E_nucnuc = 0.0;
3174
3175 for (Index i = 0; i < mol.size(); i++) {
3176 const Eigen::Vector3d& r1 = mol[i].getPos();
3177 double charge1 = double(mol[i].getNuccharge());
3178 for (Index j = 0; j < i; j++) {
3179 const Eigen::Vector3d& r2 = mol[j].getPos();
3180 double charge2 = double(mol[j].getNuccharge());
3181 E_nucnuc += charge1 * charge2 / (r1 - r2).norm();
3182 }
3183 }
3184 return E_nucnuc;
3185}
3186
3187// spherically average the density matrix belonging to two shells
3188// Average of an atom's density matrix over all rotations about the nucleus.
3189// In real spherical harmonics a rotation acts on each shell by an orthogonal
3190// matrix that depends only on l, so the average of the block between two
3191// shells is (trace / (2l+1)) times the identity if both have the same l, and
3192// zero otherwise. The result is independent of the orientation of the input
3193// (an open-shell atom's SCF converges to a symmetry-broken solution whose
3194// orientation is decided by round-off) and keeps the number of electrons:
3195// the overlap between shells of one atom is diagonal within equal l and zero
3196// between different l.
3198 const Eigen::MatrixXd& dmat, const AOBasis& dftbasis) const {
3199 Eigen::MatrixXd avdmat = Eigen::MatrixXd::Zero(dmat.rows(), dmat.cols());
3200 for (const AOShell& shellrow : dftbasis) {
3201 for (const AOShell& shellcol : dftbasis) {
3202 if (shellrow.getL() != shellcol.getL()) {
3203 continue;
3204 }
3205 const Index size = shellrow.getNumFunc();
3206 const double diagavg = dmat.block(shellrow.getStartIndex(),
3207 shellcol.getStartIndex(), size, size)
3208 .trace() /
3209 double(size);
3210 avdmat
3211 .block(shellrow.getStartIndex(), shellcol.getStartIndex(), size, size)
3212 .diagonal()
3213 .setConstant(diagavg);
3214 }
3215 }
3216 return avdmat;
3217}
3218
3220 const QMMolecule& mol,
3221 const std::vector<std::unique_ptr<StaticSite>>& multipoles) const {
3222
3223 if (multipoles.size() == 0) {
3224 return 0;
3225 }
3226
3227 double E_ext = 0;
3228 eeInteractor interactor;
3229 for (const QMAtom& atom : mol) {
3230 StaticSite nucleus = StaticSite(atom, double(atom.getNuccharge()));
3231 for (const std::unique_ptr<StaticSite>& site : multipoles) {
3232 if ((site->getPos() - nucleus.getPos()).norm() < 1e-7) {
3234 << " External site sits on nucleus, "
3235 "interaction between them is ignored."
3236 << std::flush;
3237 continue;
3238 }
3239 // The site as the NUCLEI must see it: permanent multipoles PLUS the
3240 // induced dipole.
3241 //
3242 // Why this is not what happens by default. The electronic half of
3243 // this same interaction is built by AOMultipole::FillBlock, which
3244 // reads site_->getDipole() -- a VIRTUAL accessor that PolarSite
3245 // overrides as Q_.segment<3>(1) + induced_dipole_, and it promotes
3246 // rank to 1 on exactly the test repeated below, so the electrons
3247 // see the induced dipoles deliberately. The nuclear half arrives
3248 // here and goes through eeInteractor::CalcStaticEnergy_site, whose
3249 // VSiteA reads the SOURCE's moments as siteB.Q().segment<3>(1) --
3250 // and Q() is not virtual. It returns the raw permanent vector, so
3251 // the nuclei never saw an induced dipole at all.
3252 //
3253 // For a neutral QM region the electronic and nuclear halves very
3254 // nearly cancel, so dropping one of them leaves essentially the
3255 // whole surviving half standing: measured on a methane QM/MM job
3256 // with the permanent multipoles zeroed, the QM energy moved by
3257 // -4.5e-3 Ha between inter-region iterations where the polar
3258 // region's own 1/2 F^T P F was 2.7e-5 Ha, a factor of ~84. With
3259 // zeroed permanent multipoles Q_ is identically zero, which is why
3260 // "Nuclei-external site interaction energy" printed as exactly 0
3261 // while H0 was plainly not.
3262 //
3263 // Fixed HERE rather than in VSiteA, whose use of Q() is correct and
3264 // deliberate: the induction solver contracts permanent and induced
3265 // moments through separate channels, and folding induced dipoles
3266 // into the static one there would double-count them in every polar
3267 // energy in the package. The counterparty here is a bare nucleus,
3268 // so no such channel exists and the sum is unambiguous.
3269 Vector9d Q = site->Q();
3270 Q.segment<3>(1) += site->getInducedDipole(); // zero for a StaticSite
3271 Index rank = site->getRank();
3272 if (rank < 1 && Q.segment<3>(1).norm() > 1e-12) {
3273 rank = 1; // same promotion, same threshold, as AOMultipole
3274 }
3275 StaticSite effective(site->getId(), site->getElement(), site->getPos());
3276 effective.setMultipole(Q, rank);
3277
3278 E_ext += interactor.CalcStaticEnergy_site(effective, nucleus);
3279 }
3280 }
3281 return E_ext;
3282}
3283
3284Eigen::MatrixXd DFTEngine::IntegrateExternalField(const QMMolecule& mol) const {
3285
3286 AODipole dipole;
3287 dipole.setCenter(mol.getPos());
3288 dipole.Fill(dftbasis_);
3289 Eigen::MatrixXd result =
3290 Eigen::MatrixXd::Zero(dipole.Dimension(), dipole.Dimension());
3291 for (Index i = 0; i < 3; i++) {
3292 result -= dipole.Matrix()[i] * extfield_[i];
3293 }
3294 return result;
3295}
3296
3298 const QMMolecule& mol,
3299 const std::vector<std::unique_ptr<StaticSite>>& multipoles) const {
3300
3301 Mat_p_Energy result(dftbasis_.AOBasisSize(), dftbasis_.AOBasisSize());
3302 result.energy() = ExternalRepulsion(mol, multipoles);
3303
3304 if (setup_cache_ == nullptr) {
3305 AOMultipole dftAOESP;
3306 dftAOESP.FillPotential(dftbasis_, multipoles);
3308 << TimeStamp() << " Filled DFT external multipole potential matrix"
3309 << std::flush;
3310 result.matrix() = dftAOESP.Matrix();
3311 return result;
3312 }
3313
3314 // In QM/MM only the induced dipoles change between iterations. The
3315 // potential is linear in the moments, so the permanent part is kept in the
3316 // cache and reused as long as basis, positions and permanent moments are
3317 // exactly the same; the induced dipoles are integrated every run.
3318 Eigen::MatrixXd sites(Index(multipoles.size()), 13);
3319 for (Index i = 0; i < Index(multipoles.size()); ++i) {
3320 const StaticSite& site = *multipoles[i];
3321 sites.block<1, 3>(i, 0) = site.getPos().transpose();
3322 sites(i, 3) = double(site.getRank());
3323 sites.block<1, 9>(i, 4) = site.Q().transpose();
3324 }
3325 const std::string key = RISetupKey();
3326 if (setup_cache_->multipole_key == key &&
3327 setup_cache_->multipole_matrix.rows() == dftbasis_.AOBasisSize() &&
3328 setup_cache_->multipole_sites.rows() == sites.rows() &&
3329 setup_cache_->multipole_sites == sites) {
3330 result.matrix() = setup_cache_->multipole_matrix;
3332 << TimeStamp()
3333 << " Reusing the permanent multipole potential matrix of the previous "
3334 "run"
3335 << std::flush;
3336 } else {
3337 AOMultipole permanent;
3338 permanent.FillPotential(dftbasis_, multipoles,
3340 result.matrix() = permanent.Matrix();
3341 setup_cache_->multipole_key = key;
3342 setup_cache_->multipole_sites = sites;
3343 setup_cache_->multipole_matrix = permanent.Matrix();
3345 << TimeStamp() << " Filled DFT permanent multipole potential matrix"
3346 << std::flush;
3347 }
3348
3349 const bool has_induced =
3350 std::any_of(multipoles.begin(), multipoles.end(),
3351 [](const std::unique_ptr<StaticSite>& site) {
3352 return site->getInducedDipole().norm() > 1e-12;
3353 });
3354 if (has_induced) {
3355 AOMultipole induced;
3357 result.matrix() += induced.Matrix();
3359 << TimeStamp() << " Filled DFT induced dipole potential matrix"
3360 << std::flush;
3361 }
3362
3363 return result;
3364}
3365
3367 const QMMolecule& mol, const Orbitals& extdensity) const {
3368 BasisSet basis;
3369 basis.Load(extdensity.getDFTbasisName());
3370 AOBasis aobasis;
3371 aobasis.Fill(basis, extdensity.QMAtoms());
3372 Vxc_Grid grid;
3373 grid.GridSetup(gridquality_, extdensity.QMAtoms(), aobasis);
3374 DensityIntegration<Vxc_Grid> numint(grid);
3375 Eigen::MatrixXd dmat = extdensity.DensityMatrixFull(state_);
3376
3377 numint.IntegrateDensity(dmat);
3379 << TimeStamp() << " Calculated external density" << std::flush;
3380 Eigen::MatrixXd e_contrib = numint.IntegratePotential(dftbasis_);
3382 << TimeStamp() << " Calculated potential from electron density"
3383 << std::flush;
3384 AOMultipole esp;
3385 esp.FillPotential(dftbasis_, extdensity.QMAtoms());
3386
3387 double nuc_energy = 0.0;
3388 for (const QMAtom& atom : mol) {
3389 nuc_energy +=
3390 numint.IntegratePotential(atom.getPos()) * double(atom.getNuccharge());
3391 for (const QMAtom& extatom : extdensity.QMAtoms()) {
3392 const double dist = (atom.getPos() - extatom.getPos()).norm();
3393 nuc_energy +=
3394 double(atom.getNuccharge()) * double(extatom.getNuccharge()) / dist;
3395 }
3396 }
3398 << TimeStamp() << " Calculated potential from nuclei" << std::flush;
3400 << TimeStamp() << " Electrostatic: " << nuc_energy << std::flush;
3401 return Mat_p_Energy(nuc_energy, e_contrib + esp.Matrix());
3402}
3403
3405 const Eigen::MatrixXd& GuessMOs) const {
3406 Eigen::MatrixXd nonortho =
3407 GuessMOs.transpose() * dftAOoverlap_.Matrix() * GuessMOs;
3408 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(nonortho);
3409 Eigen::MatrixXd result = GuessMOs * es.operatorInverseSqrt();
3410 return result;
3411}
3412
3413/*************************************************************
3414 * Extended Hueckel Theory
3415 ************************************************************/
3417 const QMMolecule& mol) const {
3418
3420
3421 const Index nao = dftbasis_.AOBasisSize();
3422 Eigen::VectorXd eps = Eigen::VectorXd::Zero(nao);
3423
3424 for (const AOShell& shell : dftbasis_) {
3425
3426 int l = static_cast<int>(shell.getL());
3427 Index start = shell.getStartIndex();
3428 Index nfunc = shell.getNumFunc();
3429
3430 const QMAtom& atom = mol[shell.getAtomIndex()];
3431 const std::string& element = atom.getElement();
3432
3433 int used_l = l;
3434 double e = params.GetWithFallback(element, l, &used_l);
3435
3436 for (Index i = 0; i < nfunc; ++i) {
3437 eps(start + i) = e;
3438 }
3439 }
3440
3441 return eps;
3442}
3443
3444Eigen::MatrixXd DFTEngine::BuildEHTHamiltonian(const QMMolecule& mol) const {
3445
3446 const Eigen::MatrixXd& S = dftAOoverlap_.Matrix();
3447 const Index nao = S.rows();
3448 Eigen::VectorXd eps = BuildEHTOrbitalEnergies(mol);
3449 Eigen::MatrixXd H = Eigen::MatrixXd::Zero(nao, nao);
3450 constexpr double K = 1.75;
3451
3452 for (Index mu = 0; mu < nao; ++mu) {
3453 H(mu, mu) = eps(mu);
3454 for (Index nu = 0; nu < mu; ++nu) {
3455 double hij = K * S(mu, nu) * 0.5 * (eps(mu) + eps(nu));
3456 H(mu, nu) = hij;
3457 H(nu, mu) = hij;
3458 }
3459 }
3460
3461 return H;
3462}
3463
3465
3467 << TimeStamp() << " Building Extended Huckel guess" << std::flush;
3468
3469 Eigen::MatrixXd H = BuildEHTHamiltonian(mol);
3470
3472 << TimeStamp() << " Solving EHT generalized eigenproblem" << std::flush;
3473
3474 return conv_accelerator_.SolveFockmatrix(H);
3475}
3476
3478 const Mat_p_Energy& H0, const QMMolecule& mol,
3479 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
3480
3482
3483 Eigen::MatrixXd Dmat = conv_accelerator_.DensityMatrix(eht);
3484
3485 Mat_p_Energy e_vxc = vxcpotential.IntegrateVXC(Dmat);
3486
3487 Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
3488
3489 if (ScaHFX_ > 0) {
3490 std::array<Eigen::MatrixXd, 2> both =
3491 CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dmat, 1e-12);
3492 H += both[0];
3493 H += ScaHFX_ * both[1];
3494 } else {
3495 H += CalcERIs(Dmat, 1e-12);
3496 }
3497
3498 return conv_accelerator_.SolveFockmatrix(H);
3499}
3500
3502 const QMMolecule& dimer_mol) const {
3503 Orbitals monomerA;
3505 Orbitals monomerB;
3507
3508 const QMMolecule& atomsA = monomerA.QMAtoms();
3509 const QMMolecule& atomsB = monomerB.QMAtoms();
3510 Index nA = atomsA.size();
3511 Index nB = atomsB.size();
3512
3513 // --- Sanity check 1: element count and sequence ---
3514 // Deliberately checked BEFORE the geometry check below -- a clear
3515 // "wrong element at index N" error is far more actionable than the
3516 // generic "distance mismatch" the geometry check alone would give if
3517 // the atom ordering itself were wrong.
3518 if (nA + nB != dimer_mol.size()) {
3519 throw std::runtime_error(
3520 "BuildDimerGuessFromMonomerFiles: monomer A (" + std::to_string(nA) +
3521 " atoms) + monomer B (" + std::to_string(nB) +
3522 " atoms) does not equal this calculation's own molecule (" +
3523 std::to_string(dimer_mol.size()) +
3524 " atoms) -- wrong monomer file(s), or this calculation's molecule "
3525 "is not simply the concatenation of these two monomers.");
3526 }
3527 for (Index i = 0; i < nA; ++i) {
3528 if (atomsA[i].getElement() != dimer_mol[i].getElement()) {
3529 throw std::runtime_error(
3530 "BuildDimerGuessFromMonomerFiles: monomer A's own atom " +
3531 std::to_string(i) + " (" + atomsA[i].getElement() +
3532 ") does not match this calculation's own atom " + std::to_string(i) +
3533 " (" + dimer_mol[i].getElement() +
3534 ") -- dimer_guess assumes monomer A occupies exactly the first "
3535 "N_A atoms of this calculation's molecule, in the same order.");
3536 }
3537 }
3538 for (Index i = 0; i < nB; ++i) {
3539 if (atomsB[i].getElement() != dimer_mol[nA + i].getElement()) {
3540 throw std::runtime_error(
3541 "BuildDimerGuessFromMonomerFiles: monomer B's own atom " +
3542 std::to_string(i) + " (" + atomsB[i].getElement() +
3543 ") does not match this calculation's own atom " +
3544 std::to_string(nA + i) + " (" + dimer_mol[nA + i].getElement() +
3545 ") -- dimer_guess assumes monomer B occupies exactly the "
3546 "remaining atoms of this calculation's molecule (after monomer "
3547 "A's own N_A atoms), in the same order.");
3548 }
3549 }
3550
3551 // --- Sanity check 2: internal geometry (translation/rotation
3552 // invariant) ---
3553 // Every pairwise interatomic distance WITHIN a monomer is unchanged
3554 // by rigid translation or rotation of that monomer as a whole --
3555 // exactly the operation that happens between a monomer's own,
3556 // independent optimization and its placement into the dimer. So this
3557 // checks the one thing that SHOULD be identical (internal geometry)
3558 // rather than the one thing that is EXPECTED to differ (absolute
3559 // position/orientation).
3560 constexpr double kGeometryToleranceBohr = 1e-3;
3561 auto CheckInternalGeometry = [&](const QMMolecule& monomer_atoms,
3562 Index offset_in_dimer,
3563 const std::string& label) {
3564 Index n = monomer_atoms.size();
3565 for (Index i = 0; i < n; ++i) {
3566 for (Index j = i + 1; j < n; ++j) {
3567 double monomer_distance =
3568 (monomer_atoms[i].getPos() - monomer_atoms[j].getPos()).norm();
3569 double dimer_distance = (dimer_mol[offset_in_dimer + i].getPos() -
3570 dimer_mol[offset_in_dimer + j].getPos())
3571 .norm();
3572 double diff = std::abs(monomer_distance - dimer_distance);
3573 if (diff > kGeometryToleranceBohr) {
3574 throw std::runtime_error(
3575 "BuildDimerGuessFromMonomerFiles: " + label +
3576 "'s own internal geometry does not match this calculation's "
3577 "molecule -- distance between its own atoms " +
3578 std::to_string(i) + " and " + std::to_string(j) + " is " +
3579 std::to_string(monomer_distance) +
3580 " Bohr in the monomer file, but " +
3581 std::to_string(dimer_distance) +
3582 " Bohr in this calculation's own molecule (difference " +
3583 std::to_string(diff) + " Bohr, tolerance " +
3584 std::to_string(kGeometryToleranceBohr) +
3585 " Bohr). This is checked as an INTERNAL, translation/"
3586 "rotation-invariant distance specifically because the "
3587 "monomer's absolute position/orientation is expected to "
3588 "differ between its own standalone optimization and its "
3589 "placement in the dimer -- only its internal geometry "
3590 "should still match.");
3591 }
3592 }
3593 }
3594 };
3595 CheckInternalGeometry(atomsA, 0, "Monomer A");
3596 CheckInternalGeometry(atomsB, nA, "Monomer B");
3597
3598 // The MO coefficients are copied without rotating them, so the guess is
3599 // only exact if each monomer is translated, not rotated, into the dimer.
3600 auto MaxDeviationFromTranslation = [&](const QMMolecule& monomer_atoms,
3601 Index offset_in_dimer) {
3602 Eigen::Vector3d shift =
3603 dimer_mol[offset_in_dimer].getPos() - monomer_atoms[0].getPos();
3604 double max_dev = 0.0;
3605 for (Index i = 0; i < monomer_atoms.size(); ++i) {
3606 double dev = (dimer_mol[offset_in_dimer + i].getPos() -
3607 monomer_atoms[i].getPos() - shift)
3608 .norm();
3609 max_dev = std::max(max_dev, dev);
3610 }
3611 return max_dev;
3612 };
3613 auto WarnIfRotated = [&](const QMMolecule& monomer_atoms,
3614 Index offset_in_dimer, const std::string& label) {
3615 double dev = MaxDeviationFromTranslation(monomer_atoms, offset_in_dimer);
3616 if (dev > kGeometryToleranceBohr) {
3618 << TimeStamp() << " WARNING: " << label
3619 << " is rotated with respect to its .orb file (max deviation " << dev
3620 << " bohr after translation). Its MO coefficients are not "
3621 "rotated, so the dimer guess will be poor."
3622 << std::flush;
3623 }
3624 };
3625 WarnIfRotated(atomsA, 0, "Monomer A");
3626 WarnIfRotated(atomsB, nA, "Monomer B");
3627
3628 Orbitals dimer_guess;
3629 // PrepareDimerGuess/PrepareDimerGuessMixedSpin both call SetupDftBasis
3630 // internally, which needs this->QMAtoms() already populated -- the
3631 // SAME requirement iqm.cc's own, existing caller of PrepareDimerGuess
3632 // already satisfies (orbitalsAB.QMAtoms() is set there well before its
3633 // own PrepareDimerGuess call), confirmed directly by reading that
3634 // code rather than assumed.
3635 dimer_guess.QMAtoms() = dimer_mol;
3636 dimer_guess.PrepareDimerGuessMixedSpin(monomerA, monomerB);
3637 return dimer_guess;
3638}
3639
3640} // namespace xtp
3641} // namespace votca
const Eigen::VectorXd & eigenvalues() const
Definition eigensystem.h:30
const Eigen::MatrixXd & eigenvectors() const
Definition eigensystem.h:33
class to manage program options with xml serialization functionality
Definition property.h:55
Property & get(const std::string &key)
get existing property
Definition property.cc:79
bool exists(const std::string &key) const
check whether property exists
Definition property.cc:122
T as() const
return value as type
Definition property.h:283
T ifExistsReturnElseReturnDefault(const std::string &key, T defaultvalue) const
Definition property.h:332
Container to hold Basisfunctions for all atoms.
Definition aobasis.h:42
Index AOBasisSize() const
Definition aobasis.h:46
void Fill(const BasisSet &bs, const QMMolecule &atoms)
Definition aobasis.cc:85
Index Dimension() final
Definition aomatrix.h:91
void setCenter(const Eigen::Vector3d &r)
Definition aomatrix.h:94
const std::array< Eigen::MatrixXd, 3 > & Matrix() const
Definition aomatrix.h:92
void Fill(const AOBasis &aobasis) final
void FillPotential(const AOBasis &aobasis, const ECPAOBasis &ecp)
Definition aoecp.cc:59
void Fill(const AOBasis &aobasis) final
const Eigen::MatrixXd & Matrix() const
Definition aomatrix.h:41
void FillPotential(const AOBasis &aobasis, const QMMolecule &atoms)
@ Permanent
permanent moments only (charge, dipole, quadrupole)
Definition aopotential.h:72
@ Induced
induced dipoles only; sites without one are skipped
Definition aopotential.h:73
void Fill(const AOBasis &aobasis) final
const Eigen::MatrixXd & Matrix() const
Definition aomatrix.h:52
const Eigen::Matrix< T, Eigen::Dynamic, Eigen::Dynamic > & Matrix() const
Definition aopotential.h:39
void push_back(const T &atom)
std::vector< std::string > FindUniqueElements() const
const Eigen::Vector3d & getPos() const
void Load(const std::string &name)
Definition basisset.cc:149
Eigen::MatrixXd CalcERIs(const Eigen::MatrixXd &Dmat, double error) const
Build the Coulomb matrix contribution from the current density matrix.
Definition dftengine.cc:887
Eigen::MatrixXd ComputeOverlapPulayGradientUKS(const QMMolecule &mol, const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
Definition dftengine.cc:655
std::string auxbasis_name_
Definition dftengine.h:579
std::string gridquality_
Definition dftengine.h:642
tools::EigenSystem ModelPotentialGuess(const Mat_p_Energy &H0, const QMMolecule &mol, const Vxc_Potential< Vxc_Grid > &vxcpotential) const
Definition dftengine.cc:944
Mat_p_Energy IntegrateExternalMultipoles(const QMMolecule &mol, const std::vector< std::unique_ptr< StaticSite > > &multipoles) const
tools::EigenSystem IndependentElectronGuess(const Mat_p_Energy &H0) const
Generate an initial guess by diagonalizing the core Hamiltonian only.
Definition dftengine.cc:933
void setSCFToleranceFloor(double energy, double error)
std::string eris_key_
Definition dftengine.h:623
Orbitals BuildDimerGuessFromMonomerFiles(const QMMolecule &dimer_mol) const
double cdft_population_tolerance_
Definition dftengine.h:719
void ComputeAndStoreForces(Orbitals &orb, const Eigen::MatrixXd &Dmat, const Vxc_Potential< Vxc_Grid > &vxcpotential) const
Definition dftengine.cc:434
bool EvaluateClosedShell(Orbitals &orb, const Mat_p_Energy &H0, const Vxc_Potential< Vxc_Grid > &vxcpotential)
Run the restricted closed-shell SCF loop and store the converged result.
void PrintMOsUKS(const Eigen::VectorXd &alpha_energies, const Eigen::VectorXd &beta_energies, Log::Level level) const
Print separate alpha and beta orbital energies for a UKS calculation.
Definition dftengine.cc:336
void ReportDimensionsAndMemory() const
Basis dimensions and the memory of the large intermediates.
Definition dftengine.cc:898
std::string dftbasis_name_
Definition dftengine.h:580
DFTSetupCache * setup_cache_
Definition dftengine.h:622
bool EvaluateUKS(Orbitals &orb, const Mat_p_Energy &H0, const Vxc_Potential< Vxc_Grid > &vxcpotential)
Eigen::MatrixXd RunAtomicDFT_unrestricted(const QMAtom &uniqueAtom, bool use_hunds_rule_occupation=false) const
Mat_p_Energy IntegrateExternalDensity(const QMMolecule &mol, const Orbitals &extdensity) const
std::string ecp_name_
Definition dftengine.h:581
std::string grid_name_
Definition dftengine.h:597
void Prepare(Orbitals &orb, Index numofelectrons=-1)
Vxc_Grid external_ewaldgrid_
Definition dftengine.h:668
std::string initial_guess_
Definition dftengine.h:602
std::string orbfilename_
Definition dftengine.h:641
Eigen::MatrixXd AtomicGuess(const QMMolecule &mol) const
Build an atomic-density based initial guess in the AO basis.
bool EvaluateAndTime(Orbitals &orb)
Evaluate() without the timing report.
Definition dftengine.cc:984
Eigen::MatrixXd BuildEHTHamiltonian(const QMMolecule &mol) const
Build the extended-Hückel Hamiltonian for the current molecule.
Eigen::MatrixXd IntegrateExternalField(const QMMolecule &mol) const
Integrate a homogeneous external electric field into the AO basis.
Eigen::Vector3d extfield_
Definition dftengine.h:648
std::array< Eigen::MatrixXd, 2 > CalcERIs_EXX(const Eigen::MatrixXd &MOCoeff, const Eigen::MatrixXd &Dmat, double error) const
Definition dftengine.cc:861
HirshfeldPartition::Constraint BuildCDFTConstraint(const QMMolecule &mol, const CDFTConstraintSpec &spec) const
void Initialize(tools::Property &options)
Read DFT, grid, and SCF settings from the user options tree.
Definition dftengine.cc:103
std::string xc_functional_name_
Definition dftengine.h:637
void CalcElDipole(const Orbitals &orb) const
Evaluate and print the electronic dipole moment from the final density.
Definition dftengine.cc:396
double ExternalRepulsion(const QMMolecule &mol, const std::vector< std::unique_ptr< StaticSite > > &multipoles) const
ConvergenceAcc::options conv_opt_
Definition dftengine.h:613
void SetupInvariantMatrices()
Precompute AO matrices that remain unchanged throughout the SCF procedure.
tools::EigenSystem ExtendedHuckelDFTGuess(const Mat_p_Energy &H0, const QMMolecule &mol, const Vxc_Potential< Vxc_Grid > &vxcpotential) const
std::map< std::string, Eigen::MatrixXd > ComputeHirshfeldReferenceDensities(const QMMolecule &mol) const
std::string active_atoms_as_string_
Definition dftengine.h:651
void ComputeAndStoreForcesUKS(Orbitals &orb, const UKSConvergenceAcc::SpinDensity &Dspin, const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta, const Vxc_Potential< Vxc_Grid > &vxcpotential) const
Definition dftengine.cc:756
void ConfigOrbfile(Orbitals &orb)
Propagate basis-set, XC, and metadata settings into the orbital container.
Eigen::VectorXd BuildEHTOrbitalEnergies(const QMMolecule &mol) const
Build orbital energies used in the extended-Hückel starting guess.
std::vector< HirshfeldPartition::Constraint > constraints_
Definition dftengine.h:708
bool UsableAsWarmStart(const Orbitals &orb, const std::string &previous_basis, std::string &reason) const
void PrintMOs(const Eigen::VectorXd &MOEnergies, Log::Level level)
Print a one-spin list of orbital energies and occupations to the logger.
Definition dftengine.cc:316
AOOverlap dftAOoverlap_
Definition dftengine.h:600
Mat_p_Energy SetupH0(const QMMolecule &mol) const
Assemble the one-electron core Hamiltonian for the current molecule.
tools::EigenSystem ExtendedHuckelGuess(const QMMolecule &mol) const
Eigen::MatrixXd OrthogonalizeGuess(const Eigen::MatrixXd &GuessMOs) const
Orthonormalize an initial MO guess with respect to the AO overlap matrix.
Eigen::MatrixXd SphericalAverageShells(const Eigen::MatrixXd &dmat, const AOBasis &dftbasis) const
bool Evaluate(Orbitals &orb)
Definition dftengine.cc:971
CDFTConstraintSpec cdft_constraint_spec_
Definition dftengine.h:722
Eigen::MatrixXd ComputeNonXCGradientUKS(const QMMolecule &mol, const UKSConvergenceAcc::SpinDensity &Dspin, const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
Definition dftengine.cc:703
bool RunCDFT(Orbitals &orb, HirshfeldPartition::Constraint &constraint)
std::string dimer_guess_orbB_name_
Definition dftengine.h:608
std::vector< std::unique_ptr< StaticSite > > * externalsites_
Definition dftengine.h:633
Vxc_Potential< Vxc_Grid > SetupVxc(const QMMolecule &mol)
std::string RISetupKey() const
double NuclearRepulsion(const QMMolecule &mol) const
Compute the classical nucleus-nucleus repulsion energy.
ConvergenceAcc conv_accelerator_
Definition dftengine.h:615
std::string dimer_guess_orbA_name_
Definition dftengine.h:607
static Eigen::MatrixXd RIKGradient(const Eigen::MatrixXd &occ_mo_coeffs, const AOBasis &auxbasis, const AOBasis &dftbasis)
static Eigen::MatrixXd NuclearRepulsionDerivative(const QMMolecule &mol)
static Eigen::MatrixXd RIJGradient(const Eigen::MatrixXd &density, const AOBasis &auxbasis, const AOBasis &dftbasis)
static double ResidentMemoryGB(bool peak)
Definition dfttimings.cc:57
double IntegratePotential(const Eigen::Vector3d &rvector) const
double IntegrateDensity(const Eigen::MatrixXd &density_matrix)
Container to hold ECPs for all atoms.
Definition ecpaobasis.h:43
std::vector< std::string > Fill(const ECPBasisSet &bs, QMMolecule &atoms)
Definition ecpaobasis.cc:69
void Load(const std::string &name)
Takes a density matrix and and an auxiliary basis set and calculates the electron repulsion integrals...
Definition ERIs.h:35
std::array< Eigen::MatrixXd, 2 > CalculateERIs_EXX_4c(const Eigen::MatrixXd &DMAT, double error) const
Definition ERIs.h:78
void Initialize_4c(const AOBasis &dftbasis)
Definition ERIs.cc:39
Eigen::MatrixXd CalculateERIs_4c(const Eigen::MatrixXd &DMAT, double error) const
Definition ERIs.h:73
Mat_p_Energy IntegrateEwald(Index basissize) const
double GetWithFallback(const std::string &element, int l, int *used_l=nullptr) const
Index size() const
Definition gridbox.h:71
std::vector< double > & getPotentialValues()
Definition gridbox.h:58
static std::vector< AtomicReference > BuildAtomicReferences(const QMMolecule &mol, const std::string &basisset_name, const std::map< std::string, Eigen::MatrixXd > &reference_densities)
static Eigen::MatrixXd BuildWeightMatrix(const std::vector< AtomicReference > &atoms, Index target_atom_index, const AOBasis &full_dftbasis, const Vxc_Grid &grid)
static Eigen::MatrixXd ComputeCDFTForceContribution(const std::vector< AtomicReference > &atoms, Index target_atom_index, const Eigen::MatrixXd &density_matrix, const QMMolecule &mol, const AOBasis &full_dftbasis, const Vxc_Grid &grid)
void UpdateDmats(const Eigen::MatrixXd &dmat, double DiisError, Index Iteration)
void Configure(const Eigen::MatrixXd &dmat)
void resetMatrices(Eigen::MatrixXd &J, Eigen::MatrixXd &K, const Eigen::MatrixXd &dmat)
void Start(Index iteration, double DiisError)
void UpdateCriteria(double DiisError, Index Iteration)
const Eigen::MatrixXd & getDmat_diff() const
std::vector< Index > CreateIndexVector(const std::string &Ids) const
Eigen::MatrixXd & matrix()
Definition eigen.h:80
double & energy()
Definition eigen.h:81
Index cols() const
Definition eigen.h:79
Index rows() const
Definition eigen.h:78
Container for molecular orbitals and derived one-particle data.
Definition orbitals.h:47
void setScaHFX(double ScaHFX)
Store the fraction of exact exchange associated with the functional.
Definition orbitals.h:436
Index getCharge() const
Return the stored total charge.
Definition orbitals.h:254
Index getSpin() const
Return the stored spin multiplicity.
Definition orbitals.h:252
bool hasForces() const
Report whether nuclear forces have been stored.
Definition orbitals.h:301
const tools::EigenSystem & MOs_beta() const
Return read-only access to beta-spin molecular orbitals.
Definition orbitals.h:202
bool hasMOs() const
Report whether alpha/restricted molecular orbitals are available.
Definition orbitals.h:185
std::array< Eigen::MatrixXd, 2 > DensityMatrixGroundStateSpinResolved() const
Definition orbitals.cc:1380
Index getNumberOfAlphaElectrons() const
Return the stored number of alpha electrons.
Definition orbitals.h:159
void setNumberOfAlphaElectrons(Index electrons)
Store the total number of alpha electrons.
Definition orbitals.h:139
Eigen::MatrixXd DensityMatrixFull(const QMState &state) const
Definition orbitals.cc:150
void PrepareDimerGuessMixedSpin(const Orbitals &orbitalsA, const Orbitals &orbitalsB)
Guess for a dimer of two monomers with independently ARBITRARY charge and spin, built by combining ea...
Definition orbitals.cc:1032
void SetupAuxBasis(std::string aux_basis_name)
Definition orbitals.cc:108
void setNumberOfBetaElectrons(Index electrons)
Store the total number of beta electrons.
Definition orbitals.h:144
void setForces(const Eigen::MatrixXd &forces)
Definition orbitals.h:309
void setECPName(const std::string &ECP)
Store the effective core potential label.
Definition orbitals.h:170
void setXCGrid(std::string grid)
Store the numerical XC grid quality label.
Definition orbitals.h:283
void setNumberOfOccupiedLevels(Index occupied_levels)
Definition orbitals.h:122
Index getBasisSetSize() const
Return the number of AO basis functions in the DFT basis.
Definition orbitals.h:72
void setQMEnergy(double qmenergy)
Store the total DFT energy.
Definition orbitals.h:295
bool hasECPName() const
Report whether an effective core potential label has been stored.
Definition orbitals.h:164
const tools::EigenSystem & MOs() const
Return read-only access to alpha/restricted molecular orbitals.
Definition orbitals.h:192
const QMMolecule & QMAtoms() const
Return read-only access to the molecular geometry.
Definition orbitals.h:262
void ReadFromCpt(const std::string &filename)
Read the orbital container from a checkpoint file on disk.
Definition orbitals.cc:1201
void setNumberOfOccupiedLevelsBeta(Index occupied_levels_beta)
Store the number of occupied beta-spin orbitals.
Definition orbitals.h:134
void setChargeAndSpin(Index charge, Index spin)
Definition orbitals.h:246
bool hasDFTbasisName() const
Report whether a DFT basis-set name has been stored.
Definition orbitals.h:313
const std::string & getECPName() const
Return the effective core potential label.
Definition orbitals.h:167
const std::string & getDFTbasisName() const
Return the DFT basis-set name.
Definition orbitals.h:318
bool hasBetaMOs() const
Report whether beta-spin molecular orbitals are available.
Definition orbitals.h:187
void SetupDftBasis(std::string basis_name)
Build and attach the DFT AO basis from the stored molecular geometry.
Definition orbitals.cc:99
const Eigen::MatrixXd & getForces() const
Return the stored nuclear forces (Natoms x 3, Hartree/Bohr).
Definition orbitals.h:304
Eigen::Vector3d CalcElDipole(const QMState &state) const
Compute the electronic dipole moment associated with a state density.
Definition orbitals.cc:288
Index getNumberOfBetaElectrons() const
Return the stored number of beta electrons.
Definition orbitals.h:161
const AOBasis & getDftBasis() const
Return the DFT AO basis, throwing if it has not been initialized.
Definition orbitals.h:328
void setXCFunctionalName(std::string functionalname)
Definition orbitals.h:276
container for QM atoms
Definition qmatom.h:37
Index getElementNumber() const
Definition qmatom.h:81
const std::string & getElement() const
Definition qmatom.h:73
Identifier for QMstates. Strings like S1 are converted into enum +zero indexed int.
Definition qmstate.h:135
Class to represent Atom/Site in electrostatic.
Definition staticsite.h:37
void setMultipole(const Vector9d &multipole, Index rank)
Definition staticsite.h:99
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
Index getRank() const
Definition staticsite.h:78
const Vector9d & Q() const
Definition staticsite.h:123
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
SpinDensity DensityMatrix(const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
void setOverlap(AOOverlap &S, double etol)
void Configure(const options &opt_alpha, const options &opt_beta)
SpinDensity Iterate(const SpinDensity &dmat, SpinFock &H, tools::EigenSystem &MOs_alpha, tools::EigenSystem &MOs_beta, double totE)
void setCoupledFockBuilder(const CoupledFockBuilder &builder)
Index getGridSize() const
Definition vxc_grid.h:41
void GridSetup(const std::string &type, const QMMolecule &atoms, const AOBasis &basis)
Definition vxc_grid.cc:230
Index getBoxesSize() const
Definition vxc_grid.h:42
Eigen::MatrixXd GridWeightGradient(const Eigen::MatrixXd &density_matrix, const QMMolecule &atoms) const
Eigen::MatrixXd PulayGradient(const Eigen::MatrixXd &density_matrix, const AOBasis &dftbasis) const
Eigen::MatrixXd PulayGradientUKS(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta, const AOBasis &dftbasis) const
Mat_p_Energy IntegrateVXC(const Eigen::MatrixXd &density_matrix) const
void setXCfunctional(const std::string &functional)
static double getExactExchange(const std::string &functional)
Eigen::MatrixXd GridWeightGradientUKS(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta, const QMMolecule &atoms) const
SpinResult IntegrateVXCSpin(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta) const
Mediates interaction between polar and static sites.
double CalcStaticEnergy_site(const StaticSite &site1, const StaticSite &site2) const
#define XTP_LOG(level, log)
Definition logger.h:40
const double bohr2ang
Definition constants.h:49
Index getMaxThreads()
Definition eigen.h:128
Charge transport classes.
Definition ERIs.h:28
bool XTP_HAS_MKL_OVERLOAD()
Definition eigen.h:41
std::vector< AOMatrixDerivative > ComputeNuclearAttractionDerivatives(const AOBasis &aobasis, const QMMolecule &mol)
std::vector< AOMatrixDerivative > ComputeKineticDerivatives(const AOBasis &aobasis)
std::vector< AOMatrixDerivative > ComputeOverlapDerivatives(const AOBasis &aobasis)
std::array< Eigen::MatrixXd, 3 > AOMatrixDerivative
Definition dftengine.cc:422
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
Level
be loud and noisy
Definition globals.h:28
Spin-resolved density matrices returned for open-shell SCF updates.
Eigen::MatrixXd total() const
Return the total density P = P^alpha + P^beta.
Eigen::Matrix< double, 9, 1 > Vector9d
Definition eigen.h:34