votca 2026-dev
Loading...
Searching...
No Matches
dftengine.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// Third party includes
21#include <algorithm>
22#include <boost/filesystem.hpp>
23#include <boost/format.hpp>
24#include <iostream>
25#include <map>
26#include <optional>
27#include <string>
28
29// VOTCA includes
32
33// Local VOTCA includes
37#include "votca/xtp/aomatrix.h"
40#include "votca/xtp/dftengine.h"
43#include "votca/xtp/logger.h"
44#include "votca/xtp/mmregion.h"
45#include "votca/xtp/orbitals.h"
48
49namespace votca {
50namespace xtp {
51
52namespace {
53
54void CanonicalizeOrbitalPhases(Eigen::MatrixXd& coeffs) {
55 constexpr double tol = 1e-14;
56
57 for (Index col = 0; col < coeffs.cols(); ++col) {
58 Eigen::Index pivot = 0;
59 const double maxabs = coeffs.col(col).cwiseAbs().maxCoeff(&pivot);
60
61 if (maxabs <= tol) {
62 continue;
63 }
64
65 if (coeffs(pivot, col) < 0.0) {
66 coeffs.col(col) *= -1.0;
67 }
68 }
69}
70
71void CanonicalizeOrbitalPhases(tools::EigenSystem& mos) {
72 CanonicalizeOrbitalPhases(mos.eigenvectors());
73}
74
75} // namespace
76
77// Defined in libint2_derivative_calls.cc -- forward declared here
78// (rather than only near ComputeAndStoreForces further down, where the
79// other libint2_derivative_calls.cc forward declarations live) because
80// Initialize() below needs it too, to warn early -- at options-parsing
81// time, before any SCF work at all -- if compute_forces=true was
82// requested on a build that cannot actually do it. See that file's own
83// compile-time guard (around LIBINT2_MAX_DERIV_ORDER) for the full
84// explanation of why this check exists.
86
99
101
102 const std::string key_xtpdft = "xtpdft";
103 dftbasis_name_ = options.get(".basisset").as<std::string>();
104
105 if (options.exists(".auxbasisset")) {
106 auxbasis_name_ = options.get(".auxbasisset").as<std::string>();
107 }
108
109 if (!auxbasis_name_.empty()) {
110 screening_eps_ = options.get(key_xtpdft + ".screening_eps").as<double>();
112 options.get(key_xtpdft + ".fock_matrix_reset").as<Index>();
113 }
114 if (options.exists(".ecp")) {
115 ecp_name_ = options.get(".ecp").as<std::string>();
116 }
117
118 if (options.exists(key_xtpdft + ".force_uks_path")) {
119 force_uks_path_ = options.get(key_xtpdft + ".force_uks_path").as<bool>();
120 }
121
122 if (options.exists(key_xtpdft + ".compute_forces")) {
123 compute_forces_ = options.get(key_xtpdft + ".compute_forces").as<bool>();
124 }
126 // Fail fast, at options-parsing time, rather than only discovering
127 // this after a full (potentially expensive) SCF has already
128 // converged. Genuinely throws now (previously only printed a
129 // std::cerr WARNING and let Initialize() return normally, so the
130 // full SCF still ran to completion regardless, wasting real
131 // compute on a calculation that could never produce forces) --
132 // throwing std::runtime_error here for an invalid/impossible
133 // options combination matches this file's own, already-established
134 // convention (see e.g. "Spin multiplicity must be >= 1." and
135 // several other throw std::runtime_error(...) calls elsewhere in
136 // this same function), not a new pattern.
137 throw std::runtime_error(
138 "compute_forces=true was requested, but the libint2 this was "
139 "built against does not support derivative integrals for one "
140 "or more operator categories it needs (one-body, the two-center "
141 "Coulomb metric, or three-center RI -- see "
142 "libint2_derivative_calls.cc's own compile guards for exactly "
143 "which). Many pre-packaged libint2 builds (Homebrew, Ubuntu "
144 "apt, etc.) do not enable derivative-integral support for all "
145 "of these by default; rebuild libint2 with "
146 "--enable-1body/--enable-eri2/--enable-eri3 to use this "
147 "feature.");
148 }
149 if (compute_forces_ && !ecp_name_.empty()) {
150 // A real, previously-unguarded gap: the SCF's own Hamiltonian
151 // genuinely includes the ECP contribution (H0 = T + V_nuc + V_ECP
152 // + V_ext, see the comment on that further down in this file, and
153 // dftAOECP.FillPotential(dftbasis_, ecp_) actually called during
154 // the SCF itself) -- but ComputeAndStoreForces/ComputeAndStoreForcesUKS
155 // have no d(V_ECP)/dR term at all (confirmed directly: neither
156 // function references ecp_ or ecp_name_ anywhere). Computing
157 // forces anyway in this case would not fail cleanly the way the
158 // libint2-support case above does -- it would silently produce a
159 // physically INCOMPLETE result (missing the ECP contribution to
160 // the force entirely) that looks like a normal, valid force
161 // output, which is worse than refusing outright. Refuse instead.
162 throw std::runtime_error(
163 "compute_forces=true was requested together with an ECP ('" +
164 ecp_name_ +
165 "'), but analytic nuclear forces do not yet include the ECP "
166 "contribution to the force (d(V_ECP)/dR) -- computing forces "
167 "in this configuration would silently omit that term rather "
168 "than fail visibly. Either drop the ECP or do not request "
169 "compute_forces until this is implemented.");
170 }
171
172 if (options.exists(key_xtpdft + ".cdft.enabled")) {
173 cdft_enabled_ = options.get(key_xtpdft + ".cdft.enabled").as<bool>();
174 }
175 if (cdft_enabled_) {
176 // Deliberately parsed into a CDFTConstraintSpec (atom indices +
177 // charge, both directly from the options tree) here, at
178 // Initialize() time, rather than building the actual
179 // HirshfeldPartition::Constraint (which needs the reference
180 // densities and weight matrix) right away -- those need the
181 // molecule and basis, neither of which exist yet at this point;
182 // BuildCDFTConstraint (elsewhere in this file) does that
183 // conversion later, once Evaluate() actually has an Orbitals
184 // object with real QMAtoms to work with.
185 std::string indices_str =
186 options.get(key_xtpdft + ".cdft.indices").as<std::string>();
187 if (indices_str.empty()) {
188 throw std::runtime_error(
189 "cdft.enabled=true was requested, but cdft.indices is empty -- "
190 "specify which atoms (0-based, e.g. '1 3 13:17', same syntax "
191 "already used for diabatization.xml's own fragment indices) "
192 "make up the constrained fragment.");
193 }
194 cdft_constraint_spec_.atom_indices =
195 IndexParser().CreateIndexVector(indices_str);
196 cdft_constraint_spec_.target_charge =
197 options.get(key_xtpdft + ".cdft.charge").as<double>();
198 cdft_constraint_spec_.initial_lambda =
199 options.get(key_xtpdft + ".cdft.initial_lambda").as<double>();
201 options.get(key_xtpdft + ".cdft.max_iterations").as<Index>();
203 options.get(key_xtpdft + ".cdft.population_tolerance").as<double>();
204 cdft_constraint_spec_.guess_strategy =
205 options.get(key_xtpdft + ".cdft.guess_strategy").as<std::string>();
206 // Note: CDFT itself needs no derivative integral at all (it only
207 // ever builds ENERGY-level quantities -- reference densities,
208 // weight matrices, Fock-matrix potentials -- never the
209 // deriv_order=1 machinery compute_forces needs), so there is no
210 // separate HasLibint2DerivativeSupport() check needed here; if
211 // compute_forces were ALSO left enabled alongside cdft.enabled,
212 // the earlier compute_forces-specific check above already covers
213 // that combination.
214 }
215
216 initial_guess_ = options.get(".initial_guess").as<std::string>();
217
218 if (initial_guess_ == "dimer_guess") {
219 dimer_guess_orbA_name_ = options.get(".dimer_guess_orbA").as<std::string>();
220 dimer_guess_orbB_name_ = options.get(".dimer_guess_orbB").as<std::string>();
221 if (dimer_guess_orbA_name_.empty() || dimer_guess_orbB_name_.empty()) {
222 throw std::runtime_error(
223 "initial_guess=dimer_guess requires both dimer_guess_orbA and "
224 "dimer_guess_orbB to be set to real monomer .orb file paths.");
225 }
226 }
227
228 grid_name_ = options.get(key_xtpdft + ".integration_grid").as<std::string>();
229 xc_functional_name_ = options.get(".functional").as<std::string>();
230
231 if (options.exists(key_xtpdft + ".externaldensity")) {
234 options.get(key_xtpdft + ".externaldensity.orbfile").as<std::string>();
235 gridquality_ = options.get(key_xtpdft + ".externaldensity.gridquality")
236 .as<std::string>();
237 state_ =
238 options.get(key_xtpdft + ".externaldensity.state").as<std::string>();
239 }
240
241 if (options.exists(".externalfield")) {
243 extfield_ = options.get(".externalfield").as<Eigen::Vector3d>();
244 }
245
246 conv_opt_.Econverged =
247 options.get(key_xtpdft + ".convergence.energy").as<double>();
248 conv_opt_.error_converged =
249 options.get(key_xtpdft + ".convergence.error").as<double>();
250 max_iter_ =
251 options.get(key_xtpdft + ".convergence.max_iterations").as<Index>();
252
253 std::string method =
254 options.get(key_xtpdft + ".convergence.method").as<std::string>();
255 if (method == "DIIS") {
256 conv_opt_.usediis = true;
257 } else if (method == "mixing") {
258 conv_opt_.usediis = false;
259 }
260 if (!conv_opt_.usediis) {
261 conv_opt_.histlength = 1;
262 conv_opt_.maxout = false;
263 }
264 conv_opt_.mixingparameter =
265 options.get(key_xtpdft + ".convergence.mixing").as<double>();
266 // Ceiling for adaptive damping -- see the options struct's own
267 // comment (convergenceacc.h) for the full ORCA-derived reasoning.
268 conv_opt_.mixingmax =
269 options.get(key_xtpdft + ".convergence.mixing_max").as<double>();
270 conv_opt_.levelshift =
271 options.get(key_xtpdft + ".convergence.levelshift").as<double>();
272 conv_opt_.levelshiftend =
273 options.get(key_xtpdft + ".convergence.levelshift_end").as<double>();
274 // Independent from adiis_start -- see the options struct's own
275 // comment (convergenceacc.h) and UKSConvergenceAcc::Iterate's own
276 // mixing-trigger comment for the full reasoning.
277 conv_opt_.mixingend =
278 options.get(key_xtpdft + ".convergence.mixing_end").as<double>();
279 conv_opt_.maxout =
280 options.get(key_xtpdft + ".convergence.DIIS_maxout").as<bool>();
281 conv_opt_.histlength =
282 options.get(key_xtpdft + ".convergence.DIIS_length").as<Index>();
283 conv_opt_.diis_start =
284 options.get(key_xtpdft + ".convergence.DIIS_start").as<double>();
285 conv_opt_.adiis_start =
286 options.get(key_xtpdft + ".convergence.ADIIS_start").as<double>();
287 conv_opt_.davidson_max_iter =
288 options.get(key_xtpdft + ".convergence.davidson_max_iter").as<Index>();
289
290 if (options.exists(key_xtpdft + ".dft_in_dft.activeatoms")) {
292 options.get(key_xtpdft + ".dft_in_dft.activeatoms").as<std::string>();
294 options.get(key_xtpdft + ".dft_in_dft.threshold").as<double>();
296 options.get(key_xtpdft + ".dft_in_dft.levelshift").as<double>();
297 truncate_ =
298 options.get(key_xtpdft + ".dft_in_dft.truncate_basis").as<bool>();
299 if (truncate_) {
301 options.get(key_xtpdft + ".dft_in_dft.truncation_threshold")
302 .as<double>();
303 }
304 }
305}
306
307void DFTEngine::PrintMOs(const Eigen::VectorXd& MOEnergies, Log::Level level) {
308 XTP_LOG(level, *pLog_) << " Orbital energies: " << std::flush;
309 XTP_LOG(level, *pLog_) << " index occupation energy(Hartree) " << std::flush;
310
311 for (Index i = 0; i < MOEnergies.size(); ++i) {
312 Index occupancy = 0;
313 if (i < num_docc_) {
314 occupancy = 2;
315 } else if (i < num_docc_ + num_socc_alpha_) {
316 occupancy = 1;
317 }
318
319 XTP_LOG(level, *pLog_) << (boost::format(" %1$5d %2$1d %3$+1.10f") %
320 i % occupancy % MOEnergies(i))
321 .str()
322 << std::flush;
323 }
324 return;
325}
326
327void DFTEngine::PrintMOsUKS(const Eigen::VectorXd& alpha_energies,
328 const Eigen::VectorXd& beta_energies,
329 Log::Level level) const {
330 XTP_LOG(level, *pLog_) << " UKS orbital energies:" << std::flush;
331 XTP_LOG(level, *pLog_) << " index occ eps_a(Ha) eps_b(Ha)"
332 << std::flush;
333
334 const Index nrows =
335 std::max<Index>(alpha_energies.size(), beta_energies.size());
336
337 for (Index i = 0; i < nrows; ++i) {
338 const bool occ_a = (i < num_alpha_electrons_);
339 const bool occ_b = (i < num_beta_electrons_);
340
341 std::string occ = "0";
342 if (occ_a && occ_b) {
343 occ = "2";
344 } else if (occ_a) {
345 occ = "a";
346 } else if (occ_b) {
347 occ = "b";
348 }
349
350 std::string eps_a = " -";
351 std::string eps_b = " -";
352
353 if (i < alpha_energies.size()) {
354 eps_a = (boost::format("%+1.10f") % alpha_energies(i)).str();
355 }
356 if (i < beta_energies.size()) {
357 eps_b = (boost::format("%+1.10f") % beta_energies(i)).str();
358 }
359
360 XTP_LOG(level, *pLog_) << (boost::format(
361 " %1$5d %2$1s %3$15s %4$15s") %
362 i % occ % eps_a % eps_b)
363 .str()
364 << std::flush;
365 }
366
367 if (num_alpha_electrons_ > 0 &&
368 num_alpha_electrons_ < alpha_energies.size()) {
369 XTP_LOG(level, *pLog_) << (boost::format(
370 " alpha HOMO-LUMO gap: %+1.10f Ha") %
371 (alpha_energies(num_alpha_electrons_) -
372 alpha_energies(num_alpha_electrons_ - 1)))
373 .str()
374 << std::flush;
375 }
376
377 if (num_beta_electrons_ > 0 && num_beta_electrons_ < beta_energies.size()) {
378 XTP_LOG(level, *pLog_) << (boost::format(
379 " beta HOMO-LUMO gap: %+1.10f Ha") %
380 (beta_energies(num_beta_electrons_) -
381 beta_energies(num_beta_electrons_ - 1)))
382 .str()
383 << std::flush;
384 }
385}
386
387void DFTEngine::CalcElDipole(const Orbitals& orb) const {
388 QMState state = QMState("n");
389 Eigen::Vector3d result = orb.CalcElDipole(state);
391 << TimeStamp() << " Electric Dipole is[e*bohr]:\n\t\t dx=" << result[0]
392 << "\n\t\t dy=" << result[1] << "\n\t\t dz=" << result[2] << std::flush;
393 return;
394}
395
396// Assembles the total ground-state gradient (nuclear repulsion + RI-J
397// Coulomb + XC, LDA or GGA) from the converged density matrix, negates it
398// to the physical force convention, and stores it via Orbitals::setForces().
399// See the detailed SCOPE note on the declaration in dftengine.h for exactly
400// which cases this does and does not support, and why.
401//
402// Every individual term here (NuclearRepulsionDerivative, RIJGradient,
403// PulayGradient, GridWeightGradient) was separately derived and validated
404// via finite-difference tests earlier in this branch (see
405// test_dftgradient.cc and test_xcgradient.cc) -- this function's own new
406// content is just the SUMMATION and the sign convention, not any new
407// derivative math.
408// Defined in libint2_derivative_calls.cc, not yet in any header (same
409// STATUS noted throughout that file) -- forward declared here. Unlike
410// DFTGradient::RIJGradient/PulayGradient/etc., these return RAW AO-matrix
411// derivatives (d(matrix_munu)/dR), not already-contracted energy
412// gradients -- the contraction with Dmat is done explicitly below.
413using AOMatrixDerivative = std::array<Eigen::MatrixXd, 3>;
414std::vector<AOMatrixDerivative> ComputeOverlapDerivatives(
415 const AOBasis& aobasis);
416std::vector<AOMatrixDerivative> ComputeKineticDerivatives(
417 const AOBasis& aobasis);
418std::vector<AOMatrixDerivative> ComputeNuclearAttractionDerivatives(
419 const AOBasis& aobasis, const QMMolecule& mol);
420// HasLibint2DerivativeSupport() (used below in both ComputeAndStoreForces
421// and ComputeAndStoreForcesUKS) is already forward declared earlier in
422// this file, near Initialize() -- see that declaration's own comment
423// for why it needed to be that early.
424
426 Orbitals& orb, const Eigen::MatrixXd& Dmat,
427 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
428 if (auxbasis_name_.empty()) {
430 << TimeStamp()
431 << " Skipping force calculation: RI-J gradient (DFTGradient::"
432 "RIJGradient) only implements the RI path, but this SCF ran "
433 "without an auxiliary basis (conventional 4-center ERIs)."
434 << std::flush;
435 return;
436 }
437
440 << TimeStamp()
441 << " Skipping force calculation: the libint2 this was built "
442 "against does not support derivative integrals for one or "
443 "more operator categories it needs. Many pre-packaged "
444 "libint2 builds (Homebrew, Ubuntu apt, etc.) do not enable "
445 "this by default -- rebuild libint2 with "
446 "--enable-1body/--enable-eri2/--enable-eri3 to use analytic "
447 "forces."
448 << std::flush;
449 return;
450 }
451
452 if (!ecp_name_.empty()) {
453 // Same reasoning as Initialize()'s own, earlier check (which
454 // should already have caught this before any SCF work even
455 // started) -- this is a defense-in-depth repeat, matching the
456 // existing HasLibint2DerivativeSupport() re-check just above,
457 // in case compute_forces_/ecp_name_ were ever set some other way
458 // than through Initialize()'s own options parsing. Skips cleanly
459 // (log + return) rather than throwing here, matching this
460 // function's own existing style for the libint2-support case
461 // above -- by the time SCF has already converged this far,
462 // throwing would be a less graceful failure than simply not
463 // storing forces, though Initialize()'s own check is the
464 // preferred, much earlier place for this to actually be caught.
466 << TimeStamp() << " Skipping force calculation: an ECP ('" << ecp_name_
467 << "') was used for this SCF, but analytic nuclear forces do "
468 "not yet include the ECP contribution to the force "
469 "(d(V_ECP)/dR) -- computing forces in this configuration "
470 "would silently omit that term rather than fail visibly."
471 << std::flush;
472 return;
473 }
474
475 const QMMolecule& mol = orb.QMAtoms();
476 Index natoms = mol.size();
477
478 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Starting force calculation ("
479 << natoms << " atoms)" << std::flush;
480
481 // One-electron (kinetic + nuclear attraction) contribution --
482 // dEone/dR_A = Tr[Dmat . d(T+V_ne)/dR_A]. This was the piece
483 // discovered MISSING from the total gradient by the first genuine
484 // end-to-end SCF+forces test (test_dftengine_forces.cc): kinetic
485 // derivatives were validated at the very start of this whole branch
486 // and then never actually wired into any gradient assembly, and
487 // nuclear attraction derivatives were never implemented at all until
488 // that gap was found. See ComputeNuclearAttractionDerivatives in
489 // libint2_derivative_calls.cc for the detailed derivation (sign
490 // convention checked directly against AOMultipole's own,
491 // already-validated energy-level code, not assumed).
493 << " Computing one-electron (kinetic + nuclear "
494 "attraction) derivatives"
495 << std::flush;
496 std::vector<AOMatrixDerivative> dT = ComputeKineticDerivatives(dftbasis_);
497 std::vector<AOMatrixDerivative> dVne =
499 Eigen::MatrixXd eone_grad = Eigen::MatrixXd::Zero(natoms, 3);
500 for (Index a = 0; a < natoms; ++a) {
501 for (Index xyz = 0; xyz < 3; ++xyz) {
502 eone_grad(a, xyz) = Dmat.cwiseProduct(dT[a][xyz] + dVne[a][xyz]).sum();
503 }
504 }
506 << TimeStamp() << " One-electron derivatives done" << std::flush;
507
508 // Overlap "Pulay force" -- a SECOND, genuinely distinct missing term,
509 // found after the kinetic+nuclear-attraction fix improved but did not
510 // fully resolve the discrepancy against the end-to-end finite-difference
511 // test (magnitude dropped ~7x in the right direction, but still wrong
512 // by roughly the size of a real missing term, not noise).
513 //
514 // Distinct from the earlier "PulayGradient" naming (which is about
515 // basis functions inside the XC integral) -- this is the CLASSICAL
516 // SCF Pulay/overlap force, present in essentially any Gaussian-basis
517 // HF/DFT gradient: the MO coefficients C are only implicitly
518 // R-independent because they satisfy the orthonormality constraint
519 // C^T S C = I, and S itself depends on R (basis functions move). At
520 // the SCF stationary point, the Lagrange multipliers for this
521 // constraint are exactly the orbital energies (canonical MOs), giving
522 // an extra term dE/dR_A|_overlap = -Tr[W . dS/dR_A], where
523 // W = 2 * C_occ * diag(eps_occ) * C_occ^T (the "energy-weighted
524 // density matrix", factor of 2 matching the same doubled convention
525 // Dmat already uses for closed-shell restricted). Confirmed as a
526 // standard, expected term by libint2's own reference SCF-gradient
527 // example (compute_1body_ints_deriv<Operator::overlap> combined with
528 // exactly this W construction, in
529 // libint2/include/libint2/lcao/1body.h) -- not a novel derivation.
530 //
531 // ComputeOverlapDerivatives itself was validated (finite-difference
532 // tested) at the very start of this whole branch and then never
533 // actually used in any gradient assembly until now, same as kinetic.
535 Eigen::MatrixXd C_occ = orb.MOs().eigenvectors().leftCols(n_occ);
536 Eigen::VectorXd eps_occ = orb.MOs().eigenvalues().head(n_occ);
537 Eigen::MatrixXd W = 2.0 * C_occ * eps_occ.asDiagonal() * C_occ.transpose();
538
540 << TimeStamp() << " Computing overlap (Pulay) derivatives"
541 << std::flush;
542 std::vector<AOMatrixDerivative> dS = ComputeOverlapDerivatives(dftbasis_);
543 Eigen::MatrixXd overlap_pulay_grad = Eigen::MatrixXd::Zero(natoms, 3);
544 for (Index a = 0; a < natoms; ++a) {
545 for (Index xyz = 0; xyz < 3; ++xyz) {
546 overlap_pulay_grad(a, xyz) = -W.cwiseProduct(dS[a][xyz]).sum();
547 }
548 }
550 << TimeStamp() << " Overlap derivatives done" << std::flush;
551
553 << TimeStamp() << " Computing RI-J (Coulomb) gradient" << std::flush;
554 Eigen::MatrixXd rij_term =
557 << TimeStamp() << " RI-J gradient done" << std::flush;
558
560 << TimeStamp() << " Computing XC grid (Pulay + weight) gradient terms"
561 << std::flush;
562 Eigen::MatrixXd pulay_term = vxcpotential.PulayGradient(Dmat, dftbasis_);
563 Eigen::MatrixXd weight_term = vxcpotential.GridWeightGradient(Dmat, mol);
564 Eigen::MatrixXd nucrep_term = DFTGradient::NuclearRepulsionDerivative(mol);
566 << TimeStamp() << " XC grid gradient terms done" << std::flush;
567
568 Eigen::MatrixXd grad = nucrep_term + eone_grad + overlap_pulay_grad +
569 rij_term + pulay_term + weight_term;
570
571 // Exact-exchange (RI-K) gradient -- hybrid functionals only. Skipped
572 // entirely (not just multiplied by a zero ScaHFX_) when not needed,
573 // since RIKGradient is genuinely expensive (O(nocc^2 * naux) linear
574 // solves) unlike the GGA sigma terms, which are cheap enough to
575 // compute unconditionally.
576 //
577 // RIKGradient's own energy convention (E_K = -sum_ij c_ij.d_ij) was
578 // confirmed, via direct numerical simulation of
579 // ERIs::CalculateEXX_mos's real algorithm and then a real C++
580 // finite-difference test against that same production function
581 // (test_dftgradient.cc), to equal EXACTLY 0.25*Dmat.cwiseProduct(K).sum()
582 // at ScaHFX_=1 -- so for general ScaHFX_, the contribution is
583 // ScaHFX_ * RIKGradient(...), a direct scaling, matching exactly how
584 // the real SCF energy scales its own exx term
585 // (exx = 0.25*ScaHFX_*Dmat.cwiseProduct(K).sum()).
586 //
587 // This removes what was previously an explicit, logged SCOPE
588 // limitation (hybrid functionals skipped entirely) -- see git history
589 // for the full derivation/verification that led to this.
590 if (ScaHFX_ > 0.0) {
592 << TimeStamp() << " Computing RI-K (exact exchange) gradient"
593 << std::flush;
596 << TimeStamp() << " RI-K gradient done" << std::flush;
597 }
598
599 // Sanity check independent of the finite-difference tests already done
600 // per-term: translational invariance means the TOTAL gradient must sum
601 // to zero across all atoms. Logged rather than asserted/thrown --
602 // deliberately not blocking a real SCF run over a force-only sanity
603 // check, but worth knowing about if it ever fires.
604 Eigen::Vector3d sum = grad.colwise().sum();
605 if (sum.cwiseAbs().maxCoeff() > 1e-4) {
607 << TimeStamp()
608 << " WARNING: computed forces do not sum to zero across atoms "
609 "(translational invariance check failed, max component="
610 << sum.cwiseAbs().maxCoeff()
611 << ") -- treat these forces with "
612 "caution."
613 << std::flush;
614 }
615
616 // Physical force = -dE/dR, matching the convention external tools
617 // (e.g. ASE's Calculator.get_forces()) expect -- NuclearRepulsionDerivative/
618 // RIJGradient/PulayGradient/GridWeightGradient all return dE/dR directly
619 // (the gradient, not the force), consistent with each other throughout
620 // this branch; negating once here, at the point of storage, rather than
621 // in each individual term, keeps that internal convention consistent
622 // and puts the physical-force sign flip in exactly one place.
623 Eigen::MatrixXd force = -grad;
624 orb.setForces(force);
625
627 << TimeStamp() << " Computed and stored ground-state nuclear forces."
628 << std::flush;
629 // Atomic units (Hartree/Bohr) -- deliberately NOT converted here, same
630 // convention as what gets stored via setForces()/WriteToCpt above and
631 // what the rest of this file's own log output uses for energies
632 // (Hartree throughout; only the earlier "Molecule Coordinates" section
633 // converts to Angstrom, for readability, and that conversion is
634 // unrelated to this).
635 XTP_LOG(Log::error, *pLog_) << " Forces [Ha/Bohr]" << std::flush;
636 for (Index a = 0; a < natoms; ++a) {
637 std::string output =
638 (boost::format(" %1$s"
639 " %2$+1.6f %3$+1.6f %4$+1.6f") %
640 mol[a].getElement() % force(a, 0) % force(a, 1) % force(a, 2))
641 .str();
642 XTP_LOG(Log::error, *pLog_) << output << std::flush;
643 }
644}
645
647 const QMMolecule& mol, const tools::EigenSystem& MOs_alpha,
648 const tools::EigenSystem& MOs_beta) const {
649 // W = W_alpha + W_beta, each WITHOUT the factor of 2 RKS uses -- UKS
650 // spin densities/MO occupations are not pre-doubled (each spin
651 // channel already corresponds to its own electron count).
652 //
653 // NOTE ON VALIDATION: this term is NOT checkable against a
654 // fixed-C finite difference the way the other UKS gradient terms are
655 // -- confirmed directly by a failed attempt to do exactly that (see
656 // git history). The overlap Pulay force specifically corrects for C's
657 // IMPLICIT R-dependence through the orthonormality constraint
658 // C^T S(R) C = I, valid only at a genuine variational stationary
659 // point (the Lagrange-multiplier argument requires C to actually be a
660 // converged SCF solution) -- a fixed, arbitrary C held constant across
661 // displaced geometries never satisfies that constraint
662 // self-consistently, so there is no fixed-C energy this term is
663 // supposed to match. This mirrors exactly why the RKS version of this
664 // term was only ever validated by a genuine, self-consistent
665 // end-to-end SCF test (test_dftengine_forces.cc), never a
666 // fixed-density-matrix unit test. See
667 // compute_non_xc_gradient_uks_finite_difference and
668 // overlap_pulay_gradient_uks_reduces_to_rks in
669 // test_dftengine_private.cc for how this piece is actually checked
670 // instead: the other four terms against a fixed-C finite difference,
671 // and this term separately against the already-validated RKS formula
672 // in the alpha==beta limit.
673 Index n_occ_alpha = num_alpha_electrons_;
674 Index n_occ_beta = num_beta_electrons_;
675 Eigen::MatrixXd C_alpha_occ = MOs_alpha.eigenvectors().leftCols(n_occ_alpha);
676 Eigen::MatrixXd C_beta_occ = MOs_beta.eigenvectors().leftCols(n_occ_beta);
677 Eigen::VectorXd eps_alpha_occ = MOs_alpha.eigenvalues().head(n_occ_alpha);
678 Eigen::VectorXd eps_beta_occ = MOs_beta.eigenvalues().head(n_occ_beta);
679 Eigen::MatrixXd W =
680 C_alpha_occ * eps_alpha_occ.asDiagonal() * C_alpha_occ.transpose() +
681 C_beta_occ * eps_beta_occ.asDiagonal() * C_beta_occ.transpose();
682
683 Index natoms = mol.size();
684 std::vector<AOMatrixDerivative> dS = ComputeOverlapDerivatives(dftbasis_);
685 Eigen::MatrixXd overlap_pulay_grad = Eigen::MatrixXd::Zero(natoms, 3);
686 for (Index a = 0; a < natoms; ++a) {
687 for (Index xyz = 0; xyz < 3; ++xyz) {
688 overlap_pulay_grad(a, xyz) = -W.cwiseProduct(dS[a][xyz]).sum();
689 }
690 }
691 return overlap_pulay_grad;
692}
693
695 const QMMolecule& mol, const UKSConvergenceAcc::SpinDensity& Dspin,
696 const tools::EigenSystem& MOs_alpha,
697 const tools::EigenSystem& MOs_beta) const {
698 Index natoms = mol.size();
699 const Eigen::MatrixXd D_total = Dspin.total();
700
701 // One-electron and RI-J: identical formulas/conventions to the RKS
702 // case, just built from D_total = Dspin.alpha + Dspin.beta -- matches
703 // exactly how RKS's own Dmat is already alpha+beta (E_one and E_coul
704 // in EvaluateUKS use D_total the same way EvaluateClosedShell's Eone/
705 // Etwo use Dmat), confirmed directly by reading EvaluateUKS rather
706 // than assumed.
707 std::vector<AOMatrixDerivative> dT = ComputeKineticDerivatives(dftbasis_);
708 std::vector<AOMatrixDerivative> dVne =
710 Eigen::MatrixXd eone_grad = Eigen::MatrixXd::Zero(natoms, 3);
711 for (Index a = 0; a < natoms; ++a) {
712 for (Index xyz = 0; xyz < 3; ++xyz) {
713 eone_grad(a, xyz) = D_total.cwiseProduct(dT[a][xyz] + dVne[a][xyz]).sum();
714 }
715 }
716
717 Eigen::MatrixXd overlap_pulay_grad =
718 ComputeOverlapPulayGradientUKS(mol, MOs_alpha, MOs_beta);
719
720 Eigen::MatrixXd grad =
722 overlap_pulay_grad +
724
725 // Exact exchange (RI-K), hybrids only. Factor of 0.5*ScaHFX_ (not
726 // ScaHFX_ alone) -- confirmed both algebraically and numerically
727 // (Python, to ~1e-14) that ERIs::CalculateEXX_dmat(P) ==
728 // 0.5*ERIs::CalculateEXX_mos(C) when P=C*C^T, and UKS's own exact
729 // exchange goes through CalculateEXX_dmat (a DIFFERENT code path than
730 // RIKGradient was validated against, which uses CalculateEXX_mos
731 // directly) -- tracing that factor of 0.5 through both spin channels'
732 // energy expressions gives dE_exx/dR =
733 // 0.5*ScaHFX_*[RIKGradient(C_alpha_occ)+RIKGradient(C_beta_occ)], not
734 // the naive ScaHFX_*(...) that would be a factor-of-2 error.
735 if (ScaHFX_ > 0.0) {
736 Eigen::MatrixXd C_alpha_occ =
737 MOs_alpha.eigenvectors().leftCols(num_alpha_electrons_);
738 Eigen::MatrixXd C_beta_occ =
739 MOs_beta.eigenvectors().leftCols(num_beta_electrons_);
740 grad += 0.5 * ScaHFX_ *
743 }
744 return grad;
745}
746
748 Orbitals& orb, const UKSConvergenceAcc::SpinDensity& Dspin,
749 const tools::EigenSystem& MOs_alpha, const tools::EigenSystem& MOs_beta,
750 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
751 if (auxbasis_name_.empty()) {
753 << TimeStamp()
754 << " Skipping UKS force calculation: RI-J gradient only "
755 "implements the RI path, but this SCF ran without an "
756 "auxiliary basis."
757 << std::flush;
758 return;
759 }
760
763 << TimeStamp()
764 << " Skipping UKS force calculation: the libint2 this was "
765 "built against does not support derivative integrals for "
766 "one or more operator categories it needs. Many "
767 "pre-packaged libint2 builds (Homebrew, Ubuntu apt, etc.) "
768 "do not enable this by default -- rebuild libint2 with "
769 "--enable-1body/--enable-eri2/--enable-eri3 to use "
770 "analytic forces."
771 << std::flush;
772 return;
773 }
774
775 if (!ecp_name_.empty()) {
776 // Same reasoning as the RKS ComputeAndStoreForces' own, identical
777 // check just above (and Initialize()'s own, earlier, preferred
778 // check) -- ECP forces are not implemented in either spin
779 // channel's gradient assembly.
781 << TimeStamp() << " Skipping UKS force calculation: an ECP ('"
782 << ecp_name_
783 << "') was used for this SCF, but analytic nuclear forces do "
784 "not yet include the ECP contribution to the force "
785 "(d(V_ECP)/dR) -- computing forces in this configuration "
786 "would silently omit that term rather than fail visibly."
787 << std::flush;
788 return;
789 }
790
791 Eigen::MatrixXd grad =
792 ComputeNonXCGradientUKS(orb.QMAtoms(), Dspin, MOs_alpha, MOs_beta);
793
794 // XC gradient (LDA and GGA both supported -- see the detailed
795 // derivation/validation history on this function's declaration in
796 // dftengine.h and on PulayGradientUKS/GridWeightGradientUKS in
797 // vxc_potential.h).
798 grad += vxcpotential.PulayGradientUKS(Dspin.alpha, Dspin.beta, dftbasis_);
799 grad += vxcpotential.GridWeightGradientUKS(Dspin.alpha, Dspin.beta,
800 orb.QMAtoms());
801
802 // Sanity check independent of the finite-difference tests already
803 // done per-term: translational invariance means the TOTAL gradient
804 // must sum to zero across all atoms. Logged rather than asserted/
805 // thrown, same as the RKS path -- deliberately not blocking a real
806 // SCF run over a force-only sanity check.
807 Eigen::Vector3d sum = grad.colwise().sum();
808 if (sum.cwiseAbs().maxCoeff() > 1e-4) {
810 << TimeStamp()
811 << " WARNING: computed UKS forces do not sum to zero across "
812 "atoms (translational invariance check failed, max "
813 "component="
814 << sum.cwiseAbs().maxCoeff()
815 << ") -- treat these forces with "
816 "caution."
817 << std::flush;
818 }
819
820 // Physical force = -dE/dR, matching the RKS ComputeAndStoreForces
821 // convention exactly -- all pieces above return dE/dR directly (the
822 // gradient, not the force), negated once here at the point of
823 // storage.
824 Eigen::MatrixXd force = -grad;
825 orb.setForces(force);
826
828 << TimeStamp() << " Computed and stored ground-state UKS nuclear forces."
829 << std::flush;
830 // Same convention as ComputeAndStoreForces (RKS): atomic units
831 // (Hartree/Bohr), matching what gets stored via setForces() above.
832 const QMMolecule& mol_for_print = orb.QMAtoms();
833 XTP_LOG(Log::error, *pLog_) << " Forces [Ha/Bohr]" << std::flush;
834 for (Index a = 0; a < force.rows(); ++a) {
835 std::string output = (boost::format(" %1$s"
836 " %2$+1.6f %3$+1.6f %4$+1.6f") %
837 mol_for_print[a].getElement() % force(a, 0) %
838 force(a, 1) % force(a, 2))
839 .str();
840 XTP_LOG(Log::error, *pLog_) << output << std::flush;
841 }
842}
843
844// Build the Coulomb and exact-exchange contributions generated by the current
845// density matrix. The returned pair is conventionally interpreted as
846//
847// (J[P], -K[P]),
848//
849// so that the hybrid Fock update becomes F = H0 + J[P] + a_x (-K[P]) + V_xc.
850// For RI/3c builds the occupied MO block is supplied when available to avoid an
851// unnecessary reconstruction of exchange intermediates.
852std::array<Eigen::MatrixXd, 2> DFTEngine::CalcERIs_EXX(
853 const Eigen::MatrixXd& MOCoeff, const Eigen::MatrixXd& Dmat,
854 double error) const {
855 if (!auxbasis_name_.empty()) {
856 if (conv_accelerator_.getUseMixing() || MOCoeff.rows() == 0) {
857 return ERIs_.CalculateERIs_EXX_3c(Eigen::MatrixXd::Zero(0, 0), Dmat);
858 } else {
859 Eigen::MatrixXd occblock = MOCoeff.leftCols(num_docc_ + num_socc_alpha_);
860 return ERIs_.CalculateERIs_EXX_3c(occblock, Dmat);
861 }
862 } else {
863 return ERIs_.CalculateERIs_EXX_4c(Dmat, error);
864 }
865}
866
867// Pure Coulomb contribution J[P] from the current AO density matrix. The code
868// dispatches to either RI/3c or conventional 4-center integral evaluation.
869Eigen::MatrixXd DFTEngine::CalcERIs(const Eigen::MatrixXd& Dmat,
870 double error) const {
871 if (!auxbasis_name_.empty()) {
872 return ERIs_.CalculateERIs_3c(Dmat);
873 } else {
874 return ERIs_.CalculateERIs_4c(Dmat, error);
875 }
876}
877
879 const Mat_p_Energy& H0) const {
880 return conv_accelerator_.SolveFockmatrix(H0.matrix());
881}
882
883// Construct a self-consistent model-potential guess by starting from an
884// atomic density P^(0), evaluating
885//
886// F[P^(0)] = H0 + J[P^(0)] + a_x (-K[P^(0)]) + V_xc[P^(0)],
887//
888// and diagonalizing the resulting Fock matrix once.
890 const Mat_p_Energy& H0, const QMMolecule& mol,
891 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
892 Eigen::MatrixXd Dmat = AtomicGuess(mol);
893 Mat_p_Energy e_vxc = vxcpotential.IntegrateVXC(Dmat);
895 << TimeStamp() << " Filled DFT Vxc matrix " << std::flush;
896
897 Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
898
899 if (ScaHFX_ > 0) {
900 std::array<Eigen::MatrixXd, 2> both =
901 CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dmat, 1e-12);
902 H += both[0];
903 H += ScaHFX_ * both[1];
904 } else {
905 H += CalcERIs(Dmat, 1e-12);
906 }
907 return conv_accelerator_.SolveFockmatrix(H);
908}
909
911 if (cdft_enabled_) {
912 // Deliberately dispatched here, BEFORE any of the normal
913 // Prepare/SetupH0/SetupVxc/ConfigOrbfile setup below -- RunCDFT
914 // does that same setup internally itself (matching this
915 // function's own structure exactly), so doing it here too would
916 // just duplicate the work. BuildCDFTConstraint needs orb.QMAtoms()
917 // to already be set (the same requirement Evaluate() itself has,
918 // via SetupH0(orb.QMAtoms()) below), so this is not adding any new
919 // requirement on the caller.
922
923 // Suppresses the ordinary DFT force calculation during EVERY
924 // intermediate lambda-bisection trial inside RunCDFT (each of
925 // which internally calls EvaluateUKS, which would otherwise
926 // trigger the full, expensive ComputeAndStoreForcesUKS on each one
927 // -- confirmed directly, via a real run, to be a genuine,
928 // substantial waste: only the FINAL, converged lambda's own force
929 // is ever actually used). Restored unconditionally below,
930 // regardless of whether RunCDFT converges, so this never leaks a
931 // suppressed value back to the caller.
932 bool original_compute_forces = compute_forces_;
933 compute_forces_ = false;
934 bool converged = RunCDFT(orb, constraint);
935 compute_forces_ = original_compute_forces;
936
937 if (converged && original_compute_forces) {
938 // ONE, explicit, final UKS evaluation to compute the ordinary
939 // DFT force for the now-converged, fixed CDFT density -- orb
940 // already holds the converged MOs from RunCDFT's own, final
941 // internal EvaluateUKS call, so this re-run starts from (and
942 // should remain at) that same fixed point, converging
943 // essentially immediately rather than as a fresh, cold SCF.
944 // Deliberately re-runs Prepare/SetupH0/SetupVxc/ConfigOrbfile
945 // (the same setup RunCDFT already did once, internally, at its
946 // own start) rather than threading H0/vxcpotential through
947 // RunCDFT's own signature to avoid this -- a real, accepted
948 // cost (this setup, not the SCF itself, is what gets redone),
949 // chosen specifically to avoid touching RunCDFT's own,
950 // already-validated signature/control flow at all.
952 << TimeStamp()
953 << " CDFT converged -- computing the ordinary DFT force once, "
954 "for the final, converged density only"
955 << std::flush;
956 Prepare(orb);
957 Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
958 Vxc_Potential<Vxc_Grid> vxcpotential = SetupVxc(orb.QMAtoms());
959 ConfigOrbfile(orb);
960 EvaluateUKS(orb, H0, vxcpotential);
961 }
962
963 if (converged && orb.hasForces()) {
964 // The explicit, final EvaluateUKS call just above (not RunCDFT's
965 // own, internal ones, which now have forces suppressed -- see
966 // the comment on original_compute_forces above) computed and
967 // stored the ordinary DFT force; this adds the CDFT-specific
968 // correction on top of it. Done HERE, once, after RunCDFT's
969 // outer loop has fully converged --
970 // deliberately NOT inside ComputeAndStoreForcesUKS itself (which
971 // would otherwise redo this work, wastefully and riskily, at
972 // EVERY outer CDFT iteration, since RunCDFT calls EvaluateUKS
973 // repeatedly) and deliberately NOT by changing RunCDFT's own
974 // signature to pass through the original per-atom fragment
975 // indices (constraint only carries the already-SUMMED
976 // weight_matrix, not which atoms went into it -- rebuilding here
977 // instead, via cdft_constraint_spec_'s own atom_indices, avoids
978 // touching RunCDFT's own, already-validated signature/behavior
979 // at all).
980 //
981 // Rebuilds the same reference densities/atomic references/
982 // basis/grid BuildCDFTConstraint itself already built internally
983 // -- a redundant but cheap recomputation (no SCF involved),
984 // accepted deliberately for this reason.
985 std::map<std::string, Eigen::MatrixXd> reference_densities =
987 AOBasis full_dftbasis;
988 {
989 BasisSet basisset;
990 basisset.Load(dftbasis_name_);
991 full_dftbasis.Fill(basisset, orb.QMAtoms());
992 }
993 Vxc_Grid grid;
994 grid.GridSetup(grid_name_, orb.QMAtoms(), full_dftbasis);
995 std::vector<HirshfeldPartition::AtomicReference> atoms =
997 orb.QMAtoms(), dftbasis_name_, reference_densities);
998
999 std::array<Eigen::MatrixXd, 2> Dspin =
1001 // Total (alpha+beta) density -- matches the charge constraint's
1002 // own spin_alpha_coefficient=spin_beta_coefficient=+1.0
1003 // convention exactly (Tr[(D_alpha+D_beta)*W] = the same
1004 // population EvaluateMismatch itself computes inside RunCDFT).
1005 Eigen::MatrixXd density_total = Dspin[0] + Dspin[1];
1006
1007 Eigen::MatrixXd cdft_gradient_correction =
1008 Eigen::MatrixXd::Zero(static_cast<Index>(orb.QMAtoms().size()), 3);
1009 for (Index atom_index : cdft_constraint_spec_.atom_indices) {
1010 cdft_gradient_correction +=
1012 atoms, atom_index, density_total, orb.QMAtoms(), full_dftbasis,
1013 grid);
1014 }
1015 // Physical force = -dE/dR (ComputeAndStoreForcesUKS's own,
1016 // already-established convention): the CDFT correction to the
1017 // GRADIENT is +lambda*d(Tr[D*W_c])/dR (added directly, matching
1018 // ComputeCDFTForceContribution's own gradient-convention
1019 // return), so the correction to the FORCE is -lambda times this
1020 // same quantity.
1021 orb.setForces(orb.getForces() -
1022 constraint.lambda * cdft_gradient_correction);
1023 }
1024 return converged;
1025 }
1026
1027 Prepare(orb);
1028 Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
1029 Vxc_Potential<Vxc_Grid> vxcpotential = SetupVxc(orb.QMAtoms());
1030 ConfigOrbfile(orb);
1031
1035 << TimeStamp()
1036 << " Forcing closed-shell singlet through UKS development path."
1037 << std::flush;
1038 }
1039 return EvaluateUKS(orb, H0, vxcpotential);
1040 }
1041 return EvaluateClosedShell(orb, H0, vxcpotential);
1042}
1043
1045 HirshfeldPartition::Constraint& constraint) {
1046 Prepare(orb);
1047 Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
1048 Vxc_Potential<Vxc_Grid> vxcpotential = SetupVxc(orb.QMAtoms());
1049 ConfigOrbfile(orb);
1050
1051 // Restored on every exit path (converged or not) -- RunCDFT
1052 // deliberately overrides this member's own value between outer
1053 // iterations (to force the warm-start "orbfile" guess from the
1054 // second iteration onward), so it must not leak whatever value the
1055 // caller's own options actually specified.
1056 std::string saved_initial_guess = initial_guess_;
1057
1058 constraints_ = {constraint};
1059
1060 // Bisection bracket for lambda -- deliberately not Newton's method:
1061 // bisection needs only that the population is monotonic in lambda
1062 // (true for a well-behaved CDFT problem: increasing lambda always
1063 // pushes more density toward -- or away from, depending on sign --
1064 // the constrained region), never an explicit dN/dlambda derivative,
1065 // making this the more robust choice for a first implementation.
1066 // Starts centered on the caller's own initial guess (constraint.lambda,
1067 // 0.0 by default) and expands outward, doubling each time, until the
1068 // mismatch changes sign across the bracket or a hard iteration limit
1069 // is hit -- rather than assuming any single fixed bracket width is
1070 // always wide enough for every system.
1071 double lambda_lo = constraint.lambda - 0.1;
1072 double lambda_hi = constraint.lambda + 0.1;
1073
1074 auto EvaluateMismatch = [&](double lambda) -> double {
1075 constraints_[0].lambda = lambda;
1077 << TimeStamp() << " CDFT: starting inner SCF at lambda=" << lambda
1078 << std::flush;
1079 bool scf_converged = EvaluateUKS(orb, H0, vxcpotential);
1080 if (!scf_converged) {
1081 throw std::runtime_error(
1082 "RunCDFT: inner SCF did not converge at lambda=" +
1083 std::to_string(lambda));
1084 }
1085 if (cdft_constraint_spec_.guess_strategy == "warmstart") {
1086 initial_guess_ = "orbfile"; // warm start every subsequent call
1087 }
1088 // "fresh": deliberately leave initial_guess_ untouched here, so it
1089 // stays at whatever the calculation's own, original, top-level
1090 // setting was for every trial -- see this option's own XML help
1091 // text (dftpackage.xml) for why this can matter: if consecutive
1092 // lambda trials correspond to substantially different electronic
1093 // structures, warm-starting from the immediately preceding trial's
1094 // own converged density could be a worse starting point than a
1095 // fresh guess, not a better one.
1096 std::array<Eigen::MatrixXd, 2> Dspin =
1098 double population =
1099 constraint.spin_alpha_coefficient *
1100 Dspin[0].cwiseProduct(constraint.weight_matrix).sum() +
1101 constraint.spin_beta_coefficient *
1102 Dspin[1].cwiseProduct(constraint.weight_matrix).sum();
1103 return population - constraint.target_population;
1104 };
1105
1106 try {
1107 double mismatch_lo = EvaluateMismatch(lambda_lo);
1108 double mismatch_hi = EvaluateMismatch(lambda_hi);
1109
1110 Index bracket_attempts = 0;
1111 constexpr Index kMaxBracketAttempts = 10;
1112 while (mismatch_lo * mismatch_hi > 0.0 &&
1113 bracket_attempts < kMaxBracketAttempts) {
1114 double width = lambda_hi - lambda_lo;
1115 lambda_lo -= 0.5 * width;
1116 lambda_hi += 0.5 * width;
1117 mismatch_lo = EvaluateMismatch(lambda_lo);
1118 mismatch_hi = EvaluateMismatch(lambda_hi);
1119 ++bracket_attempts;
1120 }
1121 if (mismatch_lo * mismatch_hi > 0.0) {
1123 << TimeStamp()
1124 << " RunCDFT: could not bracket a root for the population "
1125 "mismatch after "
1126 << kMaxBracketAttempts
1127 << " bracket-expansion attempts -- the target population may "
1128 "be unreachable for this system, or the initial "
1129 "lambda guess may be far from the actual root."
1130 << std::flush;
1131 initial_guess_ = saved_initial_guess;
1132 constraints_.clear();
1133 return false;
1134 }
1135
1136 for (Index outer_iter = 0; outer_iter < max_cdft_iterations_;
1137 ++outer_iter) {
1138 double lambda_mid = 0.5 * (lambda_lo + lambda_hi);
1139 double mismatch_mid = EvaluateMismatch(lambda_mid);
1140
1142 << TimeStamp() << " CDFT outer iteration " << outer_iter + 1 << " of "
1143 << max_cdft_iterations_ << ": lambda=" << lambda_mid
1144 << " population mismatch=" << mismatch_mid << std::flush;
1145
1146 if (std::abs(mismatch_mid) < cdft_population_tolerance_) {
1147 constraint.lambda = lambda_mid;
1148 initial_guess_ = saved_initial_guess;
1150 << TimeStamp() << " CDFT converged after " << outer_iter + 1
1151 << " outer iterations, lambda=" << lambda_mid << std::flush;
1152 return true;
1153 }
1154
1155 if (mismatch_mid * mismatch_lo < 0.0) {
1156 lambda_hi = lambda_mid;
1157 mismatch_hi = mismatch_mid;
1158 } else {
1159 lambda_lo = lambda_mid;
1160 mismatch_lo = mismatch_mid;
1161 }
1162 }
1163 } catch (const std::runtime_error&) {
1164 initial_guess_ = saved_initial_guess;
1165 constraints_.clear();
1166 throw;
1167 }
1168
1170 << TimeStamp()
1171 << " RunCDFT: outer bisection loop did not converge "
1172 "within "
1173 << max_cdft_iterations_ << " iterations." << std::flush;
1174 constraint.lambda = 0.5 * (lambda_lo + lambda_hi);
1175 initial_guess_ = saved_initial_guess;
1176 return false;
1177}
1178
1179// Restricted SCF loop. The total energy is assembled as
1180//
1181// E = Tr[P H0] + E_nuc + E_coul + E_xc + E_exx,
1182//
1183// with P = 2 C_occ C_occ^T. DIIS or mixing updates the density until both
1184// the energy change and the commutator error are converged.
1186 Orbitals& orb, const Mat_p_Energy& H0,
1187 const Vxc_Potential<Vxc_Grid>& vxcpotential) {
1188
1190 MOs.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
1191 MOs.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
1192
1193 if (initial_guess_ == "orbfile") {
1195 << TimeStamp() << " Reading guess from orbitals object/file"
1196 << std::flush;
1197 MOs = orb.MOs();
1199 } else {
1201 << TimeStamp() << " Setup Initial Guess using: " << initial_guess_
1202 << std::flush;
1203 if (initial_guess_ == "independent") {
1204 MOs = IndependentElectronGuess(H0);
1205 } else if (initial_guess_ == "atom") {
1206 MOs = ModelPotentialGuess(H0, orb.QMAtoms(), vxcpotential);
1207 } else if (initial_guess_ == "huckel") {
1208 MOs = ExtendedHuckelGuess(orb.QMAtoms());
1209 } else if (initial_guess_ == "huckel_dft") {
1210 MOs = ExtendedHuckelDFTGuess(H0, orb.QMAtoms(), vxcpotential);
1211 } else if (initial_guess_ == "dimer_guess") {
1212 throw std::runtime_error(
1213 "initial_guess=dimer_guess is only meaningful for a genuinely "
1214 "unrestricted (UKS) calculation -- it exists specifically to "
1215 "combine two monomers of independently arbitrary charge/spin, "
1216 "which by construction do not reduce to a single restricted "
1217 "MO set. Use force_uks_path or an odd total electron count to "
1218 "route through EvaluateUKS instead.");
1219 } else {
1220 throw std::runtime_error("Initial guess method not known/implemented");
1221 }
1222 }
1223
1224 ConvergenceAcc::SpinDensity spin_dmat =
1225 conv_accelerator_.DensityMatrixSpinResolved(MOs);
1226 Eigen::MatrixXd Dmat = spin_dmat.total();
1227
1229 << TimeStamp() << " Guess Matrix gives N=" << std::setprecision(9)
1230 << Dmat.cwiseProduct(dftAOoverlap_.Matrix()).sum() << " electrons."
1231 << std::flush;
1232
1234 << TimeStamp() << " STARTING SCF cycle" << std::flush;
1236 << " ----------------------------------------------"
1237 "----------------------------"
1238 << std::flush;
1239
1240 Eigen::MatrixXd J = Eigen::MatrixXd::Zero(Dmat.rows(), Dmat.cols());
1241 Eigen::MatrixXd K;
1242 if (ScaHFX_ > 0) {
1243 K = Eigen::MatrixXd::Zero(Dmat.rows(), Dmat.cols());
1244 }
1245
1246 double start_incremental_F_threshold = 1e-4;
1247 if (!auxbasis_name_.empty()) {
1248 start_incremental_F_threshold = 0.0; // Disable if RI is used
1249 }
1250 IncrementalFockBuilder incremental_fock(*pLog_, start_incremental_F_threshold,
1252 incremental_fock.Configure(Dmat);
1253
1254 for (Index this_iter = 0; this_iter < max_iter_; this_iter++) {
1255 XTP_LOG(Log::error, *pLog_) << std::flush;
1256 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Iteration " << this_iter + 1
1257 << " of " << max_iter_ << std::flush;
1258
1259 Mat_p_Energy e_vxc = vxcpotential.IntegrateVXC(Dmat);
1261 << TimeStamp() << " Filled DFT Vxc matrix " << std::flush;
1262
1263 Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
1264 double Eone = Dmat.cwiseProduct(H0.matrix()).sum();
1265 double Etwo = e_vxc.energy();
1266 double exx = 0.0;
1267
1268 incremental_fock.Start(this_iter, conv_accelerator_.getDIIsError());
1269 incremental_fock.resetMatrices(J, K, Dmat);
1270 incremental_fock.UpdateCriteria(conv_accelerator_.getDIIsError(),
1271 this_iter);
1272
1273 double integral_error =
1274 std::min(conv_accelerator_.getDIIsError() * 1e-5, 1e-5);
1275
1276 if (ScaHFX_ > 0) {
1277 std::array<Eigen::MatrixXd, 2> both = CalcERIs_EXX(
1278 MOs.eigenvectors(), incremental_fock.getDmat_diff(), integral_error);
1279 J += both[0];
1280 H += J;
1281 Etwo += 0.5 * Dmat.cwiseProduct(J).sum();
1282 K += both[1];
1283 H += 0.5 * ScaHFX_ * K;
1284 exx = 0.25 * ScaHFX_ * Dmat.cwiseProduct(K).sum();
1286 << TimeStamp() << " Filled F+K matrix " << std::flush;
1287 } else {
1288 J += CalcERIs(incremental_fock.getDmat_diff(), integral_error);
1290 << TimeStamp() << " Filled F matrix " << std::flush;
1291 H += J;
1292 Etwo += 0.5 * Dmat.cwiseProduct(J).sum();
1293 }
1294
1295 Etwo += exx;
1296 double totenergy = Eone + H0.energy() + Etwo;
1297
1298 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Single particle energy "
1299 << std::setprecision(12) << Eone << std::flush;
1300 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Two particle energy "
1301 << std::setprecision(12) << Etwo << std::flush;
1303 << TimeStamp() << std::setprecision(12) << " Local Exc contribution "
1304 << e_vxc.energy() << std::flush;
1305 if (ScaHFX_ > 0) {
1307 << TimeStamp() << std::setprecision(12)
1308 << " Non local Ex contribution " << exx << std::flush;
1309 }
1311 << TimeStamp() << " Total Energy " << std::setprecision(12) << totenergy
1312 << std::flush;
1313
1314 Dmat = conv_accelerator_.Iterate(Dmat, H, MOs, totenergy);
1315 incremental_fock.UpdateDmats(Dmat, conv_accelerator_.getDIIsError(),
1316 this_iter);
1317
1319
1320 if (num_docc_ + num_socc_alpha_ > 0 &&
1321 num_docc_ + num_socc_alpha_ < MOs.eigenvalues().size()) {
1323 << "\t\tGAP "
1326 << std::flush;
1327 }
1328
1329 if (conv_accelerator_.isConverged()) {
1331 << TimeStamp() << " Total Energy has converged to "
1332 << std::setprecision(9) << conv_accelerator_.getDeltaE()
1333 << "[Ha] after " << this_iter + 1
1334 << " iterations. DIIS error is converged up to "
1335 << conv_accelerator_.getDIIsError() << std::flush;
1337 << TimeStamp() << " Final Single Point Energy "
1338 << std::setprecision(12) << totenergy << " Ha" << std::flush;
1339 XTP_LOG(Log::error, *pLog_) << TimeStamp() << std::setprecision(12)
1340 << " Final Local Exc contribution "
1341 << e_vxc.energy() << " Ha" << std::flush;
1342 if (ScaHFX_ > 0) {
1343 XTP_LOG(Log::error, *pLog_) << TimeStamp() << std::setprecision(12)
1344 << " Final Non Local Ex contribution "
1345 << exx << " Ha" << std::flush;
1346 }
1347
1349
1350 Index nuclear_charge = 0;
1351 for (const QMAtom& atom : orb.QMAtoms()) {
1352 nuclear_charge += atom.getNuccharge();
1353 }
1354
1355 orb.setQMEnergy(totenergy);
1356 orb.MOs() = MOs;
1360 orb.setChargeAndSpin(
1361 nuclear_charge - numofelectrons_,
1363
1364 if (compute_forces_) {
1365 ComputeAndStoreForces(orb, Dmat, vxcpotential);
1366 }
1367
1368 CalcElDipole(orb);
1369 return true;
1370 } else if (this_iter == max_iter_ - 1) {
1372 << TimeStamp() << " DFT calculation has not converged after "
1373 << max_iter_
1374 << " iterations. Use more iterations or another convergence "
1375 "acceleration scheme."
1376 << std::flush;
1377 return false;
1378 }
1379 }
1380
1381 return true;
1382}
1383
1384// Unrestricted SCF loop. The alpha and beta channels are iterated through
1385// separate Fock matrices
1386//
1387// F^alpha = H0 + J[P^alpha + P^beta] + V_xc^alpha + K^alpha
1388// F^beta = H0 + J[P^alpha + P^beta] + V_xc^beta + K^beta,
1389//
1390// while the total energy uses the spin-summed one-electron and Coulomb terms
1391// together with spin-resolved XC and exact-exchange contributions.
1393 const Vxc_Potential<Vxc_Grid>& vxcpotential) {
1394 tools::EigenSystem MOs_alpha;
1395 tools::EigenSystem MOs_beta;
1396
1397 MOs_alpha.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
1398 MOs_alpha.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
1399 MOs_beta.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
1400 MOs_beta.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
1401
1402 UKSConvergenceAcc conv_uks;
1403
1407
1411
1412 conv_uks.Configure(opt_alpha, opt_beta);
1413 conv_uks.setLogger(pLog_);
1414 conv_uks.setOverlap(dftAOoverlap_, 1e-8);
1415
1416 if (initial_guess_ == "orbfile") {
1418 << TimeStamp() << " Reading UKS guess from orbitals object/file"
1419 << std::flush;
1420
1421 MOs_alpha = orb.MOs();
1422 MOs_alpha.eigenvectors() = OrthogonalizeGuess(MOs_alpha.eigenvectors());
1423
1424 if (orb.hasBetaMOs()) {
1425 MOs_beta = orb.MOs_beta();
1426 MOs_beta.eigenvectors() = OrthogonalizeGuess(MOs_beta.eigenvectors());
1427 } else {
1429 << TimeStamp()
1430 << " Orbital file has no beta MOs, using alpha guess for beta."
1431 << std::flush;
1432 MOs_beta = MOs_alpha;
1433 }
1434 } else if (initial_guess_ == "dimer_guess") {
1436 << TimeStamp()
1437 << " Building UKS guess from two monomer .orb files (dimer_guess)"
1438 << std::flush;
1439 Orbitals dimer_guess_orb = BuildDimerGuessFromMonomerFiles(orb.QMAtoms());
1440 MOs_alpha = dimer_guess_orb.MOs();
1441 MOs_alpha.eigenvectors() = OrthogonalizeGuess(MOs_alpha.eigenvectors());
1442 MOs_beta = dimer_guess_orb.MOs_beta();
1443 MOs_beta.eigenvectors() = OrthogonalizeGuess(MOs_beta.eigenvectors());
1444 } else {
1446 << TimeStamp() << " Setup UKS Initial Guess using: " << initial_guess_
1447 << std::flush;
1448
1449 tools::EigenSystem guess;
1450 if (initial_guess_ == "independent") {
1451 guess = IndependentElectronGuess(H0);
1452 } else if (initial_guess_ == "atom") {
1453 guess = ModelPotentialGuess(H0, orb.QMAtoms(), vxcpotential);
1454 } else if (initial_guess_ == "huckel") {
1455 guess = ExtendedHuckelGuess(orb.QMAtoms());
1456 } else if (initial_guess_ == "huckel_dft") {
1457 guess = ExtendedHuckelDFTGuess(H0, orb.QMAtoms(), vxcpotential);
1458 } else {
1459 throw std::runtime_error("Initial guess method not known/implemented");
1460 }
1461
1462 MOs_alpha = guess;
1463 MOs_beta = guess;
1464 }
1465
1466 // Build the initial spin densities P^alpha and P^beta from the chosen
1467 // starting orbitals before entering the coupled UKS iterations.
1469 conv_uks.DensityMatrix(MOs_alpha, MOs_beta);
1470
1472 << TimeStamp() << " UKS guess gives Nalpha="
1473 << Dspin.alpha.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1474 << " Nbeta=" << Dspin.beta.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1475 << " Ntot=" << Dspin.total().cwiseProduct(dftAOoverlap_.Matrix()).sum()
1476 << std::flush;
1477
1479 << TimeStamp() << " STARTING UKS SCF cycle" << std::flush;
1481 << " ------------------------------------------------------------"
1482 << std::flush;
1483
1484 for (Index this_iter = 0; this_iter < max_iter_; ++this_iter) {
1485 XTP_LOG(Log::error, *pLog_) << std::flush;
1486 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Iteration " << this_iter + 1
1487 << " of " << max_iter_ << std::flush;
1488
1489 Eigen::MatrixXd H_alpha = H0.matrix();
1490 Eigen::MatrixXd H_beta = H0.matrix();
1491
1492 // The Coulomb contribution depends only on the total density
1493 // P = P^alpha + P^beta, while exchange and XC remain spin resolved.
1494 const Eigen::MatrixXd D_total = Dspin.total();
1495
1496 double E_one = Dspin.alpha.cwiseProduct(H0.matrix()).sum() +
1497 Dspin.beta.cwiseProduct(H0.matrix()).sum();
1498
1499 double E_coul = 0.0;
1500 double E_xc = 0.0;
1501 double E_exx = 0.0;
1502
1503 double integral_error = std::min(conv_uks.getDIIsError() * 1e-5, 1e-5);
1504
1505 if (ScaHFX_ > 0) {
1506 std::array<Eigen::MatrixXd, 2> both_alpha = CalcERIs_EXX(
1507 Eigen::MatrixXd::Zero(0, 0), Dspin.alpha, integral_error);
1508 std::array<Eigen::MatrixXd, 2> both_beta =
1509 CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dspin.beta, integral_error);
1510
1511 Eigen::MatrixXd J = both_alpha[0] + both_beta[0];
1512 Eigen::MatrixXd K_alpha = both_alpha[1];
1513 Eigen::MatrixXd K_beta = both_beta[1];
1514
1515 H_alpha += J + ScaHFX_ * K_alpha;
1516 H_beta += J + ScaHFX_ * K_beta;
1517
1518 E_coul = 0.5 * D_total.cwiseProduct(J).sum();
1519 E_exx = 0.5 * ScaHFX_ *
1520 (Dspin.alpha.cwiseProduct(K_alpha).sum() +
1521 Dspin.beta.cwiseProduct(K_beta).sum());
1522 } else {
1523 Eigen::MatrixXd J = CalcERIs(D_total, integral_error);
1524 H_alpha += J;
1525 H_beta += J;
1526 E_coul = 0.5 * D_total.cwiseProduct(J).sum();
1527 }
1528
1529 auto vxc = vxcpotential.IntegrateVXCSpin(Dspin.alpha, Dspin.beta);
1530 H_alpha += vxc.vxc_alpha;
1531 H_beta += vxc.vxc_beta;
1532 E_xc = vxc.energy;
1533
1534 double totenergy = H0.energy() + E_one + E_coul + E_xc + E_exx;
1535
1536 // CDFT constraint potential -- deliberately the LAST term added to
1537 // either Fock matrix, and gated by a single, cheap .empty() check:
1538 // for any standard, non-CDFT run (constraints_ left at its default,
1539 // empty state), this entire block is skipped, and both H_alpha and
1540 // H_beta are built exactly as they always were -- no measurable
1541 // overhead, no behavior change whatsoever. Adds
1542 // lambda_c * spin_alpha/beta_coefficient * W_c to the respective
1543 // Fock matrix for every active constraint c (a charge constraint
1544 // uses +1/+1, adding the identical potential to both channels; a
1545 // future spin constraint would use +1/-1 -- see Constraint's own
1546 // comment in hirshfeldpartition.h for why these are stored
1547 // separately rather than this code assuming "charge" specifically),
1548 // and the corresponding correction term to the reported total
1549 // energy: E_CDFT = E_KS + sum_c lambda_c * (N_c^computed -
1550 // N_c^target), the standard Wu-Van Voorhis Lagrangian.
1551 if (!constraints_.empty()) {
1553 H_alpha += (c.lambda * c.spin_alpha_coefficient) * c.weight_matrix;
1554 H_beta += (c.lambda * c.spin_beta_coefficient) * c.weight_matrix;
1555 double population =
1556 c.spin_alpha_coefficient *
1557 Dspin.alpha.cwiseProduct(c.weight_matrix).sum() +
1558 c.spin_beta_coefficient *
1559 Dspin.beta.cwiseProduct(c.weight_matrix).sum();
1560 totenergy += c.lambda * (population - c.target_population);
1561 }
1562 }
1563
1564 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " One particle energy "
1565 << std::setprecision(12) << E_one << std::flush;
1566 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Coulomb contribution "
1567 << std::setprecision(12) << E_coul << std::flush;
1568 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " XC contribution "
1569 << std::setprecision(12) << E_xc << std::flush;
1570 if (ScaHFX_ > 0) {
1572 << TimeStamp() << " EXX contribution " << std::setprecision(12)
1573 << E_exx << std::flush;
1574 }
1576 << TimeStamp() << " Total Energy " << std::setprecision(12) << totenergy
1577 << std::flush;
1578
1579 UKSConvergenceAcc::SpinFock Hspin{H_alpha, H_beta};
1580
1581 // Coupled Fock builder: both new densities are used together, for
1582 // BOTH the Coulomb/exchange terms and the XC potential, in a single
1583 // call. This is what lets CoupledAugmentedHessianStep/
1584 // BuildCoupledSigmaVector capture the real alpha-beta coupling
1585 // (through the shared Coulomb potential and the XC kernel's
1586 // cross-spin terms). Mirrors the exact same H0 + Coulomb/exchange +
1587 // XC sequence already used to build H_alpha/H_beta themselves just
1588 // above, so a perturbed density that happens to equal the current
1589 // one reproduces the identical Fock matrix.
1590 conv_uks.setCoupledFockBuilder(
1591 [this, &H0, &vxcpotential](
1592 const Eigen::MatrixXd& alpha_new,
1593 const Eigen::MatrixXd& beta_new) -> UKSConvergenceAcc::SpinFock {
1595 H_new.alpha = H0.matrix();
1596 H_new.beta = H0.matrix();
1597 constexpr double kIntegralError = 1e-8;
1598 if (ScaHFX_ > 0) {
1599 std::array<Eigen::MatrixXd, 2> both_alpha_new = CalcERIs_EXX(
1600 Eigen::MatrixXd::Zero(0, 0), alpha_new, kIntegralError);
1601 std::array<Eigen::MatrixXd, 2> both_beta_new = CalcERIs_EXX(
1602 Eigen::MatrixXd::Zero(0, 0), beta_new, kIntegralError);
1603 Eigen::MatrixXd J_new = both_alpha_new[0] + both_beta_new[0];
1604 H_new.alpha += J_new + ScaHFX_ * both_alpha_new[1];
1605 H_new.beta += J_new + ScaHFX_ * both_beta_new[1];
1606 } else {
1607 Eigen::MatrixXd D_total_new = alpha_new + beta_new;
1608 Eigen::MatrixXd J_new = CalcERIs(D_total_new, kIntegralError);
1609 H_new.alpha += J_new;
1610 H_new.beta += J_new;
1611 }
1612 auto vxc_new = vxcpotential.IntegrateVXCSpin(alpha_new, beta_new);
1613 H_new.alpha += vxc_new.vxc_alpha;
1614 H_new.beta += vxc_new.vxc_beta;
1615 return H_new;
1616 });
1617
1618 Dspin = conv_uks.Iterate(Dspin, Hspin, MOs_alpha, MOs_beta, totenergy);
1620 MOs_beta = MOs_alpha;
1621 Dspin.beta = Dspin.alpha;
1622 }
1623
1625 << TimeStamp()
1626 << " Nalpha=" << Dspin.alpha.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1627 << " Nbeta=" << Dspin.beta.cwiseProduct(dftAOoverlap_.Matrix()).sum()
1628 << std::flush;
1629
1631 << TimeStamp() << " <Sz> = "
1632 << 0.5 * double(num_alpha_electrons_ - num_beta_electrons_)
1633 << std::flush;
1634
1635 PrintMOsUKS(MOs_alpha.eigenvalues(), MOs_beta.eigenvalues(), Log::info);
1636
1637 if (conv_uks.isConverged()) {
1638 Index nuclear_charge = 0;
1639 for (const QMAtom& atom : orb.QMAtoms()) {
1640 nuclear_charge += atom.getNuccharge();
1641 }
1642
1643 CanonicalizeOrbitalPhases(MOs_alpha);
1644 CanonicalizeOrbitalPhases(MOs_beta);
1645
1646 orb.setQMEnergy(totenergy);
1647 orb.MOs() = MOs_alpha;
1648 orb.MOs_beta() = MOs_beta;
1653 orb.setChargeAndSpin(
1654 nuclear_charge - numofelectrons_,
1656
1658 << TimeStamp() << " UKS converged after " << this_iter + 1
1659 << " iterations. Delta E=" << conv_uks.getDeltaE()
1660 << " DIIS error=" << conv_uks.getDIIsError() << std::flush;
1661
1663 << TimeStamp() << " Final Single Point Energy "
1664 << std::setprecision(12) << totenergy << " Ha" << std::flush;
1666 << TimeStamp() << std::setprecision(12) << " Final XC contribution "
1667 << E_xc << " Ha" << std::flush;
1668 if (ScaHFX_ > 0) {
1670 << TimeStamp() << std::setprecision(12)
1671 << " Final EXX contribution " << E_exx << " Ha" << std::flush;
1672 }
1673
1675 << TimeStamp() << " <Sz> = "
1676 << 0.5 * double(num_alpha_electrons_ - num_beta_electrons_)
1677 << std::flush;
1678
1679 PrintMOsUKS(MOs_alpha.eigenvalues(), MOs_beta.eigenvalues(), Log::error);
1680
1681 if (compute_forces_) {
1682 ComputeAndStoreForcesUKS(orb, Dspin, MOs_alpha, MOs_beta, vxcpotential);
1683 }
1684
1685 CalcElDipole(orb);
1686 return true;
1687 }
1688
1689 if (this_iter == max_iter_ - 1) {
1691 << TimeStamp() << " UKS calculation has not converged after "
1692 << max_iter_ << " iterations." << std::flush;
1693 return false;
1694 }
1695 }
1696
1697 return false;
1698}
1699
1700// One-electron core Hamiltonian and its constant energy offset.
1701//
1702// The matrix part is
1703//
1704// H0 = T + V_nuc + V_ECP + V_ext,
1705//
1706// while the scalar energy collects all nucleus-nucleus and nucleus-external
1707// interaction terms that do not depend on the electronic density.
1709
1710 AOKinetic dftAOkinetic;
1711
1712 dftAOkinetic.Fill(dftbasis_);
1714 << TimeStamp() << " Filled DFT Kinetic energy matrix ." << std::flush;
1715
1716 AOMultipole dftAOESP;
1717 dftAOESP.FillPotential(dftbasis_, mol);
1719 << TimeStamp() << " Filled DFT nuclear potential matrix." << std::flush;
1720
1721 Eigen::MatrixXd H0 = dftAOkinetic.Matrix() + dftAOESP.Matrix();
1723 << TimeStamp() << " Constructed independent particle hamiltonian "
1724 << std::flush;
1725 double E0 = NuclearRepulsion(mol);
1726 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Nuclear Repulsion Energy is "
1727 << std::setprecision(9) << E0 << std::flush;
1728
1729 if (!ecp_name_.empty()) {
1730 AOECP dftAOECP;
1731 dftAOECP.FillPotential(dftbasis_, ecp_);
1732 H0 += dftAOECP.Matrix();
1734 << TimeStamp() << " Filled DFT ECP matrix" << std::flush;
1735 }
1736
1737 if (externalsites_ != nullptr) {
1738 XTP_LOG(Log::error, *pLog_) << TimeStamp() << " " << externalsites_->size()
1739 << " External sites" << std::flush;
1740 bool has_quadrupoles = std::any_of(
1741 externalsites_->begin(), externalsites_->end(),
1742 [](const std::unique_ptr<StaticSite>& s) { return s->getRank() == 2; });
1743 std::string header =
1744 " Name Coordinates[a0] charge[e] dipole[e*a0] ";
1745 if (has_quadrupoles) {
1746 header += " quadrupole[e*a0^2]";
1747 }
1748 XTP_LOG(Log::error, *pLog_) << header << std::flush;
1749 Index limit = 50;
1750 Index counter = 0;
1751 for (const std::unique_ptr<StaticSite>& site : *externalsites_) {
1752 if (counter == limit) {
1753 break;
1754 }
1755 std::string output =
1756 (boost::format(" %1$s"
1757 " %2$+1.4f %3$+1.4f %4$+1.4f"
1758 " %5$+1.4f") %
1759 site->getElement() % site->getPos()[0] % site->getPos()[1] %
1760 site->getPos()[2] % site->getCharge())
1761 .str();
1762 const Eigen::Vector3d& dipole = site->getDipole();
1763 output += (boost::format(" %1$+1.4f %2$+1.4f %3$+1.4f") % dipole[0] %
1764 dipole[1] % dipole[2])
1765 .str();
1766 if (site->getRank() > 1) {
1767 Eigen::VectorXd quadrupole = site->Q().tail<5>();
1768 output +=
1769 (boost::format(" %1$+1.4f %2$+1.4f %3$+1.4f %4$+1.4f %5$+1.4f") %
1770 quadrupole[0] % quadrupole[1] % quadrupole[2] % quadrupole[3] %
1771 quadrupole[4])
1772 .str();
1773 }
1774 XTP_LOG(Log::error, *pLog_) << output << std::flush;
1775 counter++;
1776 }
1777 if (counter == limit) {
1779 << " ... (" << externalsites_->size() - limit
1780 << " sites not displayed)\n"
1781 << std::flush;
1782 }
1783
1784 Mat_p_Energy ext_multipoles =
1787 << TimeStamp() << " Nuclei-external site interaction energy "
1788 << std::setprecision(9) << ext_multipoles.energy() << std::flush;
1789 E0 += ext_multipoles.energy();
1790 H0 += ext_multipoles.matrix();
1791 }
1792
1794 Orbitals extdensity;
1795 extdensity.ReadFromCpt(orbfilename_);
1796 Mat_p_Energy extdensity_result = IntegrateExternalDensity(mol, extdensity);
1797 E0 += extdensity_result.energy();
1799 << TimeStamp() << " Nuclei-external density interaction energy "
1800 << std::setprecision(9) << extdensity_result.energy() << std::flush;
1801 H0 += extdensity_result.matrix();
1802 }
1803
1805
1807 << TimeStamp() << " Integrating external electric field with F[Hrt]="
1808 << extfield_.transpose() << std::flush;
1809 H0 += IntegrateExternalField(mol);
1810 }
1811
1812 return Mat_p_Energy(E0, H0);
1813}
1814
1815// Precompute SCF-invariant matrices: overlap for the generalized eigenvalue
1816// problem and the RI/4c electron-repulsion backend that later yields J[P] and
1817// K[P].
1821 << TimeStamp() << " Filled DFT Overlap matrix." << std::flush;
1822
1823 conv_opt_.numberofelectrons = numofelectrons_;
1824 conv_opt_.number_alpha_electrons = num_alpha_electrons_;
1825 conv_opt_.number_beta_electrons = num_beta_electrons_;
1829 conv_accelerator_.Configure(conv_opt_);
1830 conv_accelerator_.setLogger(pLog_);
1831 conv_accelerator_.setOverlap(dftAOoverlap_, 1e-8);
1832 conv_accelerator_.PrintConfigOptions();
1833
1834 if (!auxbasis_name_.empty()) {
1835 // prepare invariant part of electron repulsion integrals
1836 ERIs_.Initialize(dftbasis_, auxbasis_);
1838 << TimeStamp() << " Inverted AUX Coulomb matrix, removed "
1839 << ERIs_.Removedfunctions() << " functions from aux basis"
1840 << std::flush;
1842 << TimeStamp()
1843 << " Setup invariant parts of Electron Repulsion integrals "
1844 << std::flush;
1845 } else {
1847 << TimeStamp() << " Calculating 4c diagonals. " << std::flush;
1848 ERIs_.Initialize_4c(dftbasis_);
1850 << TimeStamp() << " Calculated 4c diagonals. " << std::flush;
1851 }
1852
1853 return;
1854}
1855
1856namespace {
1857// Hund's-rule ground-state (alpha electrons, beta electrons) for the
1858// main-group (s/p-block) elements most relevant to organic systems --
1859// H through Kr, plus the heavier halogens (Br, I) via their own,
1860// separately-computed period-5 entries. Explicitly does NOT cover
1861// d-block (Sc-Zn, Y-Cd) or f-block elements: the d^n s^2 vs d^(n+1) s^1
1862// (and worse, f-block) ground-state competition is genuinely subtle
1863// and functional-dependent -- exactly why CP2K's own isolated-atom
1864// ("ATOM") program requires explicit, manual per-subshell occupation
1865// specification rather than trusting any automatic rule (confirmed
1866// directly: HORTON's own CP2K pro-atom documentation states "The ATOM
1867// program of CP2K does not simply follow the Aufbau rule to assign
1868// orbital occupations"). Returns std::nullopt for anything not
1869// explicitly covered, so callers can fall back to the existing,
1870// simpler parity-based logic with a clear warning rather than silently
1871// guessing.
1872//
1873// Method: standard Aufbau filling order (1s,2s,2p,3s,3p,4s,3d,4p,5s,
1874// 4d,5p) up to (but explicitly skipping) each d-block range, applying
1875// Hund's rule within any open p subshell (spread across all 3 p
1876// orbitals with parallel/majority spin first, only pairing once every
1877// orbital in that subshell already has one) -- for p^n, n<=3 gives n
1878// alpha/0 beta in that subshell; n>3 gives 3 alpha/(n-3) beta. Every
1879// entry below was computed by hand from this rule and can be checked
1880// against any standard table of atomic ground-state term symbols
1881// (all are unambiguous, textbook Hund's-rule cases for main-group
1882// atoms -- no functional-dependent ambiguity of the kind that affects
1883// d/f-block).
1884std::optional<std::pair<Index, Index>> HundsRuleAlphaBetaElectrons(
1885 Index nuclear_charge) {
1886 switch (nuclear_charge) {
1887 case 1:
1888 return std::make_pair(1, 0); // H: 1s1
1889 case 2:
1890 return std::make_pair(1, 1); // He: 1s2
1891 case 3:
1892 return std::make_pair(2, 1); // Li: [He] 2s1
1893 case 4:
1894 return std::make_pair(2, 2); // Be: 2s2
1895 case 5:
1896 return std::make_pair(3, 2); // B: 2p1
1897 case 6:
1898 return std::make_pair(4, 2); // C: 2p2 (2a)
1899 case 7:
1900 return std::make_pair(5, 2); // N: 2p3 (3a)
1901 case 8:
1902 return std::make_pair(5, 3); // O: 2p4 (3a+1b)
1903 case 9:
1904 return std::make_pair(5, 4); // F: 2p5 (3a+2b)
1905 case 10:
1906 return std::make_pair(5, 5); // Ne: 2p6
1907 case 11:
1908 return std::make_pair(6, 5); // Na: [Ne] 3s1
1909 case 12:
1910 return std::make_pair(6, 6); // Mg: 3s2
1911 case 13:
1912 return std::make_pair(7, 6); // Al: 3p1
1913 case 14:
1914 return std::make_pair(8, 6); // Si: 3p2 (2a)
1915 case 15:
1916 return std::make_pair(9, 6); // P: 3p3 (3a)
1917 case 16:
1918 return std::make_pair(9, 7); // S: 3p4 (3a+1b)
1919 case 17:
1920 return std::make_pair(9, 8); // Cl: 3p5 (3a+2b)
1921 case 18:
1922 return std::make_pair(9, 9); // Ar: 3p6
1923 case 19:
1924 return std::make_pair(10, 9); // K: [Ar] 4s1
1925 case 20:
1926 return std::make_pair(10, 10); // Ca: 4s2
1927 // 21-30 (Sc-Zn): 3d block -- deliberately NOT covered.
1928 case 31:
1929 return std::make_pair(16, 15); // Ga: [Zn] 4p1
1930 case 32:
1931 return std::make_pair(17, 15); // Ge: 4p2 (2a)
1932 case 33:
1933 return std::make_pair(18, 15); // As: 4p3 (3a)
1934 case 34:
1935 return std::make_pair(18, 16); // Se: 4p4 (3a+1b)
1936 case 35:
1937 return std::make_pair(18, 17); // Br: 4p5 (3a+2b)
1938 case 36:
1939 return std::make_pair(18, 18); // Kr: 4p6
1940 // 39-48 (Y-Cd): 4d block -- deliberately NOT covered.
1941 case 49:
1942 return std::make_pair(25, 24); // In: [Cd] 5p1
1943 case 50:
1944 return std::make_pair(26, 24); // Sn: 5p2 (2a)
1945 case 51:
1946 return std::make_pair(27, 24); // Sb: 5p3 (3a)
1947 case 52:
1948 return std::make_pair(27, 25); // Te: 5p4 (3a+1b)
1949 case 53:
1950 return std::make_pair(27, 26); // I: 5p5 (3a+2b)
1951 case 54:
1952 return std::make_pair(27, 27); // Xe: 5p6
1953 default:
1954 return std::nullopt;
1955 }
1956}
1957} // namespace
1958
1960 const QMAtom& uniqueAtom, bool use_hunds_rule_occupation) const {
1961 bool with_ecp = !ecp_name_.empty();
1962 if (uniqueAtom.getElement() == "H" || uniqueAtom.getElement() == "He") {
1963 with_ecp = false;
1964 }
1965
1966 QMMolecule atom = QMMolecule("individual_atom", 0);
1967 atom.push_back(uniqueAtom);
1968
1969 BasisSet basisset;
1970 basisset.Load(dftbasis_name_);
1971 AOBasis dftbasis;
1972 dftbasis.Fill(basisset, atom);
1973 Vxc_Grid grid;
1974 grid.GridSetup(grid_name_, atom, dftbasis);
1975 Vxc_Potential<Vxc_Grid> gridIntegration(grid);
1976 gridIntegration.setXCfunctional(xc_functional_name_);
1977
1978 ECPAOBasis ecp;
1979 if (with_ecp) {
1980 ECPBasisSet ecps;
1981 ecps.Load(ecp_name_);
1982 ecp.Fill(ecps, atom);
1983 }
1984
1985 Index numofelectrons = uniqueAtom.getNuccharge();
1986 Index alpha_e = 0;
1987 Index beta_e = 0;
1988
1989 // Deliberately opt-in, defaulting to false: this changes ONLY which
1990 // total alpha/beta split is used for the reference atom's own SCF,
1991 // not the SphericalAverageShells step below (kept unconditionally,
1992 // for both modes) -- the existing SAD-initial-guess caller
1993 // (AtomicGuess) is not changed at all by this parameter existing, and
1994 // continues to use the simpler, parity-based split exactly as
1995 // before. A physically correct free-atom ground state is not needed
1996 // for a good SCF starting guess (the molecule's own overall spin
1997 // state, and the SCF that follows, will reshape this regardless);
1998 // it matters for the promolecular reference densities Hirshfeld-based
1999 // CDFT will need instead, which is what this parameter exists for.
2000 if (use_hunds_rule_occupation) {
2001 auto hunds_rule = HundsRuleAlphaBetaElectrons(numofelectrons);
2002 if (hunds_rule.has_value()) {
2003 alpha_e = hunds_rule->first;
2004 beta_e = hunds_rule->second;
2005 } else {
2007 << TimeStamp()
2008 << " No Hund's-rule ground-state occupation table "
2009 "entry for nuclear charge "
2010 << numofelectrons
2011 << " (d/f-block elements are not covered -- see "
2012 "HundsRuleAlphaBetaElectrons's own comment for why) -- "
2013 "falling back to the simpler, parity-based alpha/beta split."
2014 << std::flush;
2015 use_hunds_rule_occupation = false;
2016 }
2017 }
2018 if (!use_hunds_rule_occupation) {
2019 if ((numofelectrons % 2) != 0) {
2020 alpha_e = numofelectrons / 2 + numofelectrons % 2;
2021 beta_e = numofelectrons / 2;
2022 } else {
2023 alpha_e = numofelectrons / 2;
2024 beta_e = alpha_e;
2025 }
2026 }
2027
2028 AOOverlap dftAOoverlap;
2029 AOKinetic dftAOkinetic;
2030 AOMultipole dftAOESP;
2031 AOECP dftAOECP;
2032 ERIs ERIs_atom;
2033
2034 dftAOoverlap.Fill(dftbasis);
2035 dftAOkinetic.Fill(dftbasis);
2036
2037 dftAOESP.FillPotential(dftbasis, atom);
2038 ERIs_atom.Initialize_4c(dftbasis);
2039
2040 UKSConvergenceAcc conv_uks;
2043 opt_alpha.histlength = 20;
2044 opt_alpha.levelshift = 0.1;
2045 opt_alpha.levelshiftend = 0.0;
2046 opt_alpha.usediis = true;
2047 // adiis_start/diis_start deliberately NOT overridden here (previously
2048 // both hardcoded to 0.0) -- confirmed via ConvergenceAcc::Iterate's
2049 // own gating logic (the "diiserror_ < opt_.adiis_start ||
2050 // diiserror_ < opt_.diis_start" check) that 0.0 makes this condition
2051 // permanently false, since diiserror_ is a norm and can never be
2052 // negative. That silently disabled BOTH DIIS and ADIIS for the
2053 // entire atomic SCF regardless of usediis=true just above -- an
2054 // internal inconsistency, not an intentional design choice -- and is
2055 // the likely root cause of this function's own, separately reported
2056 // slow convergence (falling back to plain, level-shift-damped linear
2057 // mixing every single iteration, with no (A)DIIS extrapolation ever
2058 // actually applied). Inheriting these two fields from conv_opt_
2059 // (already the base for opt_alpha above, via "= conv_opt_") matches
2060 // the main UKS SCF path's own behavior, which never overrides them
2061 // at all.
2062 opt_alpha.numberofelectrons = alpha_e;
2063
2064 ConvergenceAcc::options opt_beta = opt_alpha;
2065 opt_beta.numberofelectrons = beta_e;
2066
2067 Logger log;
2068 // Single, shared accelerator -- previously two fully independent
2069 // ConvergenceAcc instances (Convergence_alpha, Convergence_beta),
2070 // each with its own separate DIIS history/error tracking/coefficient
2071 // calculation, with no coupling between the two spin channels at
2072 // all. The main UKS SCF path (DFTEngine::EvaluateUKS) instead uses
2073 // exactly this UKSConvergenceAcc class, which builds ONE combined
2074 // error metric from both spin channels together and applies a
2075 // single, jointly-derived set of (A)DIIS coefficients to both --
2076 // confirmed directly from uks_convergenceacc.cc's own comment ("one
2077 // shared DIIS/ADIIS history length") and its Iterate()'s own
2078 // "diis_.Update(maxerrorindex_, err_alpha, err_beta)" call. This is
2079 // the standard, textbook-correct formulation of UKS DIIS; the
2080 // previous two-independent-accelerators approach was not wrong in
2081 // the sense of being internally inconsistent (unlike the
2082 // adiis_start/diis_start bug above), but it let each spin channel's
2083 // extrapolation disagree with the other's, which is not how UKS DIIS
2084 // is meant to work.
2085 conv_uks.Configure(opt_alpha, opt_beta);
2086 conv_uks.setLogger(&log);
2087 conv_uks.setOverlap(dftAOoverlap, 1e-8);
2088
2089 Eigen::MatrixXd H0 = dftAOkinetic.Matrix() + dftAOESP.Matrix();
2090 if (with_ecp) {
2091 dftAOECP.FillPotential(dftbasis, ecp);
2092 H0 += dftAOECP.Matrix();
2093 }
2094
2095 tools::EigenSystem MOs_alpha = conv_uks.SolveFockmatrix(H0);
2096
2097 if (uniqueAtom.getElement() == "H") {
2098 // H has no beta electrons at all (beta_e=0 above) -- nocclevels_beta_
2099 // will be 0 once Configure() runs, and
2100 // DensityMatrixGroundState_unres already returns a zero matrix
2101 // whenever nocclevels==0 regardless of what MOs are passed in, so
2102 // reusing MOs_alpha as a dummy beta argument here is safe: only
2103 // Dspin_H.alpha is ever used below.
2105 conv_uks.DensityMatrix(MOs_alpha, MOs_alpha);
2106 return Dspin_H.alpha;
2107 }
2108
2109 tools::EigenSystem MOs_beta = conv_uks.SolveFockmatrix(H0);
2111 conv_uks.DensityMatrix(MOs_alpha, MOs_beta);
2112
2113 Index maxiter = 80;
2114 for (Index this_iter = 0; this_iter < maxiter; this_iter++) {
2115 Eigen::MatrixXd H_alpha = H0;
2116 Eigen::MatrixXd H_beta = H0;
2117
2118 double E_coul = 0.0;
2119 double E_exx = 0.0;
2120 double E_xc = 0.0;
2121
2122 // Matches EvaluateUKS's own formula exactly (a single combined
2123 // DIIS error now, not an average of two independent ones).
2124 double integral_error = std::min(conv_uks.getDIIsError() * 1e-5, 1e-5);
2125
2126 if (ScaHFX_ > 0) {
2127 std::array<Eigen::MatrixXd, 2> both_alpha =
2128 ERIs_atom.CalculateERIs_EXX_4c(Dspin.alpha, integral_error);
2129 std::array<Eigen::MatrixXd, 2> both_beta =
2130 ERIs_atom.CalculateERIs_EXX_4c(Dspin.beta, integral_error);
2131
2132 Eigen::MatrixXd Hartree = both_alpha[0] + both_beta[0];
2133 H_alpha += Hartree + ScaHFX_ * both_alpha[1];
2134 H_beta += Hartree + ScaHFX_ * both_beta[1];
2135
2136 E_coul = 0.5 * Dspin.total().cwiseProduct(Hartree).sum();
2137 E_exx = 0.5 * ScaHFX_ *
2138 (both_alpha[1].cwiseProduct(Dspin.alpha).sum() +
2139 both_beta[1].cwiseProduct(Dspin.beta).sum());
2140 } else {
2141 Eigen::MatrixXd Hartree =
2142 ERIs_atom.CalculateERIs_4c(Dspin.total(), integral_error);
2143 H_alpha += Hartree;
2144 H_beta += Hartree;
2145 E_coul = 0.5 * Dspin.total().cwiseProduct(Hartree).sum();
2146 }
2147
2148 auto vxc = gridIntegration.IntegrateVXCSpin(Dspin.alpha, Dspin.beta);
2149 H_alpha += vxc.vxc_alpha;
2150 H_beta += vxc.vxc_beta;
2151 E_xc = vxc.energy;
2152
2153 double E_one_alpha = Dspin.alpha.cwiseProduct(H0).sum();
2154 double E_one_beta = Dspin.beta.cwiseProduct(H0).sum();
2155 double totenergy = E_one_alpha + E_one_beta + E_coul + E_exx + E_xc;
2156
2157 UKSConvergenceAcc::SpinFock Hspin{H_alpha, H_beta};
2158 Dspin = conv_uks.Iterate(Dspin, Hspin, MOs_alpha, MOs_beta, totenergy);
2159
2161 << TimeStamp() << " Iter " << this_iter << " of " << maxiter << " Etot "
2162 << totenergy << " diise " << conv_uks.getDIIsError() << "\n\t\t a_gap "
2163 << MOs_alpha.eigenvalues()(alpha_e) -
2164 MOs_alpha.eigenvalues()(alpha_e - 1)
2165 << " b_gap "
2166 << MOs_beta.eigenvalues()(beta_e) - MOs_beta.eigenvalues()(beta_e - 1)
2167 << " Nalpha=" << dftAOoverlap.Matrix().cwiseProduct(Dspin.alpha).sum()
2168 << " Nbeta=" << dftAOoverlap.Matrix().cwiseProduct(Dspin.beta).sum()
2169 << std::flush;
2170
2171 bool converged = conv_uks.isConverged();
2172 if (converged || this_iter == maxiter - 1) {
2173 if (converged) {
2175 << TimeStamp() << " Converged after " << this_iter + 1
2176 << " iterations" << std::flush;
2177 } else {
2179 << TimeStamp() << " Not converged after " << this_iter + 1
2180 << " iterations. Unconverged density.\n\t\t\t"
2181 << " DIIsError=" << conv_uks.getDIIsError() << std::flush;
2182 }
2183 break;
2184 }
2185 }
2186
2187 Eigen::MatrixXd avgmatrix = SphericalAverageShells(Dspin.total(), dftbasis);
2189 << TimeStamp() << " Atomic density Matrix for " << uniqueAtom.getElement()
2190 << " gives N=" << std::setprecision(9)
2191 << avgmatrix.cwiseProduct(dftAOoverlap.Matrix()).sum() << " electrons."
2192 << std::flush;
2193 return avgmatrix;
2194}
2195
2196Eigen::MatrixXd DFTEngine::AtomicGuess(const QMMolecule& mol) const {
2197
2198 std::vector<std::string> elements = mol.FindUniqueElements();
2200 << TimeStamp() << " Scanning molecule of size " << mol.size()
2201 << " for unique elements" << std::flush;
2202 QMMolecule uniqueelements = QMMolecule("uniqueelements", 0);
2203 for (auto element : elements) {
2204 uniqueelements.push_back(QMAtom(0, element, Eigen::Vector3d::Zero()));
2205 }
2206
2207 XTP_LOG(Log::info, *pLog_) << TimeStamp() << " " << uniqueelements.size()
2208 << " unique elements found" << std::flush;
2209 std::vector<Eigen::MatrixXd> uniqueatom_guesses;
2210 for (QMAtom& unique_atom : uniqueelements) {
2212 << TimeStamp() << " Calculating atom density for "
2213 << unique_atom.getElement() << std::flush;
2214 Eigen::MatrixXd dmat_unrestricted = RunAtomicDFT_unrestricted(unique_atom);
2215 uniqueatom_guesses.push_back(dmat_unrestricted);
2216 }
2217
2218 Eigen::MatrixXd guess =
2219 Eigen::MatrixXd::Zero(dftbasis_.AOBasisSize(), dftbasis_.AOBasisSize());
2220 Index start = 0;
2221 for (const QMAtom& atom : mol) {
2222 Index index = 0;
2223 for (; index < uniqueelements.size(); index++) {
2224 if (atom.getElement() == uniqueelements[index].getElement()) {
2225 break;
2226 }
2227 }
2228 Eigen::MatrixXd& dmat_unrestricted = uniqueatom_guesses[index];
2229 guess.block(start, start, dmat_unrestricted.rows(),
2230 dmat_unrestricted.cols()) = dmat_unrestricted;
2231 start += dmat_unrestricted.rows();
2232 }
2233
2234 return guess;
2235}
2236
2237std::map<std::string, Eigen::MatrixXd>
2239 std::vector<std::string> elements = mol.FindUniqueElements();
2241 << TimeStamp() << " Scanning molecule of size " << mol.size()
2242 << " for unique elements (Hirshfeld reference densities)" << std::flush;
2243
2244 std::map<std::string, Eigen::MatrixXd> reference_densities;
2245 for (const std::string& element : elements) {
2246 QMAtom unique_atom(0, element, Eigen::Vector3d::Zero());
2248 << TimeStamp() << " Calculating Hirshfeld reference density for "
2249 << element << std::flush;
2250 // use_hunds_rule_occupation=true unconditionally here -- this is
2251 // the one and only caller that should ever request it; AtomicGuess
2252 // just above, the pre-existing SAD-guess caller, never does.
2253 reference_densities[element] = RunAtomicDFT_unrestricted(
2254 unique_atom, /*use_hunds_rule_occupation=*/true);
2255 }
2256 return reference_densities;
2257}
2258
2260 const QMMolecule& mol, const CDFTConstraintSpec& spec) const {
2261 std::map<std::string, Eigen::MatrixXd> reference_densities =
2263
2264 AOBasis full_dftbasis;
2265 {
2266 BasisSet basisset;
2267 basisset.Load(dftbasis_name_);
2268 full_dftbasis.Fill(basisset, mol);
2269 }
2270
2271 Vxc_Grid grid;
2272 grid.GridSetup(grid_name_, mol, full_dftbasis);
2273
2274 std::vector<HirshfeldPartition::AtomicReference> atoms =
2276 reference_densities);
2277
2279 constraint.weight_matrix = Eigen::MatrixXd::Zero(full_dftbasis.AOBasisSize(),
2280 full_dftbasis.AOBasisSize());
2281 double neutral_reference_population = 0.0;
2282 for (Index atom_index : spec.atom_indices) {
2283 if (atom_index < 0 || atom_index >= static_cast<Index>(mol.size())) {
2284 throw std::runtime_error(
2285 "BuildCDFTConstraint: cdft.indices contains atom index " +
2286 std::to_string(atom_index) + ", but this molecule only has " +
2287 std::to_string(mol.size()) +
2288 " atoms (0-based indexing -- valid range is 0.." +
2289 std::to_string(mol.size() - 1) + ").");
2290 }
2291 // Hirshfeld weights are additive across atoms in a fragment --
2292 // w_fragment(r) = sum_{i in fragment} w_i(r) -- so the fragment's
2293 // own weight matrix is just the sum of each atom's own
2294 // BuildWeightMatrix result, and the neutral reference population
2295 // (needed to convert the options file's charge-relative target
2296 // into RunCDFT's own absolute-population convention) is just the
2297 // sum of the fragment atoms' own nuclear charges.
2299 atoms, atom_index, full_dftbasis, grid);
2300 neutral_reference_population +=
2301 static_cast<double>(mol[atom_index].getNuccharge());
2302 }
2303
2304 constraint.target_population =
2305 neutral_reference_population - spec.target_charge;
2306 constraint.lambda = spec.initial_lambda;
2307 constraint.spin_alpha_coefficient = 1.0;
2308 constraint.spin_beta_coefficient = 1.0;
2309
2311 << TimeStamp() << " CDFT constraint: " << spec.atom_indices.size()
2312 << " atom(s), neutral reference population="
2313 << neutral_reference_population
2314 << ", requested relative charge=" << spec.target_charge
2315 << ", absolute target population=" << constraint.target_population
2316 << std::flush;
2317
2318 return constraint;
2319}
2320
2322 if (initial_guess_ == "orbfile") {
2323
2324 if (orb.hasDFTbasisName()) {
2325 if (orb.getDFTbasisName() != dftbasis_name_) {
2326 throw std::runtime_error(
2327 (boost::format("Basisset Name in guess orb file "
2328 "and in dftengine option file differ %1% vs %2%") %
2330 .str());
2331 }
2332 } else {
2334 << TimeStamp()
2335 << " WARNING: "
2336 "Orbital file has no basisset information,"
2337 "using it as a guess might work or not for calculation with "
2338 << dftbasis_name_ << std::flush;
2339 }
2340 }
2341
2342 const Index target_charge = orb.getCharge();
2343 const Index multiplicity = orb.getSpin();
2344
2345 orb.setChargeAndSpin(target_charge, multiplicity);
2348
2350 orb.setXCGrid(grid_name_);
2351 orb.setScaHFX(ScaHFX_);
2352 if (!ecp_name_.empty()) {
2353 orb.setECPName(ecp_name_);
2354 }
2355 if (!auxbasis_name_.empty()) {
2357 }
2358
2359 if (initial_guess_ == "orbfile") {
2360 if (orb.hasECPName() || !ecp_name_.empty()) {
2361 if (orb.getECPName() != ecp_name_) {
2362 throw std::runtime_error(
2363 (boost::format("ECPs in orb file: %1% and options %2% differ") %
2364 orb.getECPName() % ecp_name_)
2365 .str());
2366 }
2367 }
2370 throw std::runtime_error(
2371 (boost::format("Number of electrons in guess orb file "
2372 "and in dftengine differ: "
2373 "alpha %1% vs %2%, beta %3% vs %4%.") %
2376 .str());
2377 }
2378 if (orb.getBasisSetSize() != dftbasis_.AOBasisSize()) {
2379 throw std::runtime_error(
2380 (boost::format("Number of levels in guess orb file: "
2381 "%1% and in dftengine: %2% differ.") %
2382 orb.getBasisSetSize() % dftbasis_.AOBasisSize())
2383 .str());
2384 }
2385 } else {
2388 }
2389 return;
2390}
2391
2392void DFTEngine::Prepare(Orbitals& orb, Index numofelectrons) {
2393 QMMolecule& mol = orb.QMAtoms();
2394
2396 << TimeStamp() << " Using " << OPENMP::getMaxThreads() << " threads"
2397 << std::flush;
2398
2399 if (XTP_HAS_MKL_OVERLOAD()) {
2401 << TimeStamp() << " Using MKL overload for Eigen " << std::flush;
2402 } else {
2404 << TimeStamp()
2405 << " Using native Eigen implementation, no BLAS overload "
2406 << std::flush;
2407 }
2408
2409 XTP_LOG(Log::error, *pLog_) << " Molecule Coordinates [A] " << std::flush;
2410 for (const QMAtom& atom : mol) {
2411 const Eigen::Vector3d pos = atom.getPos() * tools::conv::bohr2ang;
2412 std::string output = (boost::format(" %1$s"
2413 " %2$+1.4f %3$+1.4f %4$+1.4f") %
2414 atom.getElement() % pos[0] % pos[1] % pos[2])
2415 .str();
2416
2417 XTP_LOG(Log::error, *pLog_) << output << std::flush;
2418 }
2419
2421 dftbasis_ = orb.getDftBasis();
2422
2424 << TimeStamp() << " Loaded DFT Basis Set " << dftbasis_name_ << " with "
2425 << dftbasis_.AOBasisSize() << " functions" << std::flush;
2426
2427 if (!auxbasis_name_.empty()) {
2428 BasisSet auxbasisset;
2429 auxbasisset.Load(auxbasis_name_);
2430 auxbasis_.Fill(auxbasisset, mol);
2432 << TimeStamp() << " Loaded AUX Basis Set " << auxbasis_name_ << " with "
2433 << auxbasis_.AOBasisSize() << " functions" << std::flush;
2434 }
2435 if (!ecp_name_.empty()) {
2436 ECPBasisSet ecpbasisset;
2437 ecpbasisset.Load(ecp_name_);
2439 << TimeStamp() << " Loaded ECP library " << ecp_name_ << std::flush;
2440
2441 std::vector<std::string> results = ecp_.Fill(ecpbasisset, mol);
2443 << TimeStamp() << " Filled ECP Basis" << std::flush;
2444 if (results.size() > 0) {
2445 std::string message = "";
2446 for (const std::string& element : results) {
2447 message += " " + element;
2448 }
2450 << TimeStamp() << " Found no ECPs for elements" << message
2451 << std::flush;
2452 }
2453 }
2454
2455 numofelectrons_ = 0;
2458 num_docc_ = 0;
2459 num_socc_alpha_ = 0;
2460
2461 Index nuclear_charge = 0;
2462 for (const QMAtom& atom : mol) {
2463 nuclear_charge += atom.getNuccharge();
2464 }
2465
2466 Index target_charge = orb.getCharge();
2467 Index multiplicity = orb.getSpin();
2468
2469 if (multiplicity < 1) {
2470 throw std::runtime_error("Spin multiplicity must be >= 1.");
2471 }
2472
2473 if (numofelectrons >= 0) {
2474 numofelectrons_ = numofelectrons;
2475 } else {
2476 numofelectrons_ = nuclear_charge - target_charge;
2477 }
2478
2479 Index spin_excess = multiplicity - 1;
2480
2481 if (numofelectrons_ < 0) {
2482 throw std::runtime_error("Computed a negative number of electrons.");
2483 }
2484
2485 if (spin_excess > numofelectrons_) {
2486 throw std::runtime_error(
2487 "Spin multiplicity incompatible with total number of electrons.");
2488 }
2489
2490 if (((numofelectrons_ + spin_excess) % 2) != 0) {
2491 throw std::runtime_error(
2492 "Charge and spin multiplicity imply non-integer alpha/beta "
2493 "occupations.");
2494 }
2495
2496 num_alpha_electrons_ = (numofelectrons_ + spin_excess) / 2;
2497 num_beta_electrons_ = (numofelectrons_ - spin_excess) / 2;
2498
2501
2503 << TimeStamp() << " Total number of electrons: " << numofelectrons_
2504 << " (charge=" << target_charge << ", multiplicity=" << multiplicity
2505 << ", alpha=" << num_alpha_electrons_ << ", beta=" << num_beta_electrons_
2506 << ", docc=" << num_docc_ << ", socc=" << num_socc_alpha_ << ")"
2507 << std::flush;
2508
2510 return;
2511}
2512
2515 if (ScaHFX_ > 0) {
2517 << TimeStamp() << " Using hybrid functional with alpha=" << ScaHFX_
2518 << std::flush;
2519 }
2520 Vxc_Grid grid;
2521 grid.GridSetup(grid_name_, mol, dftbasis_);
2522 Vxc_Potential<Vxc_Grid> vxc(grid);
2525 << TimeStamp() << " Setup numerical integration grid " << grid_name_
2526 << " for vxc functional " << xc_functional_name_ << std::flush;
2528 << "\t\t "
2529 << " with " << grid.getGridSize() << " points"
2530 << " divided into " << grid.getBoxesSize() << " boxes" << std::flush;
2531 return vxc;
2532}
2533
2534double DFTEngine::NuclearRepulsion(const QMMolecule& mol) const {
2535 double E_nucnuc = 0.0;
2536
2537 for (Index i = 0; i < mol.size(); i++) {
2538 const Eigen::Vector3d& r1 = mol[i].getPos();
2539 double charge1 = double(mol[i].getNuccharge());
2540 for (Index j = 0; j < i; j++) {
2541 const Eigen::Vector3d& r2 = mol[j].getPos();
2542 double charge2 = double(mol[j].getNuccharge());
2543 E_nucnuc += charge1 * charge2 / (r1 - r2).norm();
2544 }
2545 }
2546 return E_nucnuc;
2547}
2548
2549// spherically average the density matrix belonging to two shells
2551 const Eigen::MatrixXd& dmat, const AOBasis& dftbasis) const {
2552 Eigen::MatrixXd avdmat = Eigen::MatrixXd::Zero(dmat.rows(), dmat.cols());
2553 for (const AOShell& shellrow : dftbasis) {
2554 Index size_row = shellrow.getNumFunc();
2555 Index start_row = shellrow.getStartIndex();
2556 for (const AOShell& shellcol : dftbasis) {
2557 Index size_col = shellcol.getNumFunc();
2558 Index start_col = shellcol.getStartIndex();
2559 Eigen::MatrixXd shelldmat =
2560 dmat.block(start_row, start_col, size_row, size_col);
2561 if (shellrow.getL() == shellcol.getL()) {
2562 double diagavg = shelldmat.diagonal().sum() / double(shelldmat.rows());
2563 Index offdiagelements =
2564 shelldmat.rows() * shelldmat.cols() - shelldmat.cols();
2565 double offdiagavg = (shelldmat.sum() - shelldmat.diagonal().sum()) /
2566 double(offdiagelements);
2567 avdmat.block(start_row, start_col, size_row, size_col).array() =
2568 offdiagavg;
2569 avdmat.block(start_row, start_col, size_row, size_col)
2570 .diagonal()
2571 .array() = diagavg;
2572 } else {
2573 double avg = shelldmat.sum() / double(shelldmat.size());
2574 avdmat.block(start_row, start_col, size_row, size_col).array() = avg;
2575 }
2576 }
2577 }
2578 return avdmat;
2579}
2580
2582 const QMMolecule& mol,
2583 const std::vector<std::unique_ptr<StaticSite>>& multipoles) const {
2584
2585 if (multipoles.size() == 0) {
2586 return 0;
2587 }
2588
2589 double E_ext = 0;
2590 eeInteractor interactor;
2591 for (const QMAtom& atom : mol) {
2592 StaticSite nucleus = StaticSite(atom, double(atom.getNuccharge()));
2593 for (const std::unique_ptr<StaticSite>& site : *externalsites_) {
2594 if ((site->getPos() - nucleus.getPos()).norm() < 1e-7) {
2596 << " External site sits on nucleus, "
2597 "interaction between them is ignored."
2598 << std::flush;
2599 continue;
2600 }
2601 E_ext += interactor.CalcStaticEnergy_site(*site, nucleus);
2602 }
2603 }
2604 return E_ext;
2605}
2606
2607Eigen::MatrixXd DFTEngine::IntegrateExternalField(const QMMolecule& mol) const {
2608
2609 AODipole dipole;
2610 dipole.setCenter(mol.getPos());
2611 dipole.Fill(dftbasis_);
2612 Eigen::MatrixXd result =
2613 Eigen::MatrixXd::Zero(dipole.Dimension(), dipole.Dimension());
2614 for (Index i = 0; i < 3; i++) {
2615 result -= dipole.Matrix()[i] * extfield_[i];
2616 }
2617 return result;
2618}
2619
2621 const QMMolecule& mol,
2622 const std::vector<std::unique_ptr<StaticSite>>& multipoles) const {
2623
2624 Mat_p_Energy result(dftbasis_.AOBasisSize(), dftbasis_.AOBasisSize());
2625 AOMultipole dftAOESP;
2626
2627 dftAOESP.FillPotential(dftbasis_, multipoles);
2629 << TimeStamp() << " Filled DFT external multipole potential matrix"
2630 << std::flush;
2631 result.matrix() = dftAOESP.Matrix();
2632 result.energy() = ExternalRepulsion(mol, multipoles);
2633
2634 return result;
2635}
2636
2638 const QMMolecule& mol, const Orbitals& extdensity) const {
2639 BasisSet basis;
2640 basis.Load(extdensity.getDFTbasisName());
2641 AOBasis aobasis;
2642 aobasis.Fill(basis, extdensity.QMAtoms());
2643 Vxc_Grid grid;
2644 grid.GridSetup(gridquality_, extdensity.QMAtoms(), aobasis);
2645 DensityIntegration<Vxc_Grid> numint(grid);
2646 Eigen::MatrixXd dmat = extdensity.DensityMatrixFull(state_);
2647
2648 numint.IntegrateDensity(dmat);
2650 << TimeStamp() << " Calculated external density" << std::flush;
2651 Eigen::MatrixXd e_contrib = numint.IntegratePotential(dftbasis_);
2653 << TimeStamp() << " Calculated potential from electron density"
2654 << std::flush;
2655 AOMultipole esp;
2656 esp.FillPotential(dftbasis_, extdensity.QMAtoms());
2657
2658 double nuc_energy = 0.0;
2659 for (const QMAtom& atom : mol) {
2660 nuc_energy +=
2661 numint.IntegratePotential(atom.getPos()) * double(atom.getNuccharge());
2662 for (const QMAtom& extatom : extdensity.QMAtoms()) {
2663 const double dist = (atom.getPos() - extatom.getPos()).norm();
2664 nuc_energy +=
2665 double(atom.getNuccharge()) * double(extatom.getNuccharge()) / dist;
2666 }
2667 }
2669 << TimeStamp() << " Calculated potential from nuclei" << std::flush;
2671 << TimeStamp() << " Electrostatic: " << nuc_energy << std::flush;
2672 return Mat_p_Energy(nuc_energy, e_contrib + esp.Matrix());
2673}
2674
2676 const Eigen::MatrixXd& GuessMOs) const {
2677 Eigen::MatrixXd nonortho =
2678 GuessMOs.transpose() * dftAOoverlap_.Matrix() * GuessMOs;
2679 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(nonortho);
2680 Eigen::MatrixXd result = GuessMOs * es.operatorInverseSqrt();
2681 return result;
2682}
2683
2684/*************************************************************
2685 * Extended Hueckel Theory
2686 ************************************************************/
2688 const QMMolecule& mol) const {
2689
2691
2692 const Index nao = dftbasis_.AOBasisSize();
2693 Eigen::VectorXd eps = Eigen::VectorXd::Zero(nao);
2694
2695 for (const AOShell& shell : dftbasis_) {
2696
2697 int l = static_cast<int>(shell.getL());
2698 Index start = shell.getStartIndex();
2699 Index nfunc = shell.getNumFunc();
2700
2701 const QMAtom& atom = mol[shell.getAtomIndex()];
2702 const std::string& element = atom.getElement();
2703
2704 int used_l = l;
2705 double e = params.GetWithFallback(element, l, &used_l);
2706
2707 for (Index i = 0; i < nfunc; ++i) {
2708 eps(start + i) = e;
2709 }
2710 }
2711
2712 return eps;
2713}
2714
2715Eigen::MatrixXd DFTEngine::BuildEHTHamiltonian(const QMMolecule& mol) const {
2716
2717 const Eigen::MatrixXd& S = dftAOoverlap_.Matrix();
2718 const Index nao = S.rows();
2719 Eigen::VectorXd eps = BuildEHTOrbitalEnergies(mol);
2720 Eigen::MatrixXd H = Eigen::MatrixXd::Zero(nao, nao);
2721 constexpr double K = 1.75;
2722
2723 for (Index mu = 0; mu < nao; ++mu) {
2724 H(mu, mu) = eps(mu);
2725 for (Index nu = 0; nu < mu; ++nu) {
2726 double hij = K * S(mu, nu) * 0.5 * (eps(mu) + eps(nu));
2727 H(mu, nu) = hij;
2728 H(nu, mu) = hij;
2729 }
2730 }
2731
2732 return H;
2733}
2734
2736
2738 << TimeStamp() << " Building Extended Huckel guess" << std::flush;
2739
2740 Eigen::MatrixXd H = BuildEHTHamiltonian(mol);
2741
2743 << TimeStamp() << " Solving EHT generalized eigenproblem" << std::flush;
2744
2745 return conv_accelerator_.SolveFockmatrix(H);
2746}
2747
2749 const Mat_p_Energy& H0, const QMMolecule& mol,
2750 const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
2751
2753
2754 Eigen::MatrixXd Dmat = conv_accelerator_.DensityMatrix(eht);
2755
2756 Mat_p_Energy e_vxc = vxcpotential.IntegrateVXC(Dmat);
2757
2758 Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
2759
2760 if (ScaHFX_ > 0) {
2761 std::array<Eigen::MatrixXd, 2> both =
2762 CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dmat, 1e-12);
2763 H += both[0];
2764 H += ScaHFX_ * both[1];
2765 } else {
2766 H += CalcERIs(Dmat, 1e-12);
2767 }
2768
2769 return conv_accelerator_.SolveFockmatrix(H);
2770}
2771
2773 const QMMolecule& dimer_mol) const {
2774 Orbitals monomerA;
2776 Orbitals monomerB;
2778
2779 const QMMolecule& atomsA = monomerA.QMAtoms();
2780 const QMMolecule& atomsB = monomerB.QMAtoms();
2781 Index nA = atomsA.size();
2782 Index nB = atomsB.size();
2783
2784 // --- Sanity check 1: element count and sequence ---
2785 // Deliberately checked BEFORE the geometry check below -- a clear
2786 // "wrong element at index N" error is far more actionable than the
2787 // generic "distance mismatch" the geometry check alone would give if
2788 // the atom ordering itself were wrong.
2789 if (nA + nB != dimer_mol.size()) {
2790 throw std::runtime_error(
2791 "BuildDimerGuessFromMonomerFiles: monomer A (" + std::to_string(nA) +
2792 " atoms) + monomer B (" + std::to_string(nB) +
2793 " atoms) does not equal this calculation's own molecule (" +
2794 std::to_string(dimer_mol.size()) +
2795 " atoms) -- wrong monomer file(s), or this calculation's molecule "
2796 "is not simply the concatenation of these two monomers.");
2797 }
2798 for (Index i = 0; i < nA; ++i) {
2799 if (atomsA[i].getElement() != dimer_mol[i].getElement()) {
2800 throw std::runtime_error(
2801 "BuildDimerGuessFromMonomerFiles: monomer A's own atom " +
2802 std::to_string(i) + " (" + atomsA[i].getElement() +
2803 ") does not match this calculation's own atom " + std::to_string(i) +
2804 " (" + dimer_mol[i].getElement() +
2805 ") -- dimer_guess assumes monomer A occupies exactly the first "
2806 "N_A atoms of this calculation's molecule, in the same order.");
2807 }
2808 }
2809 for (Index i = 0; i < nB; ++i) {
2810 if (atomsB[i].getElement() != dimer_mol[nA + i].getElement()) {
2811 throw std::runtime_error(
2812 "BuildDimerGuessFromMonomerFiles: monomer B's own atom " +
2813 std::to_string(i) + " (" + atomsB[i].getElement() +
2814 ") does not match this calculation's own atom " +
2815 std::to_string(nA + i) + " (" + dimer_mol[nA + i].getElement() +
2816 ") -- dimer_guess assumes monomer B occupies exactly the "
2817 "remaining atoms of this calculation's molecule (after monomer "
2818 "A's own N_A atoms), in the same order.");
2819 }
2820 }
2821
2822 // --- Sanity check 2: internal geometry (translation/rotation
2823 // invariant) ---
2824 // Every pairwise interatomic distance WITHIN a monomer is unchanged
2825 // by rigid translation or rotation of that monomer as a whole --
2826 // exactly the operation that happens between a monomer's own,
2827 // independent optimization and its placement into the dimer. So this
2828 // checks the one thing that SHOULD be identical (internal geometry)
2829 // rather than the one thing that is EXPECTED to differ (absolute
2830 // position/orientation).
2831 constexpr double kGeometryToleranceBohr = 1e-3;
2832 auto CheckInternalGeometry = [&](const QMMolecule& monomer_atoms,
2833 Index offset_in_dimer,
2834 const std::string& label) {
2835 Index n = monomer_atoms.size();
2836 for (Index i = 0; i < n; ++i) {
2837 for (Index j = i + 1; j < n; ++j) {
2838 double monomer_distance =
2839 (monomer_atoms[i].getPos() - monomer_atoms[j].getPos()).norm();
2840 double dimer_distance = (dimer_mol[offset_in_dimer + i].getPos() -
2841 dimer_mol[offset_in_dimer + j].getPos())
2842 .norm();
2843 double diff = std::abs(monomer_distance - dimer_distance);
2844 if (diff > kGeometryToleranceBohr) {
2845 throw std::runtime_error(
2846 "BuildDimerGuessFromMonomerFiles: " + label +
2847 "'s own internal geometry does not match this calculation's "
2848 "molecule -- distance between its own atoms " +
2849 std::to_string(i) + " and " + std::to_string(j) + " is " +
2850 std::to_string(monomer_distance) +
2851 " Bohr in the monomer file, but " +
2852 std::to_string(dimer_distance) +
2853 " Bohr in this calculation's own molecule (difference " +
2854 std::to_string(diff) + " Bohr, tolerance " +
2855 std::to_string(kGeometryToleranceBohr) +
2856 " Bohr). This is checked as an INTERNAL, translation/"
2857 "rotation-invariant distance specifically because the "
2858 "monomer's absolute position/orientation is expected to "
2859 "differ between its own standalone optimization and its "
2860 "placement in the dimer -- only its internal geometry "
2861 "should still match.");
2862 }
2863 }
2864 }
2865 };
2866 CheckInternalGeometry(atomsA, 0, "Monomer A");
2867 CheckInternalGeometry(atomsB, nA, "Monomer B");
2868
2869 Orbitals dimer_guess;
2870 // PrepareDimerGuess/PrepareDimerGuessMixedSpin both call SetupDftBasis
2871 // internally, which needs this->QMAtoms() already populated -- the
2872 // SAME requirement iqm.cc's own, existing caller of PrepareDimerGuess
2873 // already satisfies (orbitalsAB.QMAtoms() is set there well before its
2874 // own PrepareDimerGuess call), confirmed directly by reading that
2875 // code rather than assumed.
2876 dimer_guess.QMAtoms() = dimer_mol;
2877 dimer_guess.PrepareDimerGuessMixedSpin(monomerA, monomerB);
2878 return dimer_guess;
2879}
2880
2881} // namespace xtp
2882} // 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
Container to hold Basisfunctions for all atoms.
Definition aobasis.h:42
Index AOBasisSize() const
Definition aobasis.h:46
void Fill(const BasisSet &bs, const QMMolecule &atoms)
Definition aobasis.cc:85
Index Dimension() final
Definition aomatrix.h:91
void setCenter(const Eigen::Vector3d &r)
Definition aomatrix.h:94
const std::array< Eigen::MatrixXd, 3 > & Matrix() const
Definition aomatrix.h:92
void Fill(const AOBasis &aobasis) final
void FillPotential(const AOBasis &aobasis, const ECPAOBasis &ecp)
Definition aoecp.cc:59
void Fill(const AOBasis &aobasis) final
const Eigen::MatrixXd & Matrix() const
Definition aomatrix.h:41
void FillPotential(const AOBasis &aobasis, const QMMolecule &atoms)
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:869
Eigen::MatrixXd ComputeOverlapPulayGradientUKS(const QMMolecule &mol, const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
Definition dftengine.cc:646
std::string auxbasis_name_
Definition dftengine.h:526
std::string gridquality_
Definition dftengine.h:570
tools::EigenSystem ModelPotentialGuess(const Mat_p_Energy &H0, const QMMolecule &mol, const Vxc_Potential< Vxc_Grid > &vxcpotential) const
Definition dftengine.cc:889
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:878
Orbitals BuildDimerGuessFromMonomerFiles(const QMMolecule &dimer_mol) const
double cdft_population_tolerance_
Definition dftengine.h:643
void ComputeAndStoreForces(Orbitals &orb, const Eigen::MatrixXd &Dmat, const Vxc_Potential< Vxc_Grid > &vxcpotential) const
Definition dftengine.cc:425
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:327
std::string dftbasis_name_
Definition dftengine.h:527
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:528
std::string grid_name_
Definition dftengine.h:538
void Prepare(Orbitals &orb, Index numofelectrons=-1)
std::string initial_guess_
Definition dftengine.h:543
std::string orbfilename_
Definition dftengine.h:569
Eigen::MatrixXd AtomicGuess(const QMMolecule &mol) const
Build an atomic-density based initial guess in the AO basis.
Eigen::MatrixXd BuildEHTHamiltonian(const QMMolecule &mol) const
Build the extended-Hückel Hamiltonian for the current molecule.
Eigen::MatrixXd IntegrateExternalField(const QMMolecule &mol) const
Integrate a homogeneous external electric field into the AO basis.
Eigen::Vector3d extfield_
Definition dftengine.h:576
std::array< Eigen::MatrixXd, 2 > CalcERIs_EXX(const Eigen::MatrixXd &MOCoeff, const Eigen::MatrixXd &Dmat, double error) const
Definition dftengine.cc:852
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:100
std::string xc_functional_name_
Definition dftengine.h:565
void CalcElDipole(const Orbitals &orb) const
Evaluate and print the electronic dipole moment from the final density.
Definition dftengine.cc:387
double ExternalRepulsion(const QMMolecule &mol, const std::vector< std::unique_ptr< StaticSite > > &multipoles) const
ConvergenceAcc::options conv_opt_
Definition dftengine.h:554
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:579
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:747
void ConfigOrbfile(Orbitals &orb)
Propagate basis-set, XC, and metadata settings into the orbital container.
Eigen::VectorXd BuildEHTOrbitalEnergies(const QMMolecule &mol) const
Build orbital energies used in the extended-Hückel starting guess.
std::vector< HirshfeldPartition::Constraint > constraints_
Definition dftengine.h:632
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:307
AOOverlap dftAOoverlap_
Definition dftengine.h:541
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:910
CDFTConstraintSpec cdft_constraint_spec_
Definition dftengine.h:646
Eigen::MatrixXd ComputeNonXCGradientUKS(const QMMolecule &mol, const UKSConvergenceAcc::SpinDensity &Dspin, const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
Definition dftengine.cc:694
bool RunCDFT(Orbitals &orb, HirshfeldPartition::Constraint &constraint)
std::string dimer_guess_orbB_name_
Definition dftengine.h:549
std::vector< std::unique_ptr< StaticSite > > * externalsites_
Definition dftengine.h:561
Vxc_Potential< Vxc_Grid > SetupVxc(const QMMolecule &mol)
double NuclearRepulsion(const QMMolecule &mol) const
Compute the classical nucleus-nucleus repulsion energy.
ConvergenceAcc conv_accelerator_
Definition dftengine.h:556
std::string dimer_guess_orbA_name_
Definition dftengine.h:548
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)
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:61
void Initialize_4c(const AOBasis &dftbasis)
Definition ERIs.cc:32
Eigen::MatrixXd CalculateERIs_4c(const Eigen::MatrixXd &DMAT, double error) const
Definition ERIs.h:56
double GetWithFallback(const std::string &element, int l, int *used_l=nullptr) const
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
Logger is used for thread-safe output of messages.
Definition logger.h:164
Eigen::MatrixXd & matrix()
Definition eigen.h:80
double & energy()
Definition eigen.h:81
Index cols() const
Definition eigen.h:79
Index rows() const
Definition eigen.h:78
Container for molecular orbitals and derived one-particle data.
Definition orbitals.h:47
void setScaHFX(double ScaHFX)
Store the fraction of exact exchange associated with the functional.
Definition orbitals.h:436
Index getCharge() const
Return the stored total charge.
Definition orbitals.h:254
Index getSpin() const
Return the stored spin multiplicity.
Definition orbitals.h:252
bool hasForces() const
Report whether nuclear forces have been stored.
Definition orbitals.h:301
const tools::EigenSystem & MOs_beta() const
Return read-only access to beta-spin molecular orbitals.
Definition orbitals.h:202
std::array< Eigen::MatrixXd, 2 > DensityMatrixGroundStateSpinResolved() const
Definition orbitals.cc:1380
Index getNumberOfAlphaElectrons() const
Return the stored number of alpha electrons.
Definition orbitals.h:159
void setNumberOfAlphaElectrons(Index electrons)
Store the total number of alpha electrons.
Definition orbitals.h:139
Eigen::MatrixXd DensityMatrixFull(const QMState &state) const
Definition orbitals.cc:150
void PrepareDimerGuessMixedSpin(const Orbitals &orbitalsA, const Orbitals &orbitalsB)
Guess for a dimer of two monomers with independently ARBITRARY charge and spin, built by combining ea...
Definition orbitals.cc:1032
void SetupAuxBasis(std::string aux_basis_name)
Definition orbitals.cc:108
void setNumberOfBetaElectrons(Index electrons)
Store the total number of beta electrons.
Definition orbitals.h:144
void setForces(const Eigen::MatrixXd &forces)
Definition orbitals.h:309
void setECPName(const std::string &ECP)
Store the effective core potential label.
Definition orbitals.h:170
void setXCGrid(std::string grid)
Store the numerical XC grid quality label.
Definition orbitals.h:283
void setNumberOfOccupiedLevels(Index occupied_levels)
Definition orbitals.h:122
Index getBasisSetSize() const
Return the number of AO basis functions in the DFT basis.
Definition orbitals.h:72
void setQMEnergy(double qmenergy)
Store the total DFT energy.
Definition orbitals.h:295
bool hasECPName() const
Report whether an effective core potential label has been stored.
Definition orbitals.h:164
const tools::EigenSystem & MOs() const
Return read-only access to alpha/restricted molecular orbitals.
Definition orbitals.h:192
const QMMolecule & QMAtoms() const
Return read-only access to the molecular geometry.
Definition orbitals.h:262
void ReadFromCpt(const std::string &filename)
Read the orbital container from a checkpoint file on disk.
Definition orbitals.cc:1201
void setNumberOfOccupiedLevelsBeta(Index occupied_levels_beta)
Store the number of occupied beta-spin orbitals.
Definition orbitals.h:134
void setChargeAndSpin(Index charge, Index spin)
Definition orbitals.h:246
bool hasDFTbasisName() const
Report whether a DFT basis-set name has been stored.
Definition orbitals.h:313
const std::string & getECPName() const
Return the effective core potential label.
Definition orbitals.h:167
const std::string & getDFTbasisName() const
Return the DFT basis-set name.
Definition orbitals.h:318
bool hasBetaMOs() const
Report whether beta-spin molecular orbitals are available.
Definition orbitals.h:187
void SetupDftBasis(std::string basis_name)
Build and attach the DFT AO basis from the stored molecular geometry.
Definition orbitals.cc:99
const Eigen::MatrixXd & getForces() const
Return the stored nuclear forces (Natoms x 3, Hartree/Bohr).
Definition orbitals.h:304
Eigen::Vector3d CalcElDipole(const QMState &state) const
Compute the electronic dipole moment associated with a state density.
Definition orbitals.cc:288
Index getNumberOfBetaElectrons() const
Return the stored number of beta electrons.
Definition orbitals.h:161
const AOBasis & getDftBasis() const
Return the DFT AO basis, throwing if it has not been initialized.
Definition orbitals.h:328
void setXCFunctionalName(std::string functionalname)
Definition orbitals.h:276
container for QM atoms
Definition qmatom.h:37
const std::string & getElement() const
Definition qmatom.h:63
Index getNuccharge() const
Definition qmatom.h:69
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
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
tools::EigenSystem SolveFockmatrix(const Eigen::MatrixXd &H) const
SpinDensity DensityMatrix(const tools::EigenSystem &MOs_alpha, const tools::EigenSystem &MOs_beta) const
void setOverlap(AOOverlap &S, double etol)
void Configure(const options &opt_alpha, const options &opt_beta)
SpinDensity Iterate(const SpinDensity &dmat, SpinFock &H, tools::EigenSystem &MOs_alpha, tools::EigenSystem &MOs_beta, double totE)
void setCoupledFockBuilder(const CoupledFockBuilder &builder)
Index getGridSize() const
Definition vxc_grid.h:41
void GridSetup(const std::string &type, const QMMolecule &atoms, const AOBasis &basis)
Definition vxc_grid.cc:229
Index getBoxesSize() const
Definition vxc_grid.h:42
Eigen::MatrixXd GridWeightGradient(const Eigen::MatrixXd &density_matrix, const QMMolecule &atoms) const
Eigen::MatrixXd PulayGradient(const Eigen::MatrixXd &density_matrix, const AOBasis &dftbasis) const
Eigen::MatrixXd PulayGradientUKS(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta, const AOBasis &dftbasis) const
Mat_p_Energy IntegrateVXC(const Eigen::MatrixXd &density_matrix) const
void setXCfunctional(const std::string &functional)
static double getExactExchange(const std::string &functional)
Eigen::MatrixXd GridWeightGradientUKS(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta, const QMMolecule &atoms) const
SpinResult IntegrateVXCSpin(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta) const
Mediates interaction between polar and static sites.
double CalcStaticEnergy_site(const StaticSite &site1, const StaticSite &site2) const
#define XTP_LOG(level, log)
Definition logger.h:40
const double bohr2ang
Definition constants.h:49
Index getMaxThreads()
Definition eigen.h:128
Charge transport classes.
Definition ERIs.h:28
bool XTP_HAS_MKL_OVERLOAD()
Definition eigen.h:41
std::vector< AOMatrixDerivative > ComputeNuclearAttractionDerivatives(const AOBasis &aobasis, const QMMolecule &mol)
std::vector< AOMatrixDerivative > ComputeKineticDerivatives(const AOBasis &aobasis)
std::vector< AOMatrixDerivative > ComputeOverlapDerivatives(const AOBasis &aobasis)
std::array< Eigen::MatrixXd, 3 > AOMatrixDerivative
Definition dftengine.cc:413
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.