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