votca 2026-dev
Loading...
Searching...
No Matches
vxc_potential.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// Third party includes
21#include <algorithm>
22#include <boost/format.hpp>
23#include <cmath>
24#include <exception>
25#include <iomanip>
26#include <iostream>
27#include <stdexcept>
28#include <vector>
29
30// VOTCA includes
32
33// Local VOTCA includes
34#include "votca/xtp/aobasis.h"
37#include "votca/xtp/vxc_grid.h"
39
40namespace votca {
41namespace xtp {
42
43template <class Grid>
45 if (setXC_) {
46 xc_func_end(&xfunc);
47 xc_func_end(&xfunc_pol_);
48 if (use_separate_) {
49 xc_func_end(&cfunc);
50 xc_func_end(&cfunc_pol_);
51 }
52 }
53}
54
55template <class Grid>
56double Vxc_Potential<Grid>::getExactExchange(const std::string& functional) {
57 double exactexchange = 0.0;
59
60 std::vector<std::string> functional_names =
61 tools::Tokenizer(functional, " ").ToVector();
62
63 if (functional_names.size() > 2) {
64 throw std::runtime_error("Too many functional names");
65 } else if (functional_names.empty()) {
66 throw std::runtime_error("Specify at least one functional");
67 }
68
69 for (const std::string& functional_name : functional_names) {
70 int func_id = map.getID(functional_name);
71 if (func_id < 0) {
72 exactexchange = 0.0;
73 break;
74 }
75
76 xc_func_type func;
77 if (xc_func_init(&func, func_id, XC_UNPOLARIZED) != 0) {
78 throw std::runtime_error(
79 (boost::format("Functional %s not found\n") % functional_name).str());
80 }
81
82 if (exactexchange > 0 && func.cam_alpha > 0) {
83 xc_func_end(&func);
84 throw std::runtime_error(
85 "You have specified two functionals with exact exchange");
86 }
87
88 exactexchange += func.cam_alpha;
89 xc_func_end(&func);
90 }
91
92 return exactexchange;
93}
94
95template <class Grid>
96void Vxc_Potential<Grid>::setXCfunctional(const std::string& functional) {
98 std::vector<std::string> strs =
99 tools::Tokenizer(functional, " ,\n\t").ToVector();
100
101 xfunc_id = 0;
102 use_separate_ = false;
103 cfunc_id = 0;
104
105 if (strs.size() == 1) {
106 xfunc_id = map.getID(strs[0]);
107 } else if (strs.size() == 2) {
108 xfunc_id = map.getID(strs[0]);
109 cfunc_id = map.getID(strs[1]);
110 use_separate_ = true;
111 } else {
112 throw std::runtime_error(
113 "LIBXC. Please specify one combined or an exchange and a correlation "
114 "functionals");
115 }
116
117 // Keep the stored handles UNPOLARIZED for the closed-shell/restricted code
118 // path. The UKS path creates temporary polarized handles inside
119 // EvaluateXCSpin().
120 if (xc_func_init(&xfunc, xfunc_id, XC_UNPOLARIZED) != 0) {
121 throw std::runtime_error(
122 (boost::format("Functional %s not found\n") % strs[0]).str());
123 }
124
125 if (xfunc.info->kind != 2 && !use_separate_) {
126 throw std::runtime_error(
127 "Your functional misses either correlation or exchange, please specify "
128 "another functional, separated by whitespace");
129 }
130
131 if (use_separate_) {
132 if (xc_func_init(&cfunc, cfunc_id, XC_UNPOLARIZED) != 0) {
133 xc_func_end(&xfunc);
134 throw std::runtime_error(
135 (boost::format("Functional %s not found\n") % strs[1]).str());
136 }
137
138 if ((xfunc.info->kind + cfunc.info->kind) != 1) {
139 xc_func_end(&xfunc);
140 xc_func_end(&cfunc);
141 throw std::runtime_error(
142 "Your functionals are not one exchange and one correlation");
143 }
144 }
145
146 // Polarized twins for the UKS path.
147 if (xc_func_init(&xfunc_pol_, xfunc_id, XC_POLARIZED) != 0) {
148 throw std::runtime_error(
149 "Failed to initialize polarized exchange XC "
150 "functional.");
151 }
152 if (use_separate_ && xc_func_init(&cfunc_pol_, cfunc_id, XC_POLARIZED) != 0) {
153 xc_func_end(&xfunc_pol_);
154 throw std::runtime_error(
155 "Failed to initialize polarized correlation XC "
156 "functional.");
157 }
158
159 auto is_gga = [](const xc_func_type& f) {
160 return f.info->family == XC_FAMILY_GGA ||
161 f.info->family == XC_FAMILY_HYB_GGA;
162 };
163 is_gga_ = is_gga(xfunc) || (use_separate_ && is_gga(cfunc));
164
165 setXC_ = true;
166 return;
167}
168
169namespace {
170// One libxc call for n points with any supported family.
171void CallLibxc(const xc_func_type& f, Index n, const double* rho,
172 const double* sigma, double* exc, double* vrho, double* vsigma) {
173 switch (f.info->family) {
174 case XC_FAMILY_LDA:
175 xc_lda_exc_vxc(&f, std::size_t(n), rho, exc, vrho);
176 break;
177 case XC_FAMILY_GGA:
178 case XC_FAMILY_HYB_GGA:
179 xc_gga_exc_vxc(&f, std::size_t(n), rho, sigma, exc, vrho, vsigma);
180 break;
181 default:
182 throw std::runtime_error("Unsupported XC functional family.");
183 }
184}
185} // namespace
186
187template <class Grid>
189 const double* sigma, double* exc,
190 double* vrho, double* vsigma) const {
191 std::fill(vsigma, vsigma + n, 0.0);
192 CallLibxc(xfunc, n, rho, sigma, exc, vrho, vsigma);
193 if (use_separate_) {
194 std::vector<double> e(n), vr(n), vs(n, 0.0);
195 CallLibxc(cfunc, n, rho, sigma, e.data(), vr.data(), vs.data());
196 for (Index i = 0; i < n; ++i) {
197 exc[i] += e[i];
198 vrho[i] += vr[i];
199 vsigma[i] += vs[i];
200 }
201 }
202}
203
204template <class Grid>
206 const double* sigma, double* exc,
207 double* vrho,
208 double* vsigma) const {
209 std::fill(vsigma, vsigma + 3 * n, 0.0);
210 CallLibxc(xfunc_pol_, n, rho, sigma, exc, vrho, vsigma);
211 if (use_separate_) {
212 std::vector<double> e(n), vr(2 * n), vs(3 * n, 0.0);
213 CallLibxc(cfunc_pol_, n, rho, sigma, e.data(), vr.data(), vs.data());
214 for (Index i = 0; i < n; ++i) {
215 exc[i] += e[i];
216 }
217 for (Index i = 0; i < 2 * n; ++i) {
218 vrho[i] += vr[i];
219 }
220 for (Index i = 0; i < 3 * n; ++i) {
221 vsigma[i] += vs[i];
222 }
223 }
224}
225
226template <class Grid>
228 double rho, double sigma) const {
229 typename Vxc_Potential<Grid>::XC_entry result;
230 EvaluateXCBatch(1, &rho, &sigma, &result.f_xc, &result.df_drho,
231 &result.df_dsigma);
232 return result;
233}
234
235template <class Grid>
237 double rho_a, double rho_b, double sigma_aa, double sigma_ab,
238 double sigma_bb) const {
240 const double rho[2] = {rho_a, rho_b};
241 const double sigma[3] = {sigma_aa, sigma_ab, sigma_bb};
242 double vrho[2];
243 double vsigma[3];
244 EvaluateXCSpinBatch(1, rho, sigma, &result.f_xc, vrho, vsigma);
245 result.vrho_a = vrho[0];
246 result.vrho_b = vrho[1];
247 result.vsigma_aa = vsigma[0];
248 result.vsigma_ab = vsigma[1];
249 result.vsigma_bb = vsigma[2];
250 return result;
251}
252
253// Points are processed in blocks inside each box. With Phi the box's AO
254// values (functions x points) and dPhi their gradients, the work that scales
255// with the square of the box's basis size is two matrix products per block:
256// Y = D Phi (density and its gradient at every point)
257// Vxc += A Phi^T, A_p = w_p (vrho_p/2 phi_p + 2 vsigma_p dPhi_p grad rho_p)
258// and libxc is called once per block. Per point, only O(functions) work
259// remains. Same formulas as the former point-by-point loop.
260template <class Grid>
262 const Eigen::MatrixXd& density_matrix) const {
263 assert(density_matrix.isApprox(density_matrix.transpose()) &&
264 "Density matrix has to be symmetric!");
265
266 Mat_p_Energy vxc = Mat_p_Energy(density_matrix.rows(), density_matrix.cols());
267
268#pragma omp parallel for schedule(guided) reduction(+ : vxc)
269 for (Index i = 0; i < grid_.getBoxesSize(); ++i) {
270 const GridBox& box = grid_[i];
271 if (!box.Matrixsize()) {
272 continue;
273 }
274
275 // two because we have to use the density matrix and its transpose
276 const Eigen::MatrixXd DMAT_here = 2 * box.ReadFromBigMatrix(density_matrix);
277
278 double cutoff =
279 1.e-40 / double(density_matrix.rows()) / double(density_matrix.rows());
280 if (DMAT_here.cwiseAbs2().maxCoeff() < cutoff) {
281 continue;
282 }
283
284 const Index nf = box.Matrixsize();
285 const Index block = std::min(kPointBlock, box.size());
286 Eigen::MatrixXd Vxc_here = Eigen::MatrixXd::Zero(nf, nf);
287 Eigen::MatrixXd phi(nf, block);
288 Eigen::MatrixXd dphi(nf, 3 * block);
289 Eigen::MatrixXd y(nf, block);
290 Eigen::MatrixXd a(nf, block);
291 std::vector<Index> kept;
292 std::vector<double> rho, sigma, exc, vrho, vsigma;
293 std::vector<Eigen::Vector3d> grad;
294 double EXC_box = 0.0;
295
296 const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
297 const std::vector<double>& weights = box.getGridWeights();
298
299 for (Index start = 0; start < box.size(); start += block) {
300 const Index nb = std::min(block, box.size() - start);
301 for (Index p = 0; p < nb; ++p) {
302 box.CalcAOValues(points[start + p], phi, dphi, p);
303 }
304 y.leftCols(nb).noalias() = DMAT_here * phi.leftCols(nb);
305
306 kept.clear();
307 rho.clear();
308 sigma.clear();
309 grad.clear();
310 for (Index p = 0; p < nb; ++p) {
311 const double r = 0.5 * phi.col(p).dot(y.col(p));
312 if (r * weights[start + p] < 1.e-20) {
313 continue;
314 }
315 const Eigen::Vector3d g =
316 dphi.middleCols(3 * p, 3).transpose() * y.col(p);
317 kept.push_back(p);
318 rho.push_back(r);
319 sigma.push_back(g.squaredNorm());
320 grad.push_back(g);
321 }
322 const Index nk = Index(kept.size());
323 if (nk == 0) {
324 continue;
325 }
326 exc.resize(nk);
327 vrho.resize(nk);
328 vsigma.resize(nk);
329 EvaluateXCBatch(nk, rho.data(), sigma.data(), exc.data(), vrho.data(),
330 vsigma.data());
331
332 // Kept points are compacted to the front: column k receives point
333 // kept[k] >= k, which has been read by then.
334 for (Index k = 0; k < nk; ++k) {
335 const Index p = kept[k];
336 const double w = weights[start + p];
337 EXC_box += w * rho[k] * exc[k];
338 a.col(k) = (w * 0.5 * vrho[k]) * phi.col(p);
339 if (is_gga_) {
340 a.col(k).noalias() +=
341 (w * 2.0 * vsigma[k]) * (dphi.middleCols(3 * p, 3) * grad[k]);
342 }
343 if (k != p) {
344 phi.col(k) = phi.col(p);
345 }
346 }
347 Vxc_here.noalias() += a.leftCols(nk) * phi.leftCols(nk).transpose();
348 }
349
350 box.AddtoBigMatrix(vxc.matrix(), Vxc_here);
351 vxc.energy() += EXC_box;
352 }
353
354 return Mat_p_Energy(vxc.energy(), vxc.matrix() + vxc.matrix().transpose());
355}
356
357// ===========================================================================
358// Derivation (worked out before writing this, checked against LibXC's own
359// documented API convention rather than assumed):
360//
361// E_xc = sum_p weight_p * e_xc(rho_p) (e_xc = rho*f_xc, energy density)
362//
363// dE_xc/dR_A |_Pulay = sum_p weight_p * v_xc(rho_p) * d(rho_p)/dR_A|_basis
364//
365// where v_xc(rho) = de_xc/drho. Confirmed (not assumed) that this is
366// EXACTLY xc.df_drho as already computed by EvaluateXC: xc_lda_exc_vxc /
367// xc_gga_exc_vxc are LibXC's own named functions -- "exc" = energy per
368// particle (f_xc), "vxc" = the potential dE_xc/drho directly (LibXC
369// applies the f_xc + rho*df_xc/drho correction internally; the "vxc"
370// output already IS the full potential, matching how df_drho is already
371// used to build Vxc_here above -- no extra term needed here).
372//
373// For an atom-centered Gaussian chi_mu(r) = phi_mu(r - R_A), the nuclear
374// derivative is EXACTLY -grad_r(chi_mu) for the atom mu is centered on,
375// and exactly zero for every other atom (since r-R_A depends negatively
376// on R_A). This means d(chi_mu)/dR_A can be read directly off
377// ao.derivatives (already computed for the GGA sigma term above), just
378// negated and restricted to basis functions centered on atom A -- no new
379// basis-function-derivative machinery is needed for this piece.
380//
381// Using the symmetry of the density matrix (same trick as the standard
382// Pulay-force derivation):
383// d(rho_p)/dR_A|_basis = -2 * sum_{mu in A} sum_nu P_munu (grad_r
384// chi_mu)(r_p) chi_nu(r_p)
385// = -sum_{mu in A} temp(mu) * (grad_r chi_mu)(r_p)
386// where temp = ao.values^T * DMAT_here (DMAT_here = 2P, already exactly
387// what IntegrateVXC computes above) -- the factor of 2 from symmetry is
388// already folded into DMAT_here, so no extra factor is needed here either.
389//
390// SCOPE (see also the STATUS note on the declaration in vxc_potential.h):
391// this captures the FULL Pulay term for LDA functionals. For GGA
392// functionals, it captures only the df_drho-driven part; the additional
393// df_dsigma-driven Pulay contribution needs second derivatives of basis
394// functions w.r.t. electron position, not implemented here. The
395// grid-weight (SSW partition) derivative term is a SEPARATE piece,
396// deliberately not included in this function.
397//
398// STATUS: written but NOT yet run/tested.
399// ===========================================================================
400template <class Grid>
402 const Eigen::MatrixXd& density_matrix, const AOBasis& dftbasis) const {
403 assert(density_matrix.isApprox(density_matrix.transpose()) &&
404 "Density matrix has to be symmetric!");
405
406 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
407 Index nthreads = OPENMP::getMaxThreads();
408 std::vector<Eigen::MatrixXd> grad_thread(nthreads,
409 Eigen::MatrixXd::Zero(natoms, 3));
410
411 std::exception_ptr eptr_pulay = nullptr;
412#pragma omp parallel for schedule(guided)
413 for (Index i = 0; i < grid_.getBoxesSize(); ++i) {
414 try {
415 Index thread_id = OPENMP::getThreadId();
416 const GridBox& box = grid_[i];
417 if (!box.Matrixsize()) {
418 continue;
419 }
420
421 const Eigen::MatrixXd DMAT_here =
422 2 * box.ReadFromBigMatrix(density_matrix);
423
424 double cutoff = 1.e-40 / double(density_matrix.rows()) /
425 double(density_matrix.rows());
426 if (DMAT_here.cwiseAbs2().maxCoeff() < cutoff) {
427 continue;
428 }
429
430 // Box-local AO index -> atom index, built once per box (not once per
431 // grid point), using the same getShells()/getAOranges() mapping the
432 // rest of GridBox already relies on for its own big-matrix
433 // read/write bookkeeping.
434 std::vector<Index> local_idx_to_atom(box.Matrixsize());
435 const std::vector<const AOShell*>& shells = box.getShells();
436 const std::vector<GridboxRange>& ao_ranges = box.getAOranges();
437 for (size_t s = 0; s < shells.size(); ++s) {
438 Index atom = shells[s]->getAtomIndex();
439 for (Index k = 0; k < ao_ranges[s].size; ++k) {
440 local_idx_to_atom[ao_ranges[s].start + k] = atom;
441 }
442 }
443
444 const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
445 const std::vector<double>& weights = box.getGridWeights();
446 // Needed for the grid-point-translation term added below -- NOT
447 // needed by the basis-function/Pulay term itself, which only cares
448 // about which atom owns each AO (local_idx_to_atom above), not
449 // which atom owns each grid POINT.
450 const std::vector<Index>& owner_atoms = box.getOwnerAtoms();
451
452 for (Index p = 0; p < box.size(); ++p) {
454
455 Eigen::VectorXd temp = ao.values.transpose() * DMAT_here;
456 double rho = 0.5 * temp.dot(ao.values);
457 const double weight = weights[p];
458
459 if (rho * weight < 1.e-20) {
460 continue;
461 }
462
463 const Eigen::Vector3d rho_grad = temp.transpose() * ao.derivatives;
465 EvaluateXC(rho, rho_grad.squaredNorm());
466
467 // ===========================================================
468 // GGA SIGMA-DEPENDENT TERMS (added after the LDA-only version was
469 // fully validated -- see aoshell.h/aoshell.cc for the
470 // EvalAOspaceHessian this depends on, and conversation history
471 // for the derivation, independently verified numerically in
472 // Python on a toy multi-atom system to ~1e-12 before writing any
473 // of this C++). Exactly zero for LDA functionals (xc.df_dsigma is
474 // default-initialized to 0 and untouched by xc_lda_exc_vxc, which
475 // never writes to it -- confirmed directly in EvaluateXC above),
476 // so this cannot regress the already-validated LDA behavior;
477 // safe to compute unconditionally rather than branch on
478 // functional type.
479 //
480 // sigma_p = |grad_r(rho_p)|^2 = rho_grad . rho_grad
481 //
482 // Gmat(mu,l) = sum_nu DMAT_mu,nu * grad_nu,l = (DMAT_here *
483 // ao.derivatives) s_vec(mu) = sum_l Gmat(mu,l) * rho_grad_l = Gmat *
484 // rho_grad
485 //
486 // Basis-type (mu in A):
487 // dsigma_p/dR_A|_basis = -2 * sum_{mu in A} [ grad_mu * s_vec(mu)
488 // + temp_mu *
489 // (Hessian_mu .
490 // rho_grad) ]
491 //
492 // Translation-type (A == owner(p) only), via the density's OWN
493 // Hessian at this point:
494 // Hessian_rho = sum_mu temp_mu*Hessian_mu
495 // + symmetrized(ao.derivatives^T * Gmat)
496 // dsigma_p/dR_owner|_translation = 2 * (Hessian_rho * rho_grad)
497 //
498 // Both contract with v_sigma = xc.df_dsigma (LibXC's vsigma
499 // convention, analogous to df_drho/vrho -- already the full
500 // dE_xc/dsigma, no further correction needed, same reasoning
501 // already confirmed for df_drho) and the point's full weight,
502 // exactly like the rho-dependent terms above/below.
503 Eigen::MatrixX3d Gmat = DMAT_here * ao.derivatives; // (Matrixsize x 3)
504 Eigen::VectorXd s_vec = Gmat * rho_grad; // (Matrixsize)
505
506 Eigen::Matrix3d Hessian_rho = Eigen::Matrix3d::Zero();
507 for (Index mu = 0; mu < box.Matrixsize(); ++mu) {
508 Hessian_rho += temp(mu) * ao.hessians[mu];
509 }
510 Eigen::Matrix3d M = ao.derivatives.transpose() * Gmat;
511 Hessian_rho += 0.5 * (M + M.transpose());
512
513 Index owner_of_point_sigma = owner_atoms[p];
514 Eigen::Vector3d dsigma_translation = 2.0 * (Hessian_rho * rho_grad);
515 grad_thread[thread_id].row(owner_of_point_sigma) +=
516 (weight * xc.df_dsigma * dsigma_translation).transpose();
517 // ===========================================================
518
519 // ===========================================================
520 // NEWLY ADDED TERM: grid-point translation.
521 //
522 // A genuinely distinct THIRD contribution to the XC gradient,
523 // separate from both the basis-function/Pulay term below and the
524 // grid-weight (SSW partition) term in GridWeightGradient. Found
525 // by re-deriving the full chain rule for rho_p(r_p(R)) after the
526 // C_p fix (in GridWeightGradient) substantially improved but did
527 // not fully resolve a residual discrepancy against finite
528 // differences.
529 //
530 // Grid points are rigidly attached to their owner atom
531 // (r_p = R_owner + local_offset, local_offset fixed). Since
532 // rho_p = rho(r_p(R)), differentiating through r_p itself (not
533 // just through the basis functions' own centers, which is what
534 // the Pulay term below captures) gives an EXTRA term whenever
535 // A is this point's own owner:
536 //
537 // d(rho_p)/dR_A |_translation = (dr_p/dR_A) . grad_r(rho_p)
538 // = grad_r(rho_p) if A == owner(p)
539 // = 0 otherwise
540 //
541 // (dr_p/dR_owner = identity, since r_p moves rigidly with its
542 // owner; dr_p/dR_A = 0 for any other atom, since a point's
543 // position never depends on any atom besides its own owner).
544 // grad_r(rho_p) is exactly rho_grad, already computed above for
545 // the GGA sigma term -- no new integral or expensive computation
546 // needed, just one more accumulation using quantities already in
547 // hand. Contracting with v_xc = xc.df_drho (same convention
548 // established for the basis-function term below) and the
549 // point's full stored weight (this term does NOT involve any
550 // weight derivative -- weight is held fixed here, exactly like
551 // the Pulay term; only rho_p's dependence on the point's own
552 // moving position is new):
553 //
554 // dE_xc/dR_A |_translation = sum_{p: owner(p)==A}
555 // weight_p * v_xc(rho_p) *
556 // grad_r(rho_p)
557 Index owner_of_point = owner_atoms[p];
558 grad_thread[thread_id].row(owner_of_point) +=
559 (weight * xc.df_drho * rho_grad).transpose();
560 // ===========================================================
561
562 // d(rho_p)/dR_A|_basis, accumulated per local AO index mu, then
563 // scattered to the correct atom via local_idx_to_atom. Looping
564 // over local indices directly (rather than trying to slice
565 // contiguous per-atom row ranges) since a single box's
566 // significant shells are not guaranteed to be grouped
567 // contiguously by atom.
568 for (Index mu = 0; mu < box.Matrixsize(); ++mu) {
569 Index atom = local_idx_to_atom[mu];
570 Eigen::Vector3d contribution = -weight * xc.df_drho * temp(mu) *
571 ao.derivatives.row(mu).transpose();
572 // GGA sigma basis-type term, added to the same per-mu
573 // contribution (same atom, same accumulation) -- see the
574 // detailed derivation comment above.
575 Eigen::Vector3d dsigma_basis_mu =
576 -2.0 * (ao.derivatives.row(mu).transpose() * s_vec(mu) +
577 temp(mu) * (ao.hessians[mu] * rho_grad));
578 contribution += weight * xc.df_dsigma * dsigma_basis_mu;
579 grad_thread[thread_id].row(atom) += contribution.transpose();
580 }
581 }
582 } catch (...) {
583#pragma omp critical
584 {
585 if (!eptr_pulay) {
586 eptr_pulay = std::current_exception();
587 }
588 }
589 }
590 }
591 if (eptr_pulay) {
592 std::rethrow_exception(eptr_pulay);
593 }
594
595 Eigen::MatrixXd grad = Eigen::MatrixXd::Zero(natoms, 3);
596 for (Index t = 0; t < nthreads; ++t) {
597 grad += grad_thread[t];
598 }
599 return grad;
600}
601
602// Blocked like IntegrateVXC, per spin channel. UKS density matrices are not
603// pre-doubled: rho_s = phi^T D_s phi, grad rho_s = 2 dPhi^T D_s phi.
604template <class Grid>
606 const Eigen::MatrixXd& dmat_alpha, const Eigen::MatrixXd& dmat_beta) const {
607 assert(dmat_alpha.isApprox(dmat_alpha.transpose()) &&
608 "Alpha density matrix has to be symmetric!");
609 assert(dmat_beta.isApprox(dmat_beta.transpose()) &&
610 "Beta density matrix has to be symmetric!");
611
612 typename Vxc_Potential<Grid>::SpinResult result;
613 result.vxc_alpha =
614 Eigen::MatrixXd::Zero(dmat_alpha.rows(), dmat_alpha.cols());
615 result.vxc_beta = Eigen::MatrixXd::Zero(dmat_beta.rows(), dmat_beta.cols());
616
617#pragma omp parallel
618 {
619 Eigen::MatrixXd vxc_alpha_private =
620 Eigen::MatrixXd::Zero(dmat_alpha.rows(), dmat_alpha.cols());
621 Eigen::MatrixXd vxc_beta_private =
622 Eigen::MatrixXd::Zero(dmat_beta.rows(), dmat_beta.cols());
623 double exc_private = 0.0;
624
625 std::vector<Index> kept;
626 std::vector<double> rho, sigma, exc, vrho, vsigma;
627 std::vector<Eigen::Vector3d> grad_a, grad_b;
628
629#pragma omp for schedule(guided)
630 for (Index i = 0; i < grid_.getBoxesSize(); ++i) {
631 const GridBox& box = grid_[i];
632 if (!box.Matrixsize()) {
633 continue;
634 }
635
636 const Eigen::MatrixXd DMa = box.ReadFromBigMatrix(dmat_alpha);
637 const Eigen::MatrixXd DMb = box.ReadFromBigMatrix(dmat_beta);
638
639 double cutoff =
640 1.e-40 / double(dmat_alpha.rows()) / double(dmat_alpha.rows());
641 if (std::max(DMa.cwiseAbs2().maxCoeff(), DMb.cwiseAbs2().maxCoeff()) <
642 cutoff) {
643 continue;
644 }
645
646 const Index nf = box.Matrixsize();
647 const Index block = std::min(kPointBlock, box.size());
648 Eigen::MatrixXd Vxc_a_here = Eigen::MatrixXd::Zero(nf, nf);
649 Eigen::MatrixXd Vxc_b_here = Eigen::MatrixXd::Zero(nf, nf);
650 Eigen::MatrixXd phi(nf, block);
651 Eigen::MatrixXd dphi(nf, 3 * block);
652 Eigen::MatrixXd ya(nf, block);
653 Eigen::MatrixXd yb(nf, block);
654 Eigen::MatrixXd aa(nf, block);
655 Eigen::MatrixXd ab(nf, block);
656
657 const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
658 const std::vector<double>& weights = box.getGridWeights();
659
660 for (Index start = 0; start < box.size(); start += block) {
661 const Index nb = std::min(block, box.size() - start);
662 for (Index p = 0; p < nb; ++p) {
663 box.CalcAOValues(points[start + p], phi, dphi, p);
664 }
665 ya.leftCols(nb).noalias() = DMa * phi.leftCols(nb);
666 yb.leftCols(nb).noalias() = DMb * phi.leftCols(nb);
667
668 kept.clear();
669 rho.clear();
670 sigma.clear();
671 grad_a.clear();
672 grad_b.clear();
673 for (Index p = 0; p < nb; ++p) {
674 const double rho_a = phi.col(p).dot(ya.col(p));
675 const double rho_b = phi.col(p).dot(yb.col(p));
676 if ((rho_a + rho_b) * weights[start + p] < 1.e-20) {
677 continue;
678 }
679 const Eigen::Vector3d ga =
680 2.0 * (dphi.middleCols(3 * p, 3).transpose() * ya.col(p));
681 const Eigen::Vector3d gb =
682 2.0 * (dphi.middleCols(3 * p, 3).transpose() * yb.col(p));
683 kept.push_back(p);
684 rho.push_back(rho_a);
685 rho.push_back(rho_b);
686 sigma.push_back(ga.dot(ga));
687 sigma.push_back(ga.dot(gb));
688 sigma.push_back(gb.dot(gb));
689 grad_a.push_back(ga);
690 grad_b.push_back(gb);
691 }
692 const Index nk = Index(kept.size());
693 if (nk == 0) {
694 continue;
695 }
696 exc.resize(nk);
697 vrho.resize(2 * nk);
698 vsigma.resize(3 * nk);
699 EvaluateXCSpinBatch(nk, rho.data(), sigma.data(), exc.data(),
700 vrho.data(), vsigma.data());
701
702 for (Index k = 0; k < nk; ++k) {
703 const Index p = kept[k];
704 const double w = weights[start + p];
705 exc_private += w * (rho[2 * k] + rho[2 * k + 1]) * exc[k];
706 // 0.5 because the result is symmetrized by adding the transpose
707 aa.col(k) = (w * 0.5 * vrho[2 * k]) * phi.col(p);
708 ab.col(k) = (w * 0.5 * vrho[2 * k + 1]) * phi.col(p);
709 if (is_gga_) {
710 const auto d = dphi.middleCols(3 * p, 3);
711 const Eigen::VectorXd g_a = d * grad_a[k];
712 const Eigen::VectorXd g_b = d * grad_b[k];
713 const double vs_aa = vsigma[3 * k];
714 const double vs_ab = vsigma[3 * k + 1];
715 const double vs_bb = vsigma[3 * k + 2];
716 aa.col(k).noalias() += w * (2.0 * vs_aa * g_a + vs_ab * g_b);
717 ab.col(k).noalias() += w * (vs_ab * g_a + 2.0 * vs_bb * g_b);
718 }
719 if (k != p) {
720 phi.col(k) = phi.col(p);
721 }
722 }
723 Vxc_a_here.noalias() += aa.leftCols(nk) * phi.leftCols(nk).transpose();
724 Vxc_b_here.noalias() += ab.leftCols(nk) * phi.leftCols(nk).transpose();
725 }
726
727 box.AddtoBigMatrix(vxc_alpha_private, Vxc_a_here);
728 box.AddtoBigMatrix(vxc_beta_private, Vxc_b_here);
729 }
730
731#pragma omp critical
732 {
733 result.vxc_alpha += vxc_alpha_private + vxc_alpha_private.transpose();
734 result.vxc_beta += vxc_beta_private + vxc_beta_private.transpose();
735 result.energy += exc_private;
736 }
737 }
738
739 return result;
740}
741
742namespace {
743// Standalone re-implementation of Vxc_Grid's switching function and its
744// derivative -- NOT calling into Vxc_Grid::erf1c (private, and exposing
745// it seemed like more churn than re-stating this one small, stateless,
746// pure-math formula here). Must stay byte-for-byte consistent with
747// Vxc_Grid::erf1c (0.5*erfc(|x/(1-x^2)|*alpha), alpha=1/0.30) -- if that
748// function's constants ever change, this needs to change with it.
749constexpr double kSSWAlpha = 1.0 / 0.30;
750constexpr double kSSWCutoff = 0.725;
751
752constexpr double kSqrtPi = 1.7724538509055160273; // sqrt(pi), literal
753 // constant rather than
754 // M_PI (not standard
755 // C++, not otherwise
756 // used anywhere in
757 // this codebase --
758 // avoiding relying on
759 // it being defined).
760
761double SSWValue(double mu) {
762 double val = 0.5 * std::erfc(std::abs(mu / (1.0 - mu * mu)) * kSSWAlpha);
763 if (mu > 0.0) {
764 val = 1.0 - val;
765 }
766 return val;
767}
768
769// d(SSWValue)/d(mu). Exact closed form, verified numerically against
770// finite differences in Python (matching to ~1e-11) before translating
771// to C++ -- see conversation history.
772double SSWDerivative(double mu) {
773 double h = std::abs(mu) / (1.0 - mu * mu);
774 double sign_mu = (mu > 0.0) ? 1.0 : ((mu < 0.0) ? -1.0 : 0.0);
775 double one_minus_mu2 = 1.0 - mu * mu;
776 double hprime = sign_mu * (1.0 + mu * mu) / (one_minus_mu2 * one_minus_mu2);
777 double d_erf1c = -(kSSWAlpha / kSqrtPi) *
778 std::exp(-(kSSWAlpha * h) * (kSSWAlpha * h)) * hprime;
779 return (mu > 0.0) ? -d_erf1c : d_erf1c;
780}
781} // namespace
782
783template <class Grid>
785 const Eigen::MatrixXd& density_matrix, const QMMolecule& atoms) const {
786 Index natoms = atoms.size();
787 Eigen::MatrixXd Rij = grid_.CalcInverseAtomDist(atoms);
788
789 Index nthreads = OPENMP::getMaxThreads();
790 std::vector<Eigen::MatrixXd> grad_thread(nthreads,
791 Eigen::MatrixXd::Zero(natoms, 3));
792
793 std::exception_ptr eptr_weight = nullptr;
794#pragma omp parallel for schedule(guided)
795 for (Index i = 0; i < grid_.getBoxesSize(); ++i) {
796 try {
797 Index thread_id = OPENMP::getThreadId();
798 const GridBox& box = grid_[i];
799 if (!box.Matrixsize()) {
800 continue;
801 }
802
803 const Eigen::MatrixXd DMAT_here =
804 2 * box.ReadFromBigMatrix(density_matrix);
805 double cutoff = 1.e-40 / double(density_matrix.rows()) /
806 double(density_matrix.rows());
807 if (DMAT_here.cwiseAbs2().maxCoeff() < cutoff) {
808 continue;
809 }
810
811 const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
812 const std::vector<double>& weights = box.getGridWeights();
813 const std::vector<Index>& owner_atoms = box.getOwnerAtoms();
814
815 for (Index pidx = 0; pidx < box.size(); ++pidx) {
816 AOShell::AOValues ao = box.CalcAOValues(points[pidx]);
817 Eigen::VectorXd temp = ao.values.transpose() * DMAT_here;
818 double rho = 0.5 * temp.dot(ao.values);
819 double weight = weights[pidx];
820 if (rho * weight < 1.e-20) {
821 continue;
822 }
823 const Eigen::Vector3d rho_grad = temp.transpose() * ao.derivatives;
825 EvaluateXC(rho, rho_grad.squaredNorm());
826
827 Index owner = owner_atoms[pidx];
828 if (owner < 0) {
829 // Point predates owner-atom tracking (e.g. constructed via some
830 // other path not yet updated) -- cannot compute this term for
831 // it. Should not happen for any grid built via GridSetup after
832 // this change; flagged rather than silently skipped, since a
833 // silent skip here would quietly produce a wrong (incomplete)
834 // gradient.
835 throw std::runtime_error(
836 "GridWeightGradient: grid point has no owner_atom set -- was "
837 "this grid built via GridSetup after the owner-atom tracking "
838 "change?");
839 }
840
841 // rq(k) = distance from THIS point to atom k; needed for every
842 // atom, not just the owner, since the partition weight depends on
843 // distances to all atoms.
844 const Eigen::Vector3d& point = points[pidx];
845 Eigen::VectorXd rq(natoms);
846 for (Index k = 0; k < natoms; ++k) {
847 rq(k) = (point - atoms[k].getPos()).norm();
848 }
849
850 // Build p[], mu_table, sk_table, hard-cutoff flags for every pair
851 // -- same structure as Vxc_Grid::SSWpartition, but retaining the
852 // per-pair intermediate values needed for the derivative (the
853 // energy-level code discards these immediately after use).
854 Eigen::VectorXd p = Eigen::VectorXd::Ones(natoms);
855 Eigen::MatrixXd mu_table = Eigen::MatrixXd::Zero(natoms, natoms);
856 Eigen::MatrixXd sk_table = Eigen::MatrixXd::Zero(natoms, natoms);
857 // hard(j,i): 0 = smooth (sk_table valid), 1 = mu>cutoff (p[i]=0
858 // hard), -1 = mu<-cutoff (p[j]=0 hard). Only upper triangle (j<i)
859 // populated, matching the loop structure below.
860 Eigen::MatrixXi hard = Eigen::MatrixXi::Zero(natoms, natoms);
861 for (Index ii = 1; ii < natoms; ++ii) {
862 for (Index jj = 0; jj < ii; ++jj) {
863 double mu = (rq(ii) - rq(jj)) * Rij(jj, ii);
864 mu_table(jj, ii) = mu;
865 if (mu > kSSWCutoff) {
866 p(ii) = 0.0;
867 hard(jj, ii) = 1;
868 } else if (mu < -kSSWCutoff) {
869 p(jj) = 0.0;
870 hard(jj, ii) = -1;
871 } else {
872 double sk = SSWValue(mu);
873 sk_table(jj, ii) = sk;
874 p(jj) *= sk;
875 p(ii) *= (1.0 - sk);
876 }
877 }
878 }
879 double wsum = p.sum();
880 double w_owner = p(owner) / wsum;
881
882 // d(rq(k))/dR_A, per the case analysis verified in Python: zero
883 // unless A is this point's owner or A==k; +-unit vector otherwise
884 // (with the owner==k case being exactly zero, since the point and
885 // its own owner move together).
886 auto d_rq_dR = [&](Index k, Index A) -> Eigen::Vector3d {
887 if (A == owner && A == k) {
888 return Eigen::Vector3d::Zero();
889 } else if (A == owner) {
890 return (point - atoms[k].getPos()) / rq(k);
891 } else if (A == k) {
892 return -(point - atoms[k].getPos()) / rq(k);
893 }
894 return Eigen::Vector3d::Zero();
895 };
896 auto d_Rab_dR = [&](Index a, Index b, Index A) -> Eigen::Vector3d {
897 Eigen::Vector3d rvec = atoms[a].getPos() - atoms[b].getPos();
898 double Rab = rvec.norm();
899 if (A == a) {
900 return rvec / Rab;
901 } else if (A == b) {
902 return -rvec / Rab;
903 }
904 return Eigen::Vector3d::Zero();
905 };
906 // a<b convention throughout, matching mu_table(a,b) with a<b.
907 auto dmu_dR = [&](Index a, Index b, Index A) -> Eigen::Vector3d {
908 Eigen::Vector3d d_rq_b = d_rq_dR(b, A);
909 Eigen::Vector3d d_rq_a = d_rq_dR(a, A);
910 double Rab = 1.0 / Rij(a, b);
911 Eigen::Vector3d dRab = d_Rab_dR(a, b, A);
912 double mu = mu_table(a, b);
913 return (d_rq_b - d_rq_a) / Rab - (mu / Rab) * dRab;
914 };
915 auto dp_dR = [&](Index k, Index A) -> Eigen::Vector3d {
916 // Threshold, not exact-zero check: p(k) approaches zero
917 // SMOOTHLY as any of its factors approaches the SSW saturation
918 // boundary (sk->1 or 1-sk->0), and the log-derivative terms
919 // below (divided by sk or (1-sk)) blow up faster than p(k)
920 // itself vanishes, in that regime -- a classic 0/0 numerical
921 // instability. A state with negligible weight can't meaningfully
922 // contribute to the total either way, so treating it as exactly
923 // zero here is a defensible approximation, not just a numerical
924 // patch. Threshold value not yet tuned against real data; if
925 // this test now passes but with values that look suspiciously
926 // insensitive to h, or if a genuinely different tolerance is
927 // needed elsewhere, revisit this constant specifically.
928 constexpr double kNegligibleP = 1.e-8;
929 if (p(k) < kNegligibleP) {
930 return Eigen::Vector3d::Zero();
931 }
932 Eigen::Vector3d total = Eigen::Vector3d::Zero();
933 for (Index b = k + 1; b < natoms; ++b) {
934 if (hard(k, b) != 0) {
935 continue;
936 }
937 double skv = sk_table(k, b);
938 if (skv < kNegligibleP) {
939 continue; // this factor's own contribution to p(k) is
940 // already negligible; skip rather than divide by
941 // a near-zero skv.
942 }
943 total += (SSWDerivative(mu_table(k, b)) / skv) * dmu_dR(k, b, A);
944 }
945 for (Index a = 0; a < k; ++a) {
946 if (hard(a, k) != 0) {
947 continue;
948 }
949 double skv = sk_table(a, k);
950 double one_minus_skv = 1.0 - skv;
951 if (one_minus_skv < kNegligibleP) {
952 continue; // same reasoning, for the (1-sk) denominator.
953 }
954 total += (-SSWDerivative(mu_table(a, k)) / one_minus_skv) *
955 dmu_dR(a, k, A);
956 }
957 return p(k) * total;
958 };
959
960 // Guard against dividing by a near-zero w_owner: if w_owner is
961 // negligible, weight_p = C_p*w_owner is also negligible (C_p is a
962 // bounded quadrature weight, not something that can blow up to
963 // compensate), so this point's actual contribution to the total
964 // XC energy is negligible regardless of what C_p technically
965 // works out to -- same physical justification as the earlier
966 // p(k) threshold: a point with negligible weight can't
967 // meaningfully affect the total either way, so treating it as
968 // contributing exactly zero here is defensible, not just a
969 // numerical patch.
970 constexpr double kNegligibleWOwner = 1.e-8;
971 if (w_owner < kNegligibleWOwner) {
972 continue;
973 }
974
975 // BUG FIX: weight (as stored/read from the grid) is NOT w_owner
976 // alone -- per GridSetup, weight_p = C_p * w_owner(p), where C_p
977 // is the raw radial*angular quadrature weight (position-
978 // independent -- depends only on the fixed Lebedev/Euler-Maclaurin
979 // grid design for the owning element, never on any atom's
980 // position) and w_owner is the SSW partition fraction computed
981 // above. The energy contribution from this point is
982 // weight_p*rho_p*f_xc_p = C_p*w_owner*rho_p*f_xc_p, so
983 // differentiating w_owner alone (which is all `dw` below
984 // computes) needs the missing C_p = weight/w_owner factor too --
985 // previously this was bare rho*f_xc, silently missing C_p, which
986 // is ~1 for "typical" points but swings far from 1 for points
987 // where the partition is lopsided (w_owner small), exactly
988 // matching why the resulting error's magnitude depended on which
989 // points a given density matrix happened to weight rather than
990 // ever looking pathological in any single point's own dw value.
991 double C_p = weight / w_owner;
992
993 double prefactor = C_p * rho * xc.f_xc;
994
995 for (Index A = 0; A < natoms; ++A) {
996 Eigen::Vector3d dp_owner = dp_dR(owner, A);
997 Eigen::Vector3d dwsum = Eigen::Vector3d::Zero();
998 for (Index k = 0; k < natoms; ++k) {
999 dwsum += dp_dR(k, A);
1000 }
1001 Eigen::Vector3d dw = dp_owner / wsum - w_owner * dwsum / wsum;
1002 Eigen::Vector3d contribution = prefactor * dw;
1003 grad_thread[thread_id].row(A) += contribution.transpose();
1004 }
1005 }
1006 } catch (...) {
1007#pragma omp critical
1008 {
1009 if (!eptr_weight) {
1010 eptr_weight = std::current_exception();
1011 }
1012 }
1013 }
1014 }
1015 if (eptr_weight) {
1016 std::rethrow_exception(eptr_weight);
1017 }
1018
1019 Eigen::MatrixXd grad = Eigen::MatrixXd::Zero(natoms, 3);
1020 for (Index t = 0; t < nthreads; ++t) {
1021 grad += grad_thread[t];
1022 }
1023 return grad;
1024}
1025
1026template <class Grid>
1028 const Eigen::MatrixXd& dmat_alpha, const Eigen::MatrixXd& dmat_beta,
1029 const AOBasis& dftbasis) const {
1030 // GGA support: full derivation verified numerically in Python (toy
1031 // multi-atom system, ~1e-12) before writing this -- see conversation
1032 // history. Key simplification found: the sigma-dependent gradient,
1033 // which naively needs THREE separate contributions (from sigma_aa,
1034 // sigma_ab, sigma_bb), collapses into reusing the SAME per-spin
1035 // machinery as the restricted GGA case (Gmat_s, Hessian_rho_s) twice
1036 // -- once per spin channel -- by combining the three vsigma weights
1037 // into two "effective" gradient vectors:
1038 // V_a = 2*vsigma_aa*rho_a_grad + vsigma_ab*rho_b_grad
1039 // V_b = 2*vsigma_bb*rho_b_grad + vsigma_ab*rho_a_grad
1040 // No LDA-only restriction needed anymore.
1041 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
1042 Index nthreads = OPENMP::getMaxThreads();
1043 std::vector<Eigen::MatrixXd> grad_thread(nthreads,
1044 Eigen::MatrixXd::Zero(natoms, 3));
1045
1046 std::exception_ptr eptr_pulay_uks = nullptr;
1047#pragma omp parallel for schedule(guided)
1048 for (Index i = 0; i < grid_.getBoxesSize(); ++i) {
1049 try {
1050 Index thread_id = OPENMP::getThreadId();
1051 const GridBox& box = grid_[i];
1052 if (!box.Matrixsize()) {
1053 continue;
1054 }
1055
1056 const Eigen::MatrixXd DMa = box.ReadFromBigMatrix(dmat_alpha);
1057 const Eigen::MatrixXd DMb = box.ReadFromBigMatrix(dmat_beta);
1058
1059 // Box-local AO index -> atom index -- MUST use getAOranges() (start/
1060 // size per shell), not a naive sequential push_back per shell: a
1061 // single box's significant shells are not guaranteed to be grouped
1062 // contiguously by atom. Matches the exact construction already
1063 // validated in the restricted PulayGradient.
1064 std::vector<Index> local_idx_to_atom(box.Matrixsize());
1065 const std::vector<const AOShell*>& shells = box.getShells();
1066 const std::vector<GridboxRange>& ao_ranges = box.getAOranges();
1067 for (size_t s = 0; s < shells.size(); ++s) {
1068 Index atom = shells[s]->getAtomIndex();
1069 for (Index k = 0; k < ao_ranges[s].size; ++k) {
1070 local_idx_to_atom[ao_ranges[s].start + k] = atom;
1071 }
1072 }
1073 const std::vector<Index>& owner_atoms = box.getOwnerAtoms();
1074
1075 const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
1076 const std::vector<double>& weights = box.getGridWeights();
1077
1078 for (Index p = 0; p < box.size(); ++p) {
1080
1081 Eigen::VectorXd temp_a = DMa * ao.values;
1082 Eigen::VectorXd temp_b = DMb * ao.values;
1083 const double rho_a = ao.values.dot(temp_a);
1084 const double rho_b = ao.values.dot(temp_b);
1085 const double rho = rho_a + rho_b;
1086 const double weight = weights[p];
1087
1088 if (rho * weight < 1.e-20) {
1089 continue;
1090 }
1091
1092 const Eigen::Vector3d rho_a_grad =
1093 2.0 * (ao.derivatives.transpose() * temp_a);
1094 const Eigen::Vector3d rho_b_grad =
1095 2.0 * (ao.derivatives.transpose() * temp_b);
1096 const double sigma_aa = rho_a_grad.dot(rho_a_grad);
1097 const double sigma_ab = rho_a_grad.dot(rho_b_grad);
1098 const double sigma_bb = rho_b_grad.dot(rho_b_grad);
1099
1101 EvaluateXCSpin(rho_a, rho_b, sigma_aa, sigma_ab, sigma_bb);
1102
1103 // Translation-type term (A == owner(p) only), LDA part -- same as
1104 // before, unaffected by GGA.
1105 Index owner_of_point = owner_atoms[p];
1106 grad_thread[thread_id].row(owner_of_point) +=
1107 (weight * (xc.vrho_a * rho_a_grad + xc.vrho_b * rho_b_grad))
1108 .transpose();
1109
1110 // GGA sigma terms -- both basis-type and translation-type reuse
1111 // the same Gmat_s/Hessian_rho_s construction per spin channel
1112 // (matching the restricted GGA case's own Gmat/Hessian_rho
1113 // exactly, just built from dmat_alpha/dmat_beta separately
1114 // instead of one pre-doubled DMAT_here), contracted with V_a/V_b.
1115 Eigen::MatrixX3d Gmat_a = 2.0 * (DMa * ao.derivatives);
1116 Eigen::MatrixX3d Gmat_b = 2.0 * (DMb * ao.derivatives);
1117 Eigen::Vector3d V_a =
1118 2.0 * xc.vsigma_aa * rho_a_grad + xc.vsigma_ab * rho_b_grad;
1119 Eigen::Vector3d V_b =
1120 2.0 * xc.vsigma_bb * rho_b_grad + xc.vsigma_ab * rho_a_grad;
1121
1122 // Translation-type sigma contribution: Hessian_rho_s * V_s,
1123 // summed over both spins.
1124 Eigen::Matrix3d Hessian_rho_a = Eigen::Matrix3d::Zero();
1125 Eigen::Matrix3d Hessian_rho_b = Eigen::Matrix3d::Zero();
1126 for (Index mu = 0; mu < box.Matrixsize(); ++mu) {
1127 Hessian_rho_a += 2.0 * temp_a(mu) * ao.hessians[mu];
1128 Hessian_rho_b += 2.0 * temp_b(mu) * ao.hessians[mu];
1129 }
1130 Eigen::Matrix3d Ma = ao.derivatives.transpose() * Gmat_a;
1131 Eigen::Matrix3d Mb = ao.derivatives.transpose() * Gmat_b;
1132 Hessian_rho_a += 0.5 * (Ma + Ma.transpose());
1133 Hessian_rho_b += 0.5 * (Mb + Mb.transpose());
1134
1135 grad_thread[thread_id].row(owner_of_point) +=
1136 (weight * (Hessian_rho_a * V_a + Hessian_rho_b * V_b)).transpose();
1137
1138 // Basis-type term (LDA + sigma), accumulated per local AO index
1139 // mu, scattered to the correct atom via local_idx_to_atom.
1140 Eigen::VectorXd Gmat_a_dot_Va = Gmat_a * V_a;
1141 Eigen::VectorXd Gmat_b_dot_Vb = Gmat_b * V_b;
1142 for (Index mu = 0; mu < box.Matrixsize(); ++mu) {
1143 Index atom = local_idx_to_atom[mu];
1144 Eigen::Vector3d lda_contribution =
1145 -2.0 * weight *
1146 (xc.vrho_a * temp_a(mu) + xc.vrho_b * temp_b(mu)) *
1147 ao.derivatives.row(mu).transpose();
1148 Eigen::Vector3d sigma_contribution =
1149 -weight *
1150 (ao.derivatives.row(mu).transpose() * Gmat_a_dot_Va(mu) +
1151 2.0 * temp_a(mu) * (ao.hessians[mu] * V_a) +
1152 ao.derivatives.row(mu).transpose() * Gmat_b_dot_Vb(mu) +
1153 2.0 * temp_b(mu) * (ao.hessians[mu] * V_b));
1154 grad_thread[thread_id].row(atom) +=
1155 (lda_contribution + sigma_contribution).transpose();
1156 }
1157 }
1158 } catch (...) {
1159#pragma omp critical
1160 {
1161 if (!eptr_pulay_uks) {
1162 eptr_pulay_uks = std::current_exception();
1163 }
1164 }
1165 }
1166 }
1167 if (eptr_pulay_uks) {
1168 std::rethrow_exception(eptr_pulay_uks);
1169 }
1170
1171 Eigen::MatrixXd grad = Eigen::MatrixXd::Zero(natoms, 3);
1172 for (Index t = 0; t < nthreads; ++t) {
1173 grad += grad_thread[t];
1174 }
1175 return grad;
1176}
1177
1178template <class Grid>
1180 const Eigen::MatrixXd& dmat_alpha, const Eigen::MatrixXd& dmat_beta,
1181 const QMMolecule& atoms) const {
1182 // GGA support: the weight-derivative term is functional-form-agnostic
1183 // (unchanged geometric dw/dR logic below) -- only needed real
1184 // sigma_aa/sigma_ab/sigma_bb instead of hardcoded zeros, computed
1185 // from the actual density gradients below. No LDA-only restriction
1186 // needed here at all.
1187 // Copy-adapted from GridWeightGradient (restricted case) -- the SSW
1188 // weight-derivative logic itself (d_rq_dR, d_Rab_dR, dmu_dR, dp_dR,
1189 // and the C_p/w_owner construction) is PURELY GEOMETRIC and
1190 // functional-form-agnostic, unchanged from that already-validated
1191 // implementation (which took many rounds of debugging to get right,
1192 // including the C_p fix -- see git history). Only the PREFACTOR
1193 // changes here: rho_a, rho_b computed unpaired (no 2x/0.5x factor,
1194 // matching IntegrateVXCSpin's own convention exactly, since UKS spin
1195 // density matrices are not pre-doubled), and xc.f_xc from
1196 // EvaluateXCSpin (the spin-polarized functional value) instead of
1197 // EvaluateXC.
1198 Index natoms = atoms.size();
1199 Eigen::MatrixXd Rij = grid_.CalcInverseAtomDist(atoms);
1200
1201 Index nthreads = OPENMP::getMaxThreads();
1202 std::vector<Eigen::MatrixXd> grad_thread(nthreads,
1203 Eigen::MatrixXd::Zero(natoms, 3));
1204
1205 std::exception_ptr eptr_weight_uks = nullptr;
1206#pragma omp parallel for schedule(guided)
1207 for (Index i = 0; i < grid_.getBoxesSize(); ++i) {
1208 try {
1209 Index thread_id = OPENMP::getThreadId();
1210 const GridBox& box = grid_[i];
1211 if (!box.Matrixsize()) {
1212 continue;
1213 }
1214
1215 const Eigen::MatrixXd DMa = box.ReadFromBigMatrix(dmat_alpha);
1216 const Eigen::MatrixXd DMb = box.ReadFromBigMatrix(dmat_beta);
1217 double cutoff =
1218 1.e-40 / double(dmat_alpha.rows()) / double(dmat_alpha.rows());
1219 if (std::max(DMa.cwiseAbs2().maxCoeff(), DMb.cwiseAbs2().maxCoeff()) <
1220 cutoff) {
1221 continue;
1222 }
1223
1224 const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
1225 const std::vector<double>& weights = box.getGridWeights();
1226 const std::vector<Index>& owner_atoms = box.getOwnerAtoms();
1227
1228 for (Index pidx = 0; pidx < box.size(); ++pidx) {
1229 AOShell::AOValues ao = box.CalcAOValues(points[pidx]);
1230 Eigen::VectorXd temp_a = DMa * ao.values;
1231 Eigen::VectorXd temp_b = DMb * ao.values;
1232 const double rho_a = ao.values.dot(temp_a);
1233 const double rho_b = ao.values.dot(temp_b);
1234 const double rho = rho_a + rho_b;
1235 double weight = weights[pidx];
1236 if (rho * weight < 1.e-20) {
1237 continue;
1238 }
1239 const Eigen::Vector3d rho_a_grad =
1240 2.0 * (ao.derivatives.transpose() * temp_a);
1241 const Eigen::Vector3d rho_b_grad =
1242 2.0 * (ao.derivatives.transpose() * temp_b);
1243 const double sigma_aa = rho_a_grad.dot(rho_a_grad);
1244 const double sigma_ab = rho_a_grad.dot(rho_b_grad);
1245 const double sigma_bb = rho_b_grad.dot(rho_b_grad);
1247 EvaluateXCSpin(rho_a, rho_b, sigma_aa, sigma_ab, sigma_bb);
1248
1249 Index owner = owner_atoms[pidx];
1250 if (owner < 0) {
1251 throw std::runtime_error(
1252 "GridWeightGradientUKS: grid point has no owner_atom set -- "
1253 "was this grid built via GridSetup after the owner-atom "
1254 "tracking change?");
1255 }
1256
1257 const Eigen::Vector3d& point = points[pidx];
1258 Eigen::VectorXd rq(natoms);
1259 for (Index k = 0; k < natoms; ++k) {
1260 rq(k) = (point - atoms[k].getPos()).norm();
1261 }
1262
1263 Eigen::VectorXd p = Eigen::VectorXd::Ones(natoms);
1264 Eigen::MatrixXd mu_table = Eigen::MatrixXd::Zero(natoms, natoms);
1265 Eigen::MatrixXd sk_table = Eigen::MatrixXd::Zero(natoms, natoms);
1266 Eigen::MatrixXi hard = Eigen::MatrixXi::Zero(natoms, natoms);
1267 for (Index ii = 1; ii < natoms; ++ii) {
1268 for (Index jj = 0; jj < ii; ++jj) {
1269 double mu = (rq(ii) - rq(jj)) * Rij(jj, ii);
1270 mu_table(jj, ii) = mu;
1271 if (mu > kSSWCutoff) {
1272 p(ii) = 0.0;
1273 hard(jj, ii) = 1;
1274 } else if (mu < -kSSWCutoff) {
1275 p(jj) = 0.0;
1276 hard(jj, ii) = -1;
1277 } else {
1278 double sk = SSWValue(mu);
1279 sk_table(jj, ii) = sk;
1280 p(jj) *= sk;
1281 p(ii) *= (1.0 - sk);
1282 }
1283 }
1284 }
1285 double wsum = p.sum();
1286 double w_owner = p(owner) / wsum;
1287
1288 auto d_rq_dR = [&](Index k, Index A) -> Eigen::Vector3d {
1289 if (A == owner && A == k) {
1290 return Eigen::Vector3d::Zero();
1291 } else if (A == owner) {
1292 return (point - atoms[k].getPos()) / rq(k);
1293 } else if (A == k) {
1294 return -(point - atoms[k].getPos()) / rq(k);
1295 }
1296 return Eigen::Vector3d::Zero();
1297 };
1298 auto d_Rab_dR = [&](Index a, Index b, Index A) -> Eigen::Vector3d {
1299 Eigen::Vector3d rvec = atoms[a].getPos() - atoms[b].getPos();
1300 double Rab = rvec.norm();
1301 if (A == a) {
1302 return rvec / Rab;
1303 } else if (A == b) {
1304 return -rvec / Rab;
1305 }
1306 return Eigen::Vector3d::Zero();
1307 };
1308 auto dmu_dR = [&](Index a, Index b, Index A) -> Eigen::Vector3d {
1309 Eigen::Vector3d d_rq_b = d_rq_dR(b, A);
1310 Eigen::Vector3d d_rq_a = d_rq_dR(a, A);
1311 double Rab = 1.0 / Rij(a, b);
1312 Eigen::Vector3d dRab = d_Rab_dR(a, b, A);
1313 double mu = mu_table(a, b);
1314 return (d_rq_b - d_rq_a) / Rab - (mu / Rab) * dRab;
1315 };
1316 auto dp_dR = [&](Index k, Index A) -> Eigen::Vector3d {
1317 constexpr double kNegligibleP = 1.e-8;
1318 if (p(k) < kNegligibleP) {
1319 return Eigen::Vector3d::Zero();
1320 }
1321 Eigen::Vector3d total = Eigen::Vector3d::Zero();
1322 for (Index b = k + 1; b < natoms; ++b) {
1323 if (hard(k, b) != 0) {
1324 continue;
1325 }
1326 double skv = sk_table(k, b);
1327 if (skv < kNegligibleP) {
1328 continue;
1329 }
1330 total += (SSWDerivative(mu_table(k, b)) / skv) * dmu_dR(k, b, A);
1331 }
1332 for (Index a = 0; a < k; ++a) {
1333 if (hard(a, k) != 0) {
1334 continue;
1335 }
1336 double skv = sk_table(a, k);
1337 double one_minus_skv = 1.0 - skv;
1338 if (one_minus_skv < kNegligibleP) {
1339 continue;
1340 }
1341 total += (-SSWDerivative(mu_table(a, k)) / one_minus_skv) *
1342 dmu_dR(a, k, A);
1343 }
1344 return p(k) * total;
1345 };
1346
1347 constexpr double kNegligibleWOwner = 1.e-8;
1348 if (w_owner < kNegligibleWOwner) {
1349 continue;
1350 }
1351 double C_p = weight / w_owner;
1352 double prefactor = C_p * rho * xc.f_xc;
1353
1354 for (Index A = 0; A < natoms; ++A) {
1355 Eigen::Vector3d dp_owner = dp_dR(owner, A);
1356 Eigen::Vector3d dwsum = Eigen::Vector3d::Zero();
1357 for (Index k = 0; k < natoms; ++k) {
1358 dwsum += dp_dR(k, A);
1359 }
1360 Eigen::Vector3d dw = dp_owner / wsum - w_owner * dwsum / wsum;
1361 Eigen::Vector3d contribution = prefactor * dw;
1362 grad_thread[thread_id].row(A) += contribution.transpose();
1363 }
1364 }
1365 } catch (...) {
1366#pragma omp critical
1367 {
1368 if (!eptr_weight_uks) {
1369 eptr_weight_uks = std::current_exception();
1370 }
1371 }
1372 }
1373 }
1374 if (eptr_weight_uks) {
1375 std::rethrow_exception(eptr_weight_uks);
1376 }
1377
1378 Eigen::MatrixXd grad = Eigen::MatrixXd::Zero(natoms, 3);
1379 for (Index t = 0; t < nthreads; ++t) {
1380 grad += grad_thread[t];
1381 }
1382 return grad;
1383}
1384
1385template class Vxc_Potential<Vxc_Grid>;
1386
1387} // namespace xtp
1388} // namespace votca
break string into words
Definition tokenizer.h:72
std::vector< T > ToVector()
store all words in a vector of type T, does type conversion.
Definition tokenizer.h:109
Container to hold Basisfunctions for all atoms.
Definition aobasis.h:42
const std::vector< Index > & getFuncPerAtom() const
Definition aobasis.h:72
const Eigen::Vector3d & getPos() const
const std::vector< Eigen::Vector3d > & getGridPoints() const
Definition gridbox.h:55
Eigen::MatrixXd ReadFromBigMatrix(const Eigen::MatrixXd &bigmatrix) const
Definition gridbox.cc:97
void AddtoBigMatrix(Eigen::MatrixXd &bigmatrix, const Eigen::MatrixXd &smallmatrix) const
Definition gridbox.cc:84
const std::vector< const AOShell * > & getShells() const
Definition gridbox.h:65
const std::vector< GridboxRange > & getAOranges() const
Definition gridbox.h:69
const std::vector< double > & getGridWeights() const
Definition gridbox.h:57
const std::vector< Index > & getOwnerAtoms() const
Definition gridbox.h:63
AOShell::AOValuesHessian CalcAOValuesHessian(const Eigen::Vector3d &point) const
Definition gridbox.cc:68
AOShell::AOValues CalcAOValues(const Eigen::Vector3d &point) const
Definition gridbox.cc:44
Index size() const
Definition gridbox.h:71
Index Matrixsize() const
Definition gridbox.h:75
Eigen::MatrixXd & matrix()
Definition eigen.h:97
double & energy()
Definition eigen.h:98
conversion of functional string into integer
static constexpr Index kPointBlock
void EvaluateXCSpinBatch(Index n, const double *rho, const double *sigma, double *exc, double *vrho, double *vsigma) const
Eigen::MatrixXd GridWeightGradient(const Eigen::MatrixXd &density_matrix, const QMMolecule &atoms) const
Eigen::MatrixXd PulayGradient(const Eigen::MatrixXd &density_matrix, const AOBasis &dftbasis) const
XC_entry_spin EvaluateXCSpin(double rho_a, double rho_b, double sigma_aa, double sigma_ab, double sigma_bb) const
void EvaluateXCBatch(Index n, const double *rho, const double *sigma, double *exc, double *vrho, double *vsigma) const
Eigen::MatrixXd PulayGradientUKS(const Eigen::MatrixXd &dmat_alpha, const Eigen::MatrixXd &dmat_beta, const AOBasis &dftbasis) const
XC_entry EvaluateXC(double rho, double sigma) 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
Index getMaxThreads()
Definition eigen.h:145
Index getThreadId()
Definition eigen.h:160
Charge transport classes.
Definition ERIs.h:28
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
std::vector< Eigen::Matrix3d > hessians
Definition aoshell.h:180
Eigen::MatrixX3d derivatives
Definition aoshell.h:131
double vsigma_ab
double vrho_b
double vrho_a
double vsigma_aa
double vsigma_bb
double f_xc
double f_xc
double df_dsigma
double df_drho