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