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