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