votca 2026-dev
Loading...
Searching...
No Matches
dftgradient.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2024 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// Local VOTCA includes
22#include "votca/xtp/aomatrix.h"
24
25namespace votca {
26namespace xtp {
27
28// Defined in libint2_derivative_calls.cc, not yet in any header (same
29// STATUS noted throughout that file and libint2_derivative_calls.cc
30// itself) -- forward declared here rather than adding a new header at
31// this stage, consistent with how these functions have been consumed
32// from test files so far.
33using AOMatrixDerivative = std::array<Eigen::MatrixXd, 3>;
34using ThreeCenterDerivative = std::array<std::vector<Eigen::MatrixXd>, 3>;
35std::vector<AOMatrixDerivative> ComputeCoulombMetricDerivatives(
36 const AOBasis& aobasis);
37std::vector<ThreeCenterDerivative> ComputeThreeCenterDerivatives(
38 const AOBasis& auxbasis, const AOBasis& dftbasis);
39// Memory-efficient alternative to ComputeThreeCenterDerivatives above --
40// see that function's own header comment in libint2_derivative_calls.cc
41// for why this exists (the full-tensor version is catastrophically
42// memory-unscalable for anything beyond a small, toy-sized system).
43std::vector<Eigen::MatrixXd> ComputeThreeCenterDerivativeContraction(
44 const AOBasis& auxbasis, const AOBasis& dftbasis,
45 const Eigen::MatrixXd& density);
46// CORRECTED, single-pass alternative for RIKGradient's own, different
47// contraction needs -- see that function's own header comment in
48// libint2_derivative_calls.cc for the full reasoning, including the
49// arithmetic error that led to an earlier, slower, per-atom approach
50// (ComputeThreeCenterDerivativesForAtom, kept below but no longer
51// used by RIKGradient) being chosen instead.
52std::vector<ThreeCenterDerivative> ComputeThreeCenterDerivativesMOTransformed(
53 const AOBasis& auxbasis, const AOBasis& dftbasis,
54 const Eigen::MatrixXd& occ_mo_coeffs);
55// Per-atom alternative -- superseded by
56// ComputeThreeCenterDerivativesMOTransformed above for RIKGradient's
57// own use (kept, unused by RIKGradient now, since deleting it would
58// remove a real, working, if slower, fallback with no compensating
59// benefit).
61 const AOBasis& auxbasis, const AOBasis& dftbasis, Index target_atom);
62std::vector<Eigen::MatrixXd> ComputeThreeCenterIntegrals(
63 const AOBasis& auxbasis, const AOBasis& dftbasis);
64
66 Index natoms = mol.size();
67 Eigen::MatrixXd deriv = Eigen::MatrixXd::Zero(natoms, 3);
68
69 // dE_nn/dR_A = sum_{B != A} Z_A Z_B * d(1/R_AB)/dR_A
70 // = -sum_{B != A} Z_A Z_B (R_A - R_B) / |R_A - R_B|^3
71 //
72 // NOTE: this returns the GRADIENT dE/dR, not the force -dE/dR -- the
73 // sign convention for the final assembled total gradient (which this
74 // feeds into) is a decision for whatever code combines this with the
75 // RI-J and XC terms, not fixed here. Keep this consistent when
76 // validating against finite differences: a finite difference of the
77 // energy directly gives dE/dR, matching this function's output as-is,
78 // with no extra sign flip needed.
79 for (Index a = 0; a < natoms; ++a) {
80 double Za = static_cast<double>(mol[a].getNuccharge());
81 const Eigen::Vector3d& Ra = mol[a].getPos();
82 Eigen::Vector3d sum = Eigen::Vector3d::Zero();
83 for (Index b = 0; b < natoms; ++b) {
84 if (b == a) {
85 continue;
86 }
87 double Zb = static_cast<double>(mol[b].getNuccharge());
88 const Eigen::Vector3d& Rb = mol[b].getPos();
89 Eigen::Vector3d Rab_vec = Ra - Rb;
90 double Rab = Rab_vec.norm();
91 sum += Za * Zb * Rab_vec / (Rab * Rab * Rab);
92 }
93 deriv.row(a) = -sum.transpose();
94 }
95 return deriv;
96}
97
98Eigen::MatrixXd DFTGradient::RIJGradient(const Eigen::MatrixXd& density,
99 const AOBasis& auxbasis,
100 const AOBasis& dftbasis) {
101 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
102 Index n_aux_bf = auxbasis.AOBasisSize();
103
104 // d_P = sum_{mu,nu} P_{mu,nu} (mu,nu|P)
105 std::vector<Eigen::MatrixXd> tensor =
106 ComputeThreeCenterIntegrals(auxbasis, dftbasis);
107 Eigen::VectorXd d(n_aux_bf);
108 for (Index p = 0; p < n_aux_bf; ++p) {
109 d(p) = (density.array() * tensor[p].array()).sum();
110 }
111
112 // V_PQ = (P|Q), c = V^-1 d. Using a plain SPD solve here rather than
113 // the eigenvalue-truncated Pseudo_InvSqrt approach AOCoulomb also
114 // offers (used in production for numerical stability against
115 // near-linear-dependence in the aux basis) -- adequate for this
116 // validation-scale use, but worth revisiting if this is ever used on
117 // a genuinely large or near-linearly-dependent aux basis.
118 AOCoulomb aocoulomb;
119 aocoulomb.Fill(auxbasis);
120 const Eigen::MatrixXd& V = aocoulomb.Matrix();
121 // V is positive definite; Cholesky, LDLT as before if it is not
122 const CholeskyFactor V_chol(V);
123 Eigen::VectorXd c = V_chol.ok() ? Eigen::VectorXd(V_chol.Solve(d))
124 : Eigen::VectorXd(V.ldlt().solve(d));
125
126 // Already-contracted against density (sum_{mu,nu} density(mu,nu) *
127 // d(mu,nu|p)/dR_a[xyz], not a per-(mu,nu) tensor) -- see this
128 // function's own header comment for why: the full-tensor
129 // ComputeThreeCenterDerivatives, previously used here, was confirmed
130 // to exhaust hundreds of GB of memory on a real, moderately-sized
131 // molecule (natoms*3*n_aux_bf separate, complete nao x nao matrices
132 // held simultaneously -- roughly 2.6 PETABYTES for a 53-atom, 943
133 // AO / 2320 auxiliary function system). ComputeThreeCenterDerivatives
134 // itself is left unchanged (still used, and still validated, by
135 // test_aoderivatives.cc and by RIKGradient below, which needs a
136 // genuinely different contraction of its own -- see that function's
137 // own comments).
138 std::vector<Eigen::MatrixXd> ddP_dR =
139 ComputeThreeCenterDerivativeContraction(auxbasis, dftbasis, density);
140 std::vector<AOMatrixDerivative> dV =
142
143 // dE_J/dR = sum_P c_P d(d_P)/dR - 1/2 sum_PQ c_P c_Q d(V_PQ)/dR
144 // d(d_P)/dR = sum_{mu,nu} P_{mu,nu} d(mu,nu|P)/dR (density held fixed --
145 // see the "IMPORTANT" note on this function in dftgradient.h for why
146 // that's valid regardless of whether density is a converged SCF
147 // density or an arbitrary fixed matrix). ddP_dR[a](xyz, p) is exactly
148 // this quantity, already summed over mu,nu -- no further contraction
149 // against density needed here at all.
150 Eigen::MatrixXd grad = Eigen::MatrixXd::Zero(natoms, 3);
151 for (Index a = 0; a < natoms; ++a) {
152 for (Index xyz = 0; xyz < 3; ++xyz) {
153 double term1 = ddP_dR[a].row(xyz).dot(c);
154 double term2 = 0.5 * c.dot(dV[a][xyz] * c);
155 grad(a, xyz) = term1 - term2;
156 }
157 }
158 return grad;
159}
160
161Eigen::MatrixXd DFTGradient::RIKGradient(const Eigen::MatrixXd& occ_mo_coeffs,
162 const AOBasis& auxbasis,
163 const AOBasis& dftbasis) {
164 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
165 Index n_aux_bf = auxbasis.AOBasisSize();
166 Index nocc = occ_mo_coeffs.cols();
167
168 std::vector<Eigen::MatrixXd> tensor =
169 ComputeThreeCenterIntegrals(auxbasis, dftbasis);
170
171 AOCoulomb aocoulomb;
172 aocoulomb.Fill(auxbasis);
173 const Eigen::MatrixXd& V = aocoulomb.Matrix();
174 // V is positive definite; Cholesky, LDLT as before if it is not
175 const CholeskyFactor V_chol(V);
176 Eigen::LDLT<Eigen::MatrixXd> V_ldlt;
177 if (!V_chol.ok()) {
178 V_ldlt.compute(V);
179 }
180
181 std::vector<AOMatrixDerivative> dV =
183
184 // Single pass over ALL atoms at once -- see
185 // ComputeThreeCenterDerivativesMOTransformed's own header comment for
186 // the full reasoning (including the arithmetic error that had
187 // motivated a slower, per-atom approach instead): ~23.8 GB total for
188 // the real, 53-atom/943-AO/2320-auxiliary/93-occupied-orbital system
189 // that originally motivated this whole fix, smaller than even that
190 // per-atom approach's own ~46 GB peak, and needing only ONE pass over
191 // the expensive shell-triple loop rather than natoms of them.
192 std::vector<ThreeCenterDerivative> d3c_mo =
194 occ_mo_coeffs);
195
196 Eigen::MatrixXd grad = Eigen::MatrixXd::Zero(natoms, 3);
197
198 // d_ij(P) = C_i^T tensor[P] C_j, c_ij = V^-1 d_ij (i,j both occupied
199 // MOs, all ordered pairs including i==j).
200 // E_K = -2 * sum_{i,j} [0.5 * c_ij . d_ij] = -sum_{i,j} c_ij . d_ij
201 // (matches ERIs::CalculateEXX_mos's real physical exchange energy
202 // exactly, confirmed numerically, not just up to an unknown scale).
203 //
204 // PERFORMANCE: confirmed directly, via a real run, to be a genuine
205 // bottleneck in its naive form -- computing tensor[p]*occ_mo_coeffs.col(j)
206 // (an O(nao^2) matrix-vector product) separately for every (i,j)
207 // pair recomputes the SAME result nocc times over (it does not
208 // depend on i at all). Factored out here: tensor[p]*occ_mo_coeffs
209 // (all occupied columns at once) is computed ONCE per p, and every
210 // d(p) for a given (i,j) is then a cheap O(nao) dot product against
211 // the appropriate column of that already-computed result -- reduces
212 // the dominant cost by roughly a factor of nocc. Purely a
213 // reordering of the SAME formula (see the "NOTE ON HISTORY" comment
214 // below for why changing the formula itself would be dangerous);
215 // mathematically identical to the original for every element, not a
216 // different computation.
217 std::vector<Eigen::MatrixXd> tensor_half(n_aux_bf);
218 for (Index p = 0; p < n_aux_bf; ++p) {
219 tensor_half[p] = tensor[p] * occ_mo_coeffs; // (nao, nocc)
220 }
221 std::vector<std::vector<Eigen::VectorXd>> c_ij(
222 nocc, std::vector<Eigen::VectorXd>(nocc));
223 for (Index i = 0; i < nocc; ++i) {
224 for (Index j = 0; j < nocc; ++j) {
225 Eigen::VectorXd d(n_aux_bf);
226 for (Index p = 0; p < n_aux_bf; ++p) {
227 d(p) = occ_mo_coeffs.col(i).dot(tensor_half[p].col(j));
228 }
229 c_ij[i][j] = V_chol.ok() ? Eigen::VectorXd(V_chol.Solve(d))
230 : Eigen::VectorXd(V_ldlt.solve(d));
231 }
232 }
233
234 // NOTE ON HISTORY: an earlier revision of this function briefly
235 // switched to a "half-transformed" structure (one index MO, one AO),
236 // reasoned (incorrectly, via error-prone hand algebra) to be needed
237 // to match ERIs::CalculateEXX_mos's real K matrix. Settled by DIRECT
238 // NUMERICAL SIMULATION of CalculateEXX_mos's actual algorithm
239 // (symmetric V^-1/2 fit, TCxMOs_T = occMos^T*B_tilde, etc.) against
240 // both candidate formulas, on several random test systems: the
241 // FULLY-MO-transformed structure below (both indices occupied MOs,
242 // matching the ORIGINAL version of this function) is correct, and
243 // matches the real energy EXACTLY (to ~1e-14) once multiplied by 2 --
244 // not the half-transformed structure, which did not match at all
245 // (not even up to a constant factor). See conversation history for
246 // the verification. This confirms the earlier documented concern
247 // (needing to differentiate a matrix square root) was never actually
248 // a problem -- V^-1 and V^-1/2 fitting give identical energies
249 // (|V^-1/2 x|^2 == x^T V^-1 x exactly, for symmetric positive-definite
250 // V) -- the only real fix needed here was the missing factor of 2.
251 // (NOTE: this history comment is about a DIFFERENT half-transformed
252 // structure than the one introduced just above -- that one changed
253 // the final FORMULA itself and gave a wrong physical answer; the one
254 // just above only reorders/factors the SAME formula's own
255 // computation and is mathematically identical to the original for
256 // every element, not a different computation at all.)
257
258#pragma omp parallel for
259 for (Index a = 0; a < natoms; ++a) {
260 for (Index xyz = 0; xyz < 3; ++xyz) {
261 double energy_term = 0.0;
262 double metric_term = 0.0;
263 for (Index i = 0; i < nocc; ++i) {
264 for (Index j = 0; j < nocc; ++j) {
265 const Eigen::VectorXd& c = c_ij[i][j];
266 // d3c_mo[a][xyz][p] is already the full (nocc, nocc)
267 // MO-transformed matrix -- (i, j) is read off directly, no
268 // further matrix-vector multiplication needed at all (unlike
269 // the old, per-atom AO-basis d3c_atom[xyz][p], which still
270 // needed occ_mo_coeffs.col(i).dot(d3c_atom[xyz][p] *
271 // occ_mo_coeffs.col(j)) for every (i,j) pair).
272 Eigen::VectorXd dd(n_aux_bf);
273 for (Index p = 0; p < n_aux_bf; ++p) {
274 dd(p) = d3c_mo[a][xyz][p](i, j);
275 }
276 energy_term += c.dot(dd);
277 metric_term += c.dot(dV[a][xyz] * c);
278 }
279 }
280 // Factor of -2, matching E_K = -2*sum[0.5*c.d] confirmed above --
281 // NOT the "-(energy_term - 0.5*metric_term)" (factor of -1) used
282 // in the intermediate, incorrect half-transformed revision.
283 grad(a, xyz) = -2.0 * (energy_term - 0.5 * metric_term);
284 }
285 }
286 return grad;
287}
288
289} // namespace xtp
290} // namespace votca
Container to hold Basisfunctions for all atoms.
Definition aobasis.h:42
Index AOBasisSize() const
Definition aobasis.h:46
const std::vector< Index > & getFuncPerAtom() const
Definition aobasis.h:72
void Fill(const AOBasis &aobasis) final
const Eigen::MatrixXd & Matrix() const
Definition aomatrix.h:72
const Eigen::Vector3d & getPos() const
Eigen::MatrixXd Solve(const Eigen::MatrixXd &B) const
A^-1 B.
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)
Charge transport classes.
Definition ERIs.h:28
std::vector< AOMatrixDerivative > ComputeCoulombMetricDerivatives(const AOBasis &aobasis)
std::vector< ThreeCenterDerivative > ComputeThreeCenterDerivatives(const AOBasis &auxbasis, const AOBasis &dftbasis)
std::vector< ThreeCenterDerivative > ComputeThreeCenterDerivativesMOTransformed(const AOBasis &auxbasis, const AOBasis &dftbasis, const Eigen::MatrixXd &occ_mo_coeffs)
std::vector< Eigen::MatrixXd > ComputeThreeCenterIntegrals(const AOBasis &auxbasis, const AOBasis &dftbasis)
std::vector< Eigen::MatrixXd > ComputeThreeCenterDerivativeContraction(const AOBasis &auxbasis, const AOBasis &dftbasis, const Eigen::MatrixXd &density)
std::array< std::vector< Eigen::MatrixXd >, 3 > ThreeCenterDerivative
std::array< Eigen::MatrixXd, 3 > AOMatrixDerivative
Definition dftengine.cc:421
ThreeCenterDerivative ComputeThreeCenterDerivativesForAtom(const AOBasis &auxbasis, const AOBasis &dftbasis, Index target_atom)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26