votca 2026-dev
Loading...
Searching...
No Matches
libint2_derivative_calls.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2024 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// ===========================================================================
21// STATUS UPDATE (later, via a real pyxtp run, not a unit test): a second,
22// independent, more serious bug was found AFTER everything below was
23// believed validated. Every buffer-mapping test in this file (and every
24// finite-difference test built on top of it, throughout the whole
25// plain-DFT-forces branch) used H2 with a minimal, all-s-shell basis
26// (3-21G). libint2 returns each shell-pair derivative buffer in
27// ROW-MAJOR order, but every buffer mapping below used plain
28// Eigen::Map<const Eigen::MatrixXd> (Eigen's default COLUMN-MAJOR) --
29// identical to row-major for a 1x1 (pure s-shell) block, silently wrong
30// for any block with more than one function per shell (p/d/f angular
31// momentum). Invisible for H2/3-21G; a large (~2-4%), real rotational-
32// covariance violation for a genuine CO/def2-tzvp calculation (both
33// atoms have p and d shells) -- found by comparing the same molecule in
34// two orientations term-by-term, not by any test in this codebase.
35// Fixed by switching to MatrixLibInt (Eigen::RowMajor), matching the
36// already-correct, already-validated convention libint2_calls.cc uses
37// for the energy-level (deriv_order=0) integrals this file's functions
38// are the derivatives of. See the MatrixLibInt definition below for the
39// full explanation. NOT yet re-confirmed by a real rerun as of this
40// comment -- the fix is reasoned from a clear, specific mechanism (not
41// a guess), but treat the "strongly validated" claims elsewhere in this
42// file's history below with real skepticism: they were true only for
43// all-s-shell systems, which is a much narrower claim than it reads.
44// ===========================================================================
45//
46// STATUS (ORIGINAL, PRE-DATING THE BUG ABOVE): derivative-integral code
47// below is now strongly validated -- FOR H2/3-21G SPECIFICALLY, not in
48// general; see the update above.
49//
50// Full history: build-level root cause (libint2 built without derivative
51// support -- eventually traced to Homebrew resolving formulae from its
52// hosted API rather than the locally-edited tap file, requiring
53// HOMEBREW_NO_INSTALL_FROM_API=1 to actually take effect) is fixed.
54// libint2::Engine confirmed (via Psi4's own integral-programming docs) to
55// provide all centers' derivatives explicitly, not via translational
56// invariance -- code reads all 6 buffers directly, matching the ORIGINAL
57// (v1) assumption; the diagnostic print confirms this directly at runtime
58// (buf.size()=6, all six non-null).
59//
60// The finite-difference test then showed a discrepancy that turned out to
61// be a bug in the TEST, not this file: BuildH2() displaces atoms in
62// Angstrom, but XTP represents geometry internally in Bohr, so the
63// finite-difference reference was dS/dR per-Angstrom while the analytic
64// result here is dS/dR per-Bohr. Every nonzero entry of the two matrices
65// differed by the exact same factor (~1.8897 = 1/0.529177, the
66// Bohr-to-Angstrom conversion) -- a uniform multiplicative discrepancy
67// across all entries is the signature of a unit mismatch, not a
68// structural bug (wrong buffer/index/sign would not produce one uniform
69// ratio). After converting the finite-difference reference to per-Bohr
70// units in test_aoderivatives.cc, the two agree to ~1 part in 40000,
71// consistent with expected O(h^2) truncation error at h=1e-4.
72//
73// Also independently consistent throughout: the translational-invariance
74// symmetry check (deriv[0][2] == -deriv[1][2]) passed even before the
75// units bug was found, since that check never involves finite differences
76// -- it is a real, unit-independent confirmation that the analytic code
77// satisfies the exact mathematical constraint it must. NOTE: this check
78// is ALSO blind to the row-major/column-major bug above for an all-s-shell
79// system -- d[0][2]==-d[1][2] holds regardless of storage order for 1x1
80// blocks, so it could not have caught this either.
81// ===========================================================================
82//
83// Build/run history on this file:
84// - Originally drafted with no local libint2 install available, so it
85// could not be compiled or run at all when first written.
86// - First real build attempt failed at link time (duplicate symbols for
87// libint2's internal static tables) -- fixed by removing a duplicate
88// inclusion of <libint2/statics_definition.h>, which must appear in
89// exactly one translation unit per library.
90// - First real run attempt crashed (null pointer, address 0x0) at test
91// entry, single-threaded. Multi-threaded, the same underlying issue
92// showed up as a repeating C++ exception across worker threads rather
93// than a clean crash, which is a separate, real defect in its own
94// right: the parallel loop below never wrapped its body in a
95// try/catch, so an exception escaping an OpenMP worker thread is
96// undefined behavior. That is not yet fixed either -- still worth
97// adding once the buffer-count question is settled, so a real error
98// in production use terminates cleanly instead of hanging.
99//
100// CURRENT HYPOTHESIS (revised from the original 6-buffer assumption):
101// For a two-center one-electron integral, libint2 most likely returns
102// only 3 derivative buffers (d/dx,dy,dz with respect to shell1's atom),
103// not 6, exploiting the translational-invariance sum rule
104// d(integral)/dR_atom1 + d(integral)/dR_atom2 = 0 (valid because this
105// integral depends only on the relative position of the two atoms) to
106// avoid computing shell2's derivative explicitly. The original
107// assert(buf.size() == 6) did not catch this, most likely because
108// target_ptr_vec's size() reports a fixed nominal capacity rather than
109// the number of meaningfully-populated buffers -- so an out-of-bounds
110// read at buf[3+xyz] returned a null pointer rather than tripping a
111// bounds check. The code below has been revised to derive shell2's
112// derivative as the negative of shell1's, per shell pair, rather than
113// reading buf[3+xyz] at all. A diagnostic print has been left in to
114// directly confirm the real buf.size() and which entries are non-null
115// on the next run -- please check that output before trusting this fix,
116// and remove the diagnostic once confirmed either way.
117// ===========================================================================
118
119// Local VOTCA includes
120#include "votca/xtp/aobasis.h"
121#include "votca/xtp/aomatrix.h"
123#include "votca/xtp/qmmolecule.h"
125#include <atomic>
126#include <exception>
127#include <iostream>
128#include <stdexcept>
129#include <string>
130
131// include libint last otherwise it overrides eigen
133#define LIBINT2_CONSTEXPR_STATICS 0
134#if defined(__clang__)
135#pragma clang diagnostic push
136#pragma clang diagnostic ignored "-W#warnings"
137#elif defined(__GNUC__)
138#pragma GCC diagnostic push
139#pragma GCC diagnostic ignored "-Warray-bounds"
140#pragma GCC diagnostic ignored "-Wcpp"
141#endif
142#include <libint2.hpp>
143// NOTE: deliberately NOT including <libint2/statics_definition.h> here.
144// That header *defines* storage for libint2's internal static tables
145// (e.g. FmEval_Chebyshev7<double>::cheb_table) and must appear in exactly
146// one translation unit per library -- libint2_calls.cc already provides
147// it for the whole votca_xtp library. Including it here too caused a
148// duplicate-symbol link error (caught when actually building against a
149// real libint2 install, which wasn't possible in the environment this
150// file was originally drafted in -- see file status note above).
151#if defined(__clang__)
152#pragma clang diagnostic pop
153#elif defined(__GNUC__)
154#pragma GCC diagnostic pop
155#endif
156
157namespace votca {
158namespace xtp {
159
160// Defined in libint2_calls.cc, not declared in any header; forward
161// declared here for use by ComputeThreeCenterIntegrals below, to avoid
162// re-deriving the same shell-iteration logic a third time.
163std::vector<Eigen::MatrixXd> ComputeAO3cBlock(const libint2::Shell& auxshell,
164 const AOBasis& dftbasis,
165 libint2::Engine& engine);
166
167// Result type: one entry per atom, each holding the three Cartesian
168// derivative matrices (d/dx, d/dy, d/dz) of the full AO matrix with respect
169// to that atom's nuclear coordinate. Matches the AO matrix convention used
170// elsewhere in this file (dense Eigen::MatrixXd, size AOBasisSize^2).
171using AOMatrixDerivative = std::array<Eigen::MatrixXd, 3>;
172
173// BUG FOUND (real pyxtp run on CO, def2-tzvp -- see conversation history
174// for the full diagnostic trail): libint2 returns each shell-pair
175// derivative buffer in ROW-MAJOR order (n1 rows x n2 columns, matching
176// libint2_calls.cc's own already-validated MatrixLibInt convention used
177// throughout the energy-level, deriv_order=0 integral code). Every
178// function below originally mapped these buffers with plain
179// Eigen::Map<const Eigen::MatrixXd>, which defaults to Eigen's
180// COLUMN-MAJOR storage order -- silently transposing each block within
181// a shell pair whenever either shell has more than one function (any
182// p/d/f angular momentum, not pure s-type). This was invisible in
183// EVERY finite-difference test in this branch, all of which used H2
184// with a minimal, all-s-shell basis (3-21G) -- row-major and
185// column-major are identical for a 1x1 block. It showed up as a
186// genuine, large (~2-4%) rotational-covariance violation for a real
187// CO/def2-tzvp calculation (both O and C have p and d shells).
188using MatrixLibInt =
189 Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
190
191namespace {
192// Shared by every derivative-computing function's own compile-guard
193// stub below (see each guard's own comment for why it exists and which
194// specific libint2 configure flag is relevant to it -- overlap/
195// kinetic/nuclear attraction, the Coulomb metric, and three-center RI
196// each have their OWN, independent flag, confirmed via libint2's own
197// engine.impl.h, which validates braket_ against LIBINT_INCLUDE_ONEBODY/
198// _ERI2/_ERI3 completely independently of one another). Deliberately
199// defined here, unconditionally (not inside any #if guard), since it
200// does not itself depend on any of those macros -- only the stubs that
201// call it do.
202// [[maybe_unused]]: on a build where all three categories (ONEBODY,
203// ERI2, ERI3) are actually supported, none of the #else stub blocks
204// below that call this are ever compiled -- making this function
205// itself genuinely unused. Confirmed as a real -Werror -Wunused-function
206// build failure on a CI runner with full derivative support (clang,
207// this exact function flagged). Defined unconditionally (not inside
208// any #if guard) specifically so it can be shared across every stub
209// block regardless of which category(ies) are missing; that same
210// choice is what makes it possible for none of them to be active at
211// once.
212[[noreturn, maybe_unused]] void ThrowNoDerivativeSupport(
213 const char* function_name, const char* configure_flag) {
214 throw std::runtime_error(
215 std::string(function_name) +
216 ": this libint2 build was compiled without derivative integral "
217 "support for this specific operator category -- analytic nuclear "
218 "forces using this function are unavailable. Rebuild libint2 "
219 "with this configured (" +
220 configure_flag +
221 " at libint2 configure time) to use this feature. Many "
222 "pre-packaged libint2 builds (Homebrew, Ubuntu apt, etc.) do not "
223 "enable this by default; note in particular that different "
224 "operator categories (one-body, the two-center Coulomb metric, "
225 "three-center RI) each have their own, independent flag -- "
226 "enabling one does not enable the others.");
227}
228} // namespace
229
230// Computes nuclear-coordinate derivatives of a two-center one-electron
231// integral matrix (overlap, kinetic) via libint2's deriv_order=1 engine.
232// obtype must be a two-center operator (libint2::Operator::overlap or
233// libint2::Operator::kinetic); this function has not been checked against
234// operators with a params argument (e.g. emultipole) or against operators
235// with more than two centers (e.g. coulomb/three-center RI integrals) --
236// those need separate handling for the additional center(s)' derivatives
237// and are deliberately out of scope for this first pass.
238//
239// COMPILE GUARD: many pre-packaged libint2 builds (Homebrew, Ubuntu
240// apt, etc.) are NOT built with derivative support at all -- this
241// branch's own build hit exactly this issue early on (traced to
242// Homebrew resolving formulae from its hosted API rather than a
243// locally-edited tap file; see the file-level history comment above).
244// libint2 exposes its own build-time maximum derivative order as the
245// LIBINT2_MAX_DERIV_ORDER macro (confirmed directly: libint2's own
246// Engine::initialize asserts deriv_order_ <= LIBINT2_MAX_DERIV_ORDER).
247//
248// THIS ALONE WAS NOT ENOUGH: real CI runs showed LIBINT2_MAX_DERIV_ORDER
249// can report >=1 (library-wide -- the maximum ANY operator supports)
250// while individual, independently-configured operator categories are
251// disabled or broken -- either silently returning zero-filled buffers,
252// or segfaulting outright inside engine.compute() itself (a crash that
253// cannot be caught by any try/catch, since it happens before control
254// ever returns to this file's own code, no matter how carefully the
255// returned buffers are checked afterward).
256//
257// An earlier version of this guard tried to catch this by actually
258// compiling and running a small test program at CMake configure time
259// (a try_run check). That approach was abandoned -- fragile in
260// practice (needed multiple rounds of fixes for the check program's
261// own bugs, including once for a missing Eigen include path) and,
262// more importantly, unnecessary: libint2's OWN engine.impl.h already
263// validates braket_ against per-category macros internally
264// (LIBINT_INCLUDE_ONEBODY, LIBINT_INCLUDE_ERI2, LIBINT_INCLUDE_ERI3),
265// each set independently at libint2's OWN configure time
266// (LIBINT2_ENABLE_ONEBODY / _ERI2 / _ERI3, each with its own N or -1
267// for "off" -- confirmed via libint2's own INSTALL.md and
268// engine.impl.h).
269//
270// CRITICAL, confirmed via a real crash: these macros are NOT bare
271// on/off flags -- they are defined WITH THE ACTUAL CONFIGURED
272// DERIVATIVE ORDER as their value (confirmed directly: libint2's own
273// test suite uses "#if INCLUDE_ERI2 >= 1" and
274// "if (deriv_order > INCLUDE_ERI2) return true;", checking the VALUE,
275// not just definedness). LIBINT2_ENABLE_ONEBODY defaults to 0 (not
276// -1/off) if never explicitly set -- meaning one-body integrals ARE
277// built, but only at deriv_order=0 (energies only, no first
278// derivatives) -- exactly the situation the standard Homebrew libint
279// formula leaves onebody in, since it only ever sets
280// -DLIBINT2_ENABLE_ERI2=1/_ERI3=1/_ERI=1, never _ONEBODY. In that
281// configuration LIBINT_INCLUDE_ONEBODY is defined AS 0, so
282// "defined(LIBINT_INCLUDE_ONEBODY)" alone is TRUE (wrongly implying
283// support) while the correct check, "LIBINT_INCLUDE_ONEBODY >= 1", is
284// FALSE. The former passed this exact guard once, wrongly, on a real
285// CI run -- letting the real, deriv_order=1 implementation compile in
286// and then hit a null function-pointer assertion deep inside
287// libint2's own dispatch table at runtime (a hard SIGABRT, not a C++
288// exception -- uncatchable, exactly like the null-buffer segfault
289// this guard was originally added to prevent). Checking the value,
290// not just definedness, is what actually distinguishes "not
291// configured at all" from "configured, but only for energies."
292//
293// computeOneBodyIntegralDerivatives (and ComputeNuclearAttractionDerivatives
294// further below, which uses the same two-center compute() API) needs
295// LIBINT_INCLUDE_ONEBODY; ComputeCoulombMetricDerivatives needs
296// LIBINT_INCLUDE_ERI2; ComputeThreeCenterDerivatives needs
297// LIBINT_INCLUDE_ERI3 -- three genuinely independent flags, any one of
298// which can be enabled while the others are not (e.g. Psi4's own
299// default configuration leaves ERI2/ERI3 off while ONEBODY is on; the
300// standard Homebrew libint formula is the mirror image -- ERI2/ERI3/ERI
301// on, ONEBODY at its default-0/energies-only state).
302#if defined(LIBINT2_MAX_DERIV_ORDER) && LIBINT2_MAX_DERIV_ORDER >= 1 && \
303 (LIBINT_INCLUDE_ONEBODY >= 1)
304template <libint2::Operator obtype>
305std::vector<AOMatrixDerivative> computeOneBodyIntegralDerivatives(
306 const AOBasis& aobasis) {
307
308 static_assert(
309 libint2::operator_traits<obtype>::nopers == 1,
310 "computeOneBodyIntegralDerivatives only supports single-operator "
311 "types (overlap, kinetic) in this first pass; multipole-type "
312 "operators with multiple result matrices are not yet handled here.");
313
314 // AOBasis has no direct getNumofAtoms(); getFuncPerAtom() is sized by
315 // number of atoms and IS a confirmed real accessor (checked directly
316 // against aobasis.h), so its size is used here instead.
317 Index natoms = static_cast<Index>(aobasis.getFuncPerAtom().size());
318 Index nthreads = OPENMP::getMaxThreads();
319 std::vector<libint2::Shell> shells = aobasis.GenerateLibintBasis();
320 std::vector<std::vector<Index>> shellpair_list = aobasis.ComputeShellPairs();
321 std::vector<Index> shell2bf = aobasis.getMapToBasisFunctions();
322
323 // shell -> atom index, needed to know which atom's derivative each
324 // returned buffer belongs to.
325 std::vector<Index> shell2atom;
326 shell2atom.reserve(shells.size());
327 for (Index s = 0; s < aobasis.getNumofShells(); ++s) {
328 // AOShell::getAtomIndex() and AOBasis::getShell() both confirmed
329 // directly against the real headers (aoshell.h, aobasis.h).
330 shell2atom.push_back(aobasis.getShell(s).getAtomIndex());
331 }
332
333 Index nbf = aobasis.AOBasisSize();
334 std::vector<AOMatrixDerivative> result(natoms);
335 for (Index a = 0; a < natoms; ++a) {
336 for (Index xyz = 0; xyz < 3; ++xyz) {
337 result[a][xyz] = Eigen::MatrixXd::Zero(nbf, nbf);
338 }
339 }
340
341 std::vector<libint2::Engine> engines(nthreads);
342 // deriv_order = 1 instead of 0 is the one substantive change relative to
343 // computeOneBodyIntegrals(); everything else follows the same pattern.
344 engines[0] = libint2::Engine(obtype, aobasis.getMaxNprim(),
345 static_cast<int>(aobasis.getMaxL()), 1);
346 for (Index i = 1; i < nthreads; ++i) {
347 engines[i] = engines[0];
348 }
349
350 std::exception_ptr eptr = nullptr;
351 std::atomic<bool> any_nonnull_buffer{false};
352#pragma omp parallel for schedule(dynamic)
353 for (Index s1 = 0; s1 < aobasis.getNumofShells(); ++s1) {
354 try {
355 Index thread_id = OPENMP::getThreadId();
356 libint2::Engine& engine = engines[thread_id];
357 const libint2::Engine::target_ptr_vec& buf = engine.results();
358
359 Index bf1 = shell2bf[s1];
360 Index n1 = shells[s1].size();
361 Index atom1 = shell2atom[s1];
362
363 for (Index s2 : shellpair_list[s1]) {
364
365 engine.compute(shells[s1], shells[s2]);
366 // CRITICAL FIX: buf[0]==nullptr alone is not a sufficient guard.
367 // A real, hard crash (SIGSEGV, "no mapping at fault address 0x0")
368 // was observed on a different CI architecture than the one that
369 // showed the "silently all zero" failure mode -- consistent with
370 // buf[0] coming back non-null (passing this check) while
371 // buf[3+xyz] (dereferenced below, unconditionally, for shell2's/
372 // atom2's derivative) is still null. This exact possibility was
373 // flagged in this file's own history from early in this branch
374 // ("buf[3..5] being unpopulated even though buf.size() reports
375 // 6") but the check was never actually added until now. A
376 // segfault is a hardware signal, not a C++ exception -- no
377 // try/catch anywhere can catch it, so this must be prevented
378 // before the dereference, not handled after.
379 if (buf[0] == nullptr || buf[3] == nullptr) {
380 continue; // integrals screened out, or this operator's
381 // derivative support is genuinely absent (caught
382 // below by any_nonnull_buffer/the zero-result check
383 // once every shell pair has been skipped this way).
384 }
385 any_nonnull_buffer.store(true, std::memory_order_relaxed);
386
387 // See HIGHEST-RISK ASSUMPTION note at the top of this file -- the
388 // original assert(buf.size()==6) here did not catch a real problem:
389 // a null-pointer crash (address 0x0) was observed at runtime,
390 // consistent with buf[3..5] being unpopulated even though buf.size()
391 // reports 6 (likely a fixed nominal capacity rather than the number
392 // of meaningfully-populated buffers). Diagnostic left in place below
393 // to confirm this directly on the next run rather than guess again.
394 Index bf2 = shell2bf[s2];
395 Index n2 = shells[s2].size();
396 Index atom2 = shell2atom[s2];
397
398 // CORRECTED (see file header): libint2::Engine provides all centers'
399 // derivatives explicitly -- confirmed against Psi4's own
400 // integral-programming documentation, which contrasts this directly
401 // against an older, legacy one-electron implementation that DID rely
402 // on translational invariance to omit one center. Reading buf[3+xyz]
403 // directly (not deriving it via negation) is the corrected approach.
404 for (Index xyz = 0; xyz < 3; ++xyz) {
405 Eigen::Map<const MatrixLibInt> buf_mat1(buf[xyz], n1, n2);
406 result[atom1][xyz].block(bf1, bf2, n1, n2) += buf_mat1;
407 if (s1 != s2) {
408 result[atom1][xyz].block(bf2, bf1, n2, n1) += buf_mat1.transpose();
409 }
410
411 Eigen::Map<const MatrixLibInt> buf_mat2(buf[3 + xyz], n1, n2);
412 result[atom2][xyz].block(bf1, bf2, n1, n2) += buf_mat2;
413 if (s1 != s2) {
414 result[atom2][xyz].block(bf2, bf1, n2, n1) += buf_mat2.transpose();
415 }
416 }
417 }
418 } catch (...) {
419 // An exception escaping an OpenMP worker thread uncaught is
420 // undefined behavior -- observed in this exact function's own
421 // early history (see file header) to manifest as a hang/repeating
422 // exception across worker threads rather than a clean crash, not
423 // just theoretically risky. Capture it here, inside the thread,
424 // and rethrow once, safely, in the sequential context after the
425 // parallel region ends.
426#pragma omp critical
427 {
428 if (!eptr) {
429 eptr = std::current_exception();
430 }
431 }
432 }
433 }
434 if (eptr) {
435 std::rethrow_exception(eptr);
436 }
437 if (!any_nonnull_buffer.load() && aobasis.getNumofShells() > 0) {
438 // See the detailed explanation on ComputeNuclearAttractionDerivatives'
439 // own version of this check -- distinct from the compile-time
440 // LIBINT2_MAX_DERIV_ORDER guard, since individual operators can each
441 // have their own build configuration; this specific operator
442 // (obtype -- overlap or kinetic, depending on which public entry
443 // point called this template) never produced a single non-null
444 // buffer across the entire computation.
445 throw std::runtime_error(
446 "computeOneBodyIntegralDerivatives: engine.results() returned a "
447 "null buffer for EVERY shell pair -- this libint2 build does not "
448 "actually support this operator's derivative integrals at "
449 "runtime, even though it may report LIBINT2_MAX_DERIV_ORDER >= 1 "
450 "for other operators. Rebuild libint2 with this operator's "
451 "derivative support enabled (--enable-1body=1) to use this "
452 "feature.");
453 }
454 return result;
455}
456
457// Public entry points, mirroring AOOverlap::Fill / AOKinetic::Fill naming.
458// Deliberately free functions rather than new methods on AOOverlap/AOKinetic
459// for this first pass, to avoid touching those classes' existing (working,
460// deriv_order=0) code paths at all.
461std::vector<AOMatrixDerivative> ComputeOverlapDerivatives(
462 const AOBasis& aobasis) {
463 return computeOneBodyIntegralDerivatives<libint2::Operator::overlap>(aobasis);
464}
465
466std::vector<AOMatrixDerivative> ComputeKineticDerivatives(
467 const AOBasis& aobasis) {
468 return computeOneBodyIntegralDerivatives<libint2::Operator::kinetic>(aobasis);
469}
470#else // !(LIBINT_INCLUDE_ONEBODY)
471// See the detailed compile-guard explanation above
472// computeOneBodyIntegralDerivatives. This stub covers ONLY overlap and
473// kinetic here -- ComputeCoulombMetricDerivatives (ERI2) and
474// ComputeThreeCenterDerivatives (ERI3) are each guarded separately
475// below, against their OWN, independent libint2 configure-time flags,
476// since these are genuinely separate capabilities that can be enabled
477// or disabled independently of each other and of LIBINT_INCLUDE_ONEBODY.
478std::vector<AOMatrixDerivative> ComputeOverlapDerivatives(const AOBasis&) {
479 ThrowNoDerivativeSupport("ComputeOverlapDerivatives", "--enable-1body=1");
480}
481std::vector<AOMatrixDerivative> ComputeKineticDerivatives(const AOBasis&) {
482 ThrowNoDerivativeSupport("ComputeKineticDerivatives", "--enable-1body=1");
483}
484#endif // LIBINT_INCLUDE_ONEBODY
485
486// ===========================================================================
487// Two-center Coulomb metric (P|Q) derivatives, needed for the RI-J/RI-K
488// gradient assembly: d(V_PQ)/dR, contracted per the formula derived
489// separately (dE_J/dR = sum_P c_P d(d_P)/dR - 1/2 sum_PQ c_P c_Q d(V_PQ)/dR).
490//
491// STATUS: written following the same validated pattern as the one-body
492// case above (computeOneBodyIntegralDerivatives), but NOT yet run or
493// tested. Two things specifically are hypotheses, not confirmed facts,
494// unlike the one-body case where both are now confirmed:
495//
496// 1. Buffer count/content: this integral is represented internally as a
497// degenerate 4-center integral via two dummy "unit" shells (s-type,
498// no physical center) -- see AOCoulomb::computeCoulombIntegrals for
499// the deriv_order=0 version this mirrors. The hypothesis here is that
500// at deriv_order=1, libint2 still returns derivatives for only the TWO
501// REAL centers (shells[s1]'s atom, shells[s2]'s atom), i.e. 6 buffers,
502// the same count as the one-body case, since the dummy unit shells
503// have no nuclear position to differentiate with respect to. This is
504// a reasonable extrapolation from the one-body case (confirmed: all
505// real centers' derivatives are returned explicitly, not omitted via
506// translational invariance) but has NOT been checked against this
507// specific operator/braket combination. If this is wrong, expect
508// either a wrong buffer count (should show up immediately as an
509// assertion or crash, same as the one-body case's build-configuration
510// issue did) or, more subtly, a right count with wrong center
511// ordering (would show up as a numerical discrepancy in a
512// finite-difference test, same as the units bug did for one-body).
513//
514// 2. libint2 build configuration: needs ENABLE_ERI2=2 (per Psi4's
515// documented pattern: "In order for gradient and Hessian tests to not
516// segfault, need ENABLE_ERI and ENABLE_ERI3 and ENABLE_ERI2 =2"),
517// which is a DIFFERENT flag from the ENABLE_ONEBODY=2 already
518// confirmed necessary and sufficient for the one-body case. This has
519// NOT been separately verified working -- if this crashes with the
520// same "null build function ptr" assertion the one-body case hit
521// before its build was fixed, check this flag/value first, not the
522// buffer-indexing code below.
523//
524// COMPILE GUARD: this is EXACTLY the flag/value distinction the comment
525// above already called out, now actually wired up -- libint2's own
526// engine.impl.h validates braket_ against LIBINT_INCLUDE_ERI2
527// specifically for this braket type (xs_xs), a macro entirely
528// independent of LIBINT_INCLUDE_ONEBODY (which guards overlap/kinetic/
529// nuclear attraction above) and of LIBINT_INCLUDE_ERI3 (which guards
530// the three-center case below). A libint2 build can have ONEBODY
531// enabled while ERI2 is disabled (Psi4's own default configuration,
532// LIBINT2_ENABLE_ERI2=-1, "OFF", is a real, common example) -- exactly
533// the situation that caused this specific operator's real, observed
534// runtime failures (null buffers / segfault) despite
535// LIBINT2_MAX_DERIV_ORDER alone reporting derivative support was
536// available somewhere in the library.
537#if (LIBINT_INCLUDE_ERI2 >= 1)
538std::vector<AOMatrixDerivative> ComputeCoulombMetricDerivatives(
539 const AOBasis& aobasis) {
540 Index natoms = static_cast<Index>(aobasis.getFuncPerAtom().size());
541 Index nthreads = OPENMP::getMaxThreads();
542 std::vector<libint2::Shell> shells = aobasis.GenerateLibintBasis();
543 std::vector<Index> shell2bf = aobasis.getMapToBasisFunctions();
544
545 std::vector<Index> shell2atom;
546 shell2atom.reserve(aobasis.getNumofShells());
547 for (Index s = 0; s < aobasis.getNumofShells(); ++s) {
548 shell2atom.push_back(aobasis.getShell(s).getAtomIndex());
549 }
550
551 Index nbf = aobasis.AOBasisSize();
552 std::vector<AOMatrixDerivative> result(natoms);
553 for (Index a = 0; a < natoms; ++a) {
554 for (Index xyz = 0; xyz < 3; ++xyz) {
555 result[a][xyz] = Eigen::MatrixXd::Zero(nbf, nbf);
556 }
557 }
558
559 std::vector<libint2::Engine> engines(nthreads);
560 engines[0] =
561 libint2::Engine(libint2::Operator::coulomb, aobasis.getMaxNprim(),
562 static_cast<int>(aobasis.getMaxL()), 1);
563 engines[0].set(libint2::BraKet::xs_xs);
564 for (Index i = 1; i < nthreads; ++i) {
565 engines[i] = engines[0];
566 }
567
568 std::exception_ptr eptr_coulmetric = nullptr;
569 std::atomic<bool> any_nonnull_buffer_coulmetric{false};
570#pragma omp parallel for schedule(dynamic)
571 for (Index s1 = 0; s1 < aobasis.getNumofShells(); ++s1) {
572 try {
573 libint2::Engine& engine = engines[OPENMP::getThreadId()];
574 const libint2::Engine::target_ptr_vec& buf = engine.results();
575
576 Index bf1 = shell2bf[s1];
577 Index n1 = shells[s1].size();
578 Index atom1 = shell2atom[s1];
579
580 // Same note as AOCoulomb::computeCoulombIntegrals: cannot use
581 // shellpairs here, this is a two-center integral and overlap
582 // screening would give the wrong result.
583 for (Index s2 = 0; s2 <= s1; ++s2) {
584 engine.compute2<libint2::Operator::coulomb, libint2::BraKet::xs_xs, 1>(
585 shells[s1], libint2::Shell::unit(), shells[s2],
586 libint2::Shell::unit());
587
588 // See the detailed explanation on the analogous check in
589 // computeOneBodyIntegralDerivatives above -- buf[0]==nullptr
590 // alone is not sufficient; buf[3+xyz] is dereferenced
591 // unconditionally below too.
592 if (buf[0] == nullptr || buf[3] == nullptr) {
593 continue;
594 }
595 any_nonnull_buffer_coulmetric.store(true, std::memory_order_relaxed);
596
597 Index bf2 = shell2bf[s2];
598 Index n2 = shells[s2].size();
599 Index atom2 = shell2atom[s2];
600
601 for (Index xyz = 0; xyz < 3; ++xyz) {
602 Eigen::Map<const MatrixLibInt> buf_mat1(buf[xyz], n1, n2);
603 result[atom1][xyz].block(bf1, bf2, n1, n2) += buf_mat1;
604 if (s1 != s2) {
605 result[atom1][xyz].block(bf2, bf1, n2, n1) += buf_mat1.transpose();
606 }
607
608 Eigen::Map<const MatrixLibInt> buf_mat2(buf[3 + xyz], n1, n2);
609 result[atom2][xyz].block(bf1, bf2, n1, n2) += buf_mat2;
610 if (s1 != s2) {
611 result[atom2][xyz].block(bf2, bf1, n2, n1) += buf_mat2.transpose();
612 }
613 }
614 }
615 } catch (...) {
616#pragma omp critical
617 {
618 if (!eptr_coulmetric) {
619 eptr_coulmetric = std::current_exception();
620 }
621 }
622 }
623 }
624 if (eptr_coulmetric) {
625 std::rethrow_exception(eptr_coulmetric);
626 }
627 if (!any_nonnull_buffer_coulmetric.load() && aobasis.getNumofShells() > 0) {
628 // See the detailed explanation on ComputeNuclearAttractionDerivatives'
629 // own version of this check.
630 throw std::runtime_error(
631 "ComputeCoulombMetricDerivatives: engine.results() returned a "
632 "null buffer for EVERY shell pair -- this libint2 build does not "
633 "actually support this operator's derivative integrals at "
634 "runtime, even though it may report LIBINT2_MAX_DERIV_ORDER >= 1 "
635 "for other operators. Rebuild libint2 with this operator's "
636 "derivative support enabled (--enable-eri2=1) to use this "
637 "feature.");
638 }
639 return result;
640}
641#else // !(LIBINT_INCLUDE_ERI2)
642std::vector<AOMatrixDerivative> ComputeCoulombMetricDerivatives(
643 const AOBasis&) {
644 ThrowNoDerivativeSupport("ComputeCoulombMetricDerivatives",
645 "--enable-eri2=1");
646}
647#endif // LIBINT_INCLUDE_ERI2
648
649// ===========================================================================
650// Three-center RI integral derivatives, d(mu,nu|P)/dR, the remaining piece
651// needed (alongside the two-center metric above) for the RI-J/RI-K
652// gradient assembly.
653//
654// STATUS: written but NOT yet run or tested. This case is genuinely new
655// relative to everything validated so far (one-body: 2 real centers, 6
656// buffers, confirmed; two-center metric: 2 real centers via dummy unit
657// shells, 6 buffers, confirmed): THIS integral has THREE real centers
658// (the aux shell's atom, and both dft shells' atoms -- see
659// ComputeAO3cBlock above, which this mirrors at deriv_order=1 instead of
660// 0, using the same auxshell/unit()/shell_col/shell_row shell ordering).
661// The hypothesis, extrapolating from the confirmed "libint2 returns every
662// real center's derivative explicitly, never omits one via translational
663// invariance" pattern, is that this returns 9 buffers (3 real centers x
664// 3 Cartesian), NOT yet checked. If the buffer count is actually
665// different, expect either an immediate crash/assertion (same failure
666// mode as the wrong libint2 build-configuration flag earlier) or a
667// right-count-wrong-ordering bug that would only show up as a numerical
668// discrepancy in a finite-difference test (same as the units bug did for
669// the one-body case) -- do not trust this without running the
670// corresponding test first.
671//
672// Also NOT yet verified: which libint2 build flag(s) this needs. Given
673// Psi4's documented pattern ("need ENABLE_ERI and ENABLE_ERI3 and
674// ENABLE_ERI2 =2" for gradients), ENABLE_ERI3=2 is the most likely
675// requirement, separate from both ENABLE_ONEBODY=2 (confirmed necessary
676// for the one-body case) and whatever ENABLE_ERI2 level the two-center
677// metric case above turned out to need.
678//
679// Memory/design note, not a correctness concern for this validation-scale
680// function but worth flagging before this gets used in a real gradient
681// assembly on a real molecule: this materializes the FULL derivative
682// tensor (all aux functions x all dft functions x all dft functions x
683// all atoms x 3) at once, which scales as O(Naux * Ndft^2 * Natoms * 3) --
684// fine for a small test system, but a real RI-J gradient assembly should
685// almost certainly contract with the density matrix and fitting
686// coefficients on the fly, per shell, rather than ever materializing this
687// full tensor. Not addressed here; this function exists to validate the
688// underlying integral derivative in isolation first, same as the
689// one-body and two-center cases above.
690//
691// COMPILE GUARD: confirmed via libint2's own engine.impl.h, which
692// validates braket_ against LIBINT_INCLUDE_ERI3 specifically for this
693// braket type (xs_xx / xx_xs) -- independent of both
694// LIBINT_INCLUDE_ONEBODY (overlap/kinetic/nuclear attraction) and
695// LIBINT_INCLUDE_ERI2 (the two-center metric just above). Exactly the
696// already-documented-but-until-now-not-actually-checked distinction
697// from the STATUS comment above this function.
698// ===========================================================================
699
700// One entry per Cartesian direction (x,y,z); each holds one matrix per
701// aux basis function (indexed by absolute aux AO index), each matrix
702// being (dftbasis size x dftbasis size).
703using ThreeCenterDerivative = std::array<std::vector<Eigen::MatrixXd>, 3>;
704
705#if (LIBINT_INCLUDE_ERI3 >= 1)
706std::vector<ThreeCenterDerivative> ComputeThreeCenterDerivatives(
707 const AOBasis& auxbasis, const AOBasis& dftbasis) {
708 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
709 // NOTE: assumes auxbasis and dftbasis are defined on the SAME molecule
710 // (same atoms, same count) -- true for every real use case (RI fitting
711 // basis and DFT basis both live on the same molecular geometry), not
712 // separately checked here.
713
714 Index nthreads = OPENMP::getMaxThreads();
715 std::vector<libint2::Shell> dftshells = dftbasis.GenerateLibintBasis();
716 std::vector<libint2::Shell> auxshells = auxbasis.GenerateLibintBasis();
717 std::vector<Index> shell2bf = dftbasis.getMapToBasisFunctions();
718 std::vector<Index> auxshell2bf = auxbasis.getMapToBasisFunctions();
719
720 std::vector<Index> dftshell2atom;
721 dftshell2atom.reserve(dftbasis.getNumofShells());
722 for (Index s = 0; s < dftbasis.getNumofShells(); ++s) {
723 dftshell2atom.push_back(dftbasis.getShell(s).getAtomIndex());
724 }
725 std::vector<Index> auxshell2atom;
726 auxshell2atom.reserve(auxbasis.getNumofShells());
727 for (Index s = 0; s < auxbasis.getNumofShells(); ++s) {
728 auxshell2atom.push_back(auxbasis.getShell(s).getAtomIndex());
729 }
730
731 Index n_dft_bf = dftbasis.AOBasisSize();
732 Index n_aux_bf = auxbasis.AOBasisSize();
733
734 std::vector<ThreeCenterDerivative> result(natoms);
735 for (Index a = 0; a < natoms; ++a) {
736 for (Index xyz = 0; xyz < 3; ++xyz) {
737 result[a][xyz] = std::vector<Eigen::MatrixXd>(
738 n_aux_bf, Eigen::MatrixXd::Zero(n_dft_bf, n_dft_bf));
739 }
740 }
741
742 std::vector<libint2::Engine> engines(nthreads);
743 engines[0] = libint2::Engine(
744 libint2::Operator::coulomb,
745 std::max(dftbasis.getMaxNprim(), auxbasis.getMaxNprim()),
746 static_cast<int>(std::max(dftbasis.getMaxL(), auxbasis.getMaxL())), 1);
747 engines[0].set(libint2::BraKet::xs_xx);
748 for (Index i = 1; i < nthreads; ++i) {
749 engines[i] = engines[0];
750 }
751
752 std::exception_ptr eptr_3c = nullptr;
753 std::atomic<bool> any_nonnull_buffer_3c{false};
754#pragma omp parallel for schedule(dynamic)
755 for (Index aux = 0; aux < auxbasis.getNumofShells(); ++aux) {
756 try {
757 libint2::Engine& engine = engines[OPENMP::getThreadId()];
758 const libint2::Engine::target_ptr_vec& buf = engine.results();
759
760 const libint2::Shell& auxshell = auxshells[aux];
761 Index aux_start = auxshell2bf[aux];
762 Index atom_aux = auxshell2atom[aux];
763
764 for (Index row = 0; row < dftbasis.getNumofShells(); ++row) {
765 const libint2::Shell& shell_row = dftshells[row];
766 Index row_start = shell2bf[row];
767 Index atom_row = dftshell2atom[row];
768
769 // NOTE: unlike ComputeAO3cBlock (deriv_order=0), NOT restricting to
770 // col <= row here even though (mu,nu|P) is symmetric in mu,nu --
771 // keeping the full loop for this validation-scale function to
772 // avoid also having to get a derivative-specific symmetrization
773 // step right on the first pass. Revisit once this is confirmed
774 // correct; the deriv_order=0 code's triangular-then-mirror
775 // approach should still apply to the derivative case in principle
776 // (differentiating a symmetric quantity preserves the symmetry),
777 // but that itself is an assumption worth checking separately
778 // rather than combining with the buffer-count question here.
779 for (Index col = 0; col < dftbasis.getNumofShells(); ++col) {
780 const libint2::Shell& shell_col = dftshells[col];
781 Index col_start = shell2bf[col];
782 Index atom_col = dftshell2atom[col];
783
784 engine
785 .compute2<libint2::Operator::coulomb, libint2::BraKet::xs_xx, 1>(
786 auxshell, libint2::Shell::unit(), shell_col, shell_row);
787
788 // See the detailed explanation on the analogous check in
789 // computeOneBodyIntegralDerivatives above -- this operator
790 // dereferences buf[3+xyz] AND buf[6+xyz] unconditionally below
791 // too (3 real centers, not 2), so both need checking here.
792 if (buf[0] == nullptr || buf[3] == nullptr || buf[6] == nullptr) {
793 continue;
794 }
795 any_nonnull_buffer_3c.store(true, std::memory_order_relaxed);
796
797 // See HIGHEST-RISK note above: 9 buffers assumed (3 real centers
798 // x 3 Cartesian), ordered [aux center xyz][col-shell's atom
799 // xyz][row-shell's atom xyz] -- matching the shell ARGUMENT order
800 // passed to compute2 above (auxshell, unit, shell_col,
801 // shell_row), by direct analogy with the two-center case where
802 // buffer order matched argument order. NOT independently
803 // confirmed for this 3-real-center case.
804 for (Index xyz = 0; xyz < 3; ++xyz) {
805 Eigen::TensorMap<
806 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
807 result_aux(buf[xyz], auxshell.size(), shell_col.size(),
808 shell_row.size());
809 Eigen::TensorMap<
810 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
811 result_col(buf[3 + xyz], auxshell.size(), shell_col.size(),
812 shell_row.size());
813 Eigen::TensorMap<
814 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
815 result_row(buf[6 + xyz], auxshell.size(), shell_col.size(),
816 shell_row.size());
817
818 for (size_t aux_c = 0; aux_c < auxshell.size(); ++aux_c) {
819 Index global_aux = aux_start + static_cast<Index>(aux_c);
820 for (size_t col_c = 0; col_c < shell_col.size(); ++col_c) {
821 for (size_t row_c = 0; row_c < shell_row.size(); ++row_c) {
822 double val_aux = result_aux(aux_c, col_c, row_c);
823 double val_col = result_col(aux_c, col_c, row_c);
824 double val_row = result_row(aux_c, col_c, row_c);
825 Index r = row_start + static_cast<Index>(row_c);
826 Index c = col_start + static_cast<Index>(col_c);
827 result[atom_aux][xyz][global_aux](r, c) += val_aux;
828 result[atom_col][xyz][global_aux](r, c) += val_col;
829 result[atom_row][xyz][global_aux](r, c) += val_row;
830 }
831 }
832 }
833 }
834 }
835 }
836 } catch (...) {
837#pragma omp critical
838 {
839 if (!eptr_3c) {
840 eptr_3c = std::current_exception();
841 }
842 }
843 }
844 }
845 if (eptr_3c) {
846 std::rethrow_exception(eptr_3c);
847 }
848 if (!any_nonnull_buffer_3c.load() && auxbasis.getNumofShells() > 0 &&
849 dftbasis.getNumofShells() > 0) {
850 // See the detailed explanation on ComputeNuclearAttractionDerivatives'
851 // own version of this check.
852 throw std::runtime_error(
853 "ComputeThreeCenterDerivatives: engine.results() returned a null "
854 "buffer for EVERY shell triple -- this libint2 build does not "
855 "actually support this operator's derivative integrals at "
856 "runtime, even though it may report LIBINT2_MAX_DERIV_ORDER >= 1 "
857 "for other operators. Rebuild libint2 with this operator's "
858 "derivative support enabled (--enable-eri3=1) to use this "
859 "feature.");
860 }
861 return result;
862}
863#else // !(LIBINT_INCLUDE_ERI3)
864std::vector<ThreeCenterDerivative> ComputeThreeCenterDerivatives(
865 const AOBasis&, const AOBasis&) {
866 ThrowNoDerivativeSupport("ComputeThreeCenterDerivatives", "--enable-eri3=1");
867}
868#endif // LIBINT_INCLUDE_ERI3
869
870#if (LIBINT_INCLUDE_ERI3 >= 1)
871// Memory-efficient alternative to ComputeThreeCenterDerivatives above,
872// for exactly the use case DFTGradient::RIJGradient needs: the caller
873// only ever wants sum_{mu,nu} density(mu,nu) * d(mu,nu|P)/dR_a[xyz],
874// summed over mu,nu for a FIXED, already-known density -- it never
875// actually needs the full, per-(mu,nu) derivative tensor itself.
876//
877// The full-tensor version stores natoms * 3 * n_aux_bf separate,
878// complete nao x nao matrices SIMULTANEOUSLY (every atom, every
879// Cartesian direction, every auxiliary function, all at once) --
880// confirmed, via a real, reported failure, to exhaust hundreds of GB
881// of memory on a real, moderately-sized molecule (53 atoms, 943 AO
882// basis functions, 2320 auxiliary functions: natoms*3*naux = 368,880
883// separate 943x943 matrices, ~7.1 MB each, totaling roughly 2.6
884// PETABYTES if ever actually held at once -- the process was killed
885// long before completing this single allocation). This function
886// instead contracts each shell-triple's own contribution against
887// density IMMEDIATELY, as it is computed, rather than storing it and
888// contracting later -- reducing the total storage from
889// O(natoms * 3 * n_aux_bf * nao^2) down to O(natoms * 3 * n_aux_bf)
890// plain scalars (a few MB even for the system above), since density
891// itself is already fully known at the time this function runs and
892// does not need to wait for the full tensor to be assembled first.
893//
894// Deliberately a SEPARATE function, not a modification of
895// ComputeThreeCenterDerivatives itself: that function is still relied
896// upon, unchanged, by its own existing, already-validated
897// finite-difference test (test_aoderivatives.cc) and by
898// DFTGradient::RIKGradient (which needs a genuinely different
899// contraction -- against nocc^2 separate occupied-MO-pair products,
900// not one fixed density -- and needs its own, separate fix, not yet
901// done as of this function's own addition).
902//
903// Mirrors ComputeThreeCenterDerivatives's own shell-loop structure
904// exactly (same engine setup, same parallelization over aux shells,
905// same buffer-ordering assumptions -- see that function's own,
906// extensive comments for the caveats that still apply identically
907// here), differing only in what happens to each shell-triple's own
908// computed value once available.
909std::vector<Eigen::MatrixXd> ComputeThreeCenterDerivativeContraction(
910 const AOBasis& auxbasis, const AOBasis& dftbasis,
911 const Eigen::MatrixXd& density) {
912 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
913
914 Index nthreads = OPENMP::getMaxThreads();
915 std::vector<libint2::Shell> dftshells = dftbasis.GenerateLibintBasis();
916 std::vector<libint2::Shell> auxshells = auxbasis.GenerateLibintBasis();
917 std::vector<Index> shell2bf = dftbasis.getMapToBasisFunctions();
918 std::vector<Index> auxshell2bf = auxbasis.getMapToBasisFunctions();
919
920 std::vector<Index> dftshell2atom;
921 dftshell2atom.reserve(dftbasis.getNumofShells());
922 for (Index s = 0; s < dftbasis.getNumofShells(); ++s) {
923 dftshell2atom.push_back(dftbasis.getShell(s).getAtomIndex());
924 }
925 std::vector<Index> auxshell2atom;
926 auxshell2atom.reserve(auxbasis.getNumofShells());
927 for (Index s = 0; s < auxbasis.getNumofShells(); ++s) {
928 auxshell2atom.push_back(auxbasis.getShell(s).getAtomIndex());
929 }
930
931 Index n_aux_bf = auxbasis.AOBasisSize();
932
933 // result[a] is a (3, n_aux_bf) matrix: rows are xyz, columns are the
934 // auxiliary function index p -- the already-contracted
935 // sum_{mu,nu} density(mu,nu) * d(mu,nu|p)/dR_a[xyz], not a per-(mu,nu)
936 // tensor at all.
937 std::vector<Eigen::MatrixXd> result(natoms,
938 Eigen::MatrixXd::Zero(3, n_aux_bf));
939
940 std::vector<libint2::Engine> engines(nthreads);
941 engines[0] = libint2::Engine(
942 libint2::Operator::coulomb,
943 std::max(dftbasis.getMaxNprim(), auxbasis.getMaxNprim()),
944 static_cast<int>(std::max(dftbasis.getMaxL(), auxbasis.getMaxL())), 1);
945 engines[0].set(libint2::BraKet::xs_xx);
946 for (Index i = 1; i < nthreads; ++i) {
947 engines[i] = engines[0];
948 }
949
950 // Direct writes into the shared result vector, matching
951 // ComputeThreeCenterDerivatives's own, already-validated pattern
952 // exactly -- safe without any extra synchronization: global_aux is
953 // always unique per aux shell (and therefore per thread, since aux
954 // shells are what the outer loop parallelizes over), so even though
955 // atom_col/atom_row are not themselves partitioned by thread,
956 // concurrent writes from different threads always land in different
957 // COLUMNS of the same result[atom] matrix -- never the same memory
958 // location, which is what a genuine data race would require.
959 std::exception_ptr eptr_3c = nullptr;
960 std::atomic<bool> any_nonnull_buffer_3c{false};
961#pragma omp parallel for schedule(dynamic)
962 for (Index aux = 0; aux < auxbasis.getNumofShells(); ++aux) {
963 try {
964 libint2::Engine& engine = engines[OPENMP::getThreadId()];
965 const libint2::Engine::target_ptr_vec& buf = engine.results();
966
967 const libint2::Shell& auxshell = auxshells[aux];
968 Index aux_start = auxshell2bf[aux];
969 Index atom_aux = auxshell2atom[aux];
970
971 for (Index row = 0; row < dftbasis.getNumofShells(); ++row) {
972 const libint2::Shell& shell_row = dftshells[row];
973 Index row_start = shell2bf[row];
974 Index atom_row = dftshell2atom[row];
975
976 for (Index col = 0; col < dftbasis.getNumofShells(); ++col) {
977 const libint2::Shell& shell_col = dftshells[col];
978 Index col_start = shell2bf[col];
979 Index atom_col = dftshell2atom[col];
980
981 engine
982 .compute2<libint2::Operator::coulomb, libint2::BraKet::xs_xx, 1>(
983 auxshell, libint2::Shell::unit(), shell_col, shell_row);
984
985 if (buf[0] == nullptr || buf[3] == nullptr || buf[6] == nullptr) {
986 continue;
987 }
988 any_nonnull_buffer_3c.store(true, std::memory_order_relaxed);
989
990 for (Index xyz = 0; xyz < 3; ++xyz) {
991 Eigen::TensorMap<
992 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
993 result_aux(buf[xyz], auxshell.size(), shell_col.size(),
994 shell_row.size());
995 Eigen::TensorMap<
996 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
997 result_col(buf[3 + xyz], auxshell.size(), shell_col.size(),
998 shell_row.size());
999 Eigen::TensorMap<
1000 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1001 result_row(buf[6 + xyz], auxshell.size(), shell_col.size(),
1002 shell_row.size());
1003
1004 for (size_t aux_c = 0; aux_c < auxshell.size(); ++aux_c) {
1005 Index global_aux = aux_start + static_cast<Index>(aux_c);
1006 for (size_t col_c = 0; col_c < shell_col.size(); ++col_c) {
1007 for (size_t row_c = 0; row_c < shell_row.size(); ++row_c) {
1008 double val_aux = result_aux(aux_c, col_c, row_c);
1009 double val_col = result_col(aux_c, col_c, row_c);
1010 double val_row = result_row(aux_c, col_c, row_c);
1011 Index r = row_start + static_cast<Index>(row_c);
1012 Index c = col_start + static_cast<Index>(col_c);
1013 // Immediate contraction against density, at exactly
1014 // the (r, c) element just computed -- this is the
1015 // ONLY structural difference from
1016 // ComputeThreeCenterDerivatives's own inner loop,
1017 // which instead scatters val_aux/val_col/val_row into
1018 // a full, retained nao x nao matrix here.
1019 double d_rc = density(r, c);
1020 result[atom_aux](xyz, global_aux) += d_rc * val_aux;
1021 result[atom_col](xyz, global_aux) += d_rc * val_col;
1022 result[atom_row](xyz, global_aux) += d_rc * val_row;
1023 }
1024 }
1025 }
1026 }
1027 }
1028 }
1029 } catch (...) {
1030#pragma omp critical
1031 {
1032 if (!eptr_3c) {
1033 eptr_3c = std::current_exception();
1034 }
1035 }
1036 }
1037 }
1038 if (eptr_3c) {
1039 std::rethrow_exception(eptr_3c);
1040 }
1041 if (!any_nonnull_buffer_3c.load() && auxbasis.getNumofShells() > 0 &&
1042 dftbasis.getNumofShells() > 0) {
1043 throw std::runtime_error(
1044 "ComputeThreeCenterDerivativeContraction: engine.results() "
1045 "returned a null buffer for EVERY shell triple -- this libint2 "
1046 "build does not actually support this operator's derivative "
1047 "integrals at runtime, even though it may report "
1048 "LIBINT2_MAX_DERIV_ORDER >= 1 for other operators. Rebuild "
1049 "libint2 with this operator's derivative support enabled "
1050 "(--enable-eri3=1) to use this feature.");
1051 }
1052 return result;
1053}
1054#else // !(LIBINT_INCLUDE_ERI3)
1055std::vector<Eigen::MatrixXd> ComputeThreeCenterDerivativeContraction(
1056 const AOBasis&, const AOBasis&, const Eigen::MatrixXd&) {
1057 ThrowNoDerivativeSupport("ComputeThreeCenterDerivativeContraction",
1058 "--enable-eri3=1");
1059}
1060#endif // LIBINT_INCLUDE_ERI3
1061
1062#if (LIBINT_INCLUDE_ERI3 >= 1)
1063// Per-atom alternative to ComputeThreeCenterDerivatives, for
1064// DFTGradient::RIKGradient's own needs specifically: unlike
1065// RIJGradient's single, fixed density, RIKGradient needs the same
1066// derivative tensor contracted against nocc^2 separate occupied-MO-pair
1067// products (see that function's own comments) -- immediate contraction
1068// (ComputeThreeCenterDerivativeContraction's own approach) would still
1069// need a (natoms, 3, n_aux_bf, nocc, nocc) result, which remains far
1070// too large for a real, moderately-sized system (roughly 252 GB for
1071// the 53-atom, 943 AO / 2320 auxiliary / 93 occupied-orbital system
1072// that originally motivated this whole fix).
1073//
1074// Instead: compute and return only ONE atom's own (3, n_aux_bf) set of
1075// FULL nao x nao derivative matrices at a time -- the caller (RIKGradient)
1076// loops over atoms itself, calling this once per atom and discarding
1077// each atom's own result before requesting the next. Peak memory drops
1078// from natoms times a single atom's own footprint down to just that
1079// single atom's own footprint (roughly 49.5 GB for the same real
1080// system above, comfortably within a 256 GB machine, versus the
1081// original ~2.6 PETABYTES for holding every atom's own tensor
1082// simultaneously).
1083//
1084// The real cost of this choice, stated plainly rather than glossed
1085// over: since a single shell-triple integral can contribute to up to
1086// THREE different atoms at once (via the aux, col, and row shells'
1087// own, potentially different atom centers -- see
1088// ComputeThreeCenterDerivatives's own comments on this same point),
1089// there is no way to compute "just atom A's own contribution" without
1090// still evaluating every shell triple -- this function's own,
1091// expensive shell-loop is NOT actually cheaper per call than the
1092// original, full function's own single pass. Calling it once per atom
1093// (as RIKGradient below does) therefore costs roughly natoms times the
1094// integral evaluation work of a single, full pass -- a genuine,
1095// deliberate trade of speed for memory, chosen here because the
1096// alternative (holding everything at once) does not fit in memory at
1097// all for real systems, not because this is free.
1099 const AOBasis& auxbasis, const AOBasis& dftbasis, Index target_atom) {
1100 std::vector<libint2::Shell> dftshells = dftbasis.GenerateLibintBasis();
1101 std::vector<libint2::Shell> auxshells = auxbasis.GenerateLibintBasis();
1102 std::vector<Index> shell2bf = dftbasis.getMapToBasisFunctions();
1103 std::vector<Index> auxshell2bf = auxbasis.getMapToBasisFunctions();
1104
1105 std::vector<Index> dftshell2atom;
1106 dftshell2atom.reserve(dftbasis.getNumofShells());
1107 for (Index s = 0; s < dftbasis.getNumofShells(); ++s) {
1108 dftshell2atom.push_back(dftbasis.getShell(s).getAtomIndex());
1109 }
1110 std::vector<Index> auxshell2atom;
1111 auxshell2atom.reserve(auxbasis.getNumofShells());
1112 for (Index s = 0; s < auxbasis.getNumofShells(); ++s) {
1113 auxshell2atom.push_back(auxbasis.getShell(s).getAtomIndex());
1114 }
1115
1116 Index n_dft_bf = dftbasis.AOBasisSize();
1117 Index n_aux_bf = auxbasis.AOBasisSize();
1118 Index nthreads = OPENMP::getMaxThreads();
1119
1120 // Only THIS atom's own (3, n_aux_bf) set of full nao x nao matrices --
1121 // never all natoms atoms' own matrices at once, unlike
1122 // ComputeThreeCenterDerivatives above.
1123 ThreeCenterDerivative result;
1124 for (Index xyz = 0; xyz < 3; ++xyz) {
1125 result[xyz] = std::vector<Eigen::MatrixXd>(
1126 n_aux_bf, Eigen::MatrixXd::Zero(n_dft_bf, n_dft_bf));
1127 }
1128
1129 std::vector<libint2::Engine> engines(nthreads);
1130 engines[0] = libint2::Engine(
1131 libint2::Operator::coulomb,
1132 std::max(dftbasis.getMaxNprim(), auxbasis.getMaxNprim()),
1133 static_cast<int>(std::max(dftbasis.getMaxL(), auxbasis.getMaxL())), 1);
1134 engines[0].set(libint2::BraKet::xs_xx);
1135 for (Index i = 1; i < nthreads; ++i) {
1136 engines[i] = engines[0];
1137 }
1138
1139 std::exception_ptr eptr_3c = nullptr;
1140 std::atomic<bool> any_nonnull_buffer_3c{false};
1141#pragma omp parallel for schedule(dynamic)
1142 for (Index aux = 0; aux < auxbasis.getNumofShells(); ++aux) {
1143 try {
1144 libint2::Engine& engine = engines[OPENMP::getThreadId()];
1145 const libint2::Engine::target_ptr_vec& buf = engine.results();
1146
1147 const libint2::Shell& auxshell = auxshells[aux];
1148 Index aux_start = auxshell2bf[aux];
1149 Index atom_aux = auxshell2atom[aux];
1150
1151 for (Index row = 0; row < dftbasis.getNumofShells(); ++row) {
1152 const libint2::Shell& shell_row = dftshells[row];
1153 Index row_start = shell2bf[row];
1154 Index atom_row = dftshell2atom[row];
1155
1156 for (Index col = 0; col < dftbasis.getNumofShells(); ++col) {
1157 const libint2::Shell& shell_col = dftshells[col];
1158 Index col_start = shell2bf[col];
1159 Index atom_col = dftshell2atom[col];
1160
1161 // Skip entirely if none of the three shells in this triple are
1162 // even centered on the target atom -- this triple contributes
1163 // nothing to this atom's own result at all, so there is no
1164 // need to run the (still genuinely expensive) integral
1165 // evaluation itself.
1166 if (atom_aux != target_atom && atom_col != target_atom &&
1167 atom_row != target_atom) {
1168 continue;
1169 }
1170
1171 engine
1172 .compute2<libint2::Operator::coulomb, libint2::BraKet::xs_xx, 1>(
1173 auxshell, libint2::Shell::unit(), shell_col, shell_row);
1174
1175 if (buf[0] == nullptr || buf[3] == nullptr || buf[6] == nullptr) {
1176 continue;
1177 }
1178 any_nonnull_buffer_3c.store(true, std::memory_order_relaxed);
1179
1180 for (Index xyz = 0; xyz < 3; ++xyz) {
1181 Eigen::TensorMap<
1182 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1183 result_aux(buf[xyz], auxshell.size(), shell_col.size(),
1184 shell_row.size());
1185 Eigen::TensorMap<
1186 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1187 result_col(buf[3 + xyz], auxshell.size(), shell_col.size(),
1188 shell_row.size());
1189 Eigen::TensorMap<
1190 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1191 result_row(buf[6 + xyz], auxshell.size(), shell_col.size(),
1192 shell_row.size());
1193
1194 for (size_t aux_c = 0; aux_c < auxshell.size(); ++aux_c) {
1195 Index global_aux = aux_start + static_cast<Index>(aux_c);
1196 for (size_t col_c = 0; col_c < shell_col.size(); ++col_c) {
1197 for (size_t row_c = 0; row_c < shell_row.size(); ++row_c) {
1198 Index r = row_start + static_cast<Index>(row_c);
1199 Index c = col_start + static_cast<Index>(col_c);
1200 // Only accumulate the term(s) whose own atom center
1201 // actually matches target_atom -- e.g. if atom_aux ==
1202 // target_atom but atom_col/atom_row do not, only
1203 // val_aux contributes to this atom's own result; the
1204 // other two terms belong to OTHER atoms' own results,
1205 // which this call does not compute or return at all.
1206 if (atom_aux == target_atom) {
1207 result[xyz][global_aux](r, c) +=
1208 result_aux(aux_c, col_c, row_c);
1209 }
1210 if (atom_col == target_atom) {
1211 result[xyz][global_aux](r, c) +=
1212 result_col(aux_c, col_c, row_c);
1213 }
1214 if (atom_row == target_atom) {
1215 result[xyz][global_aux](r, c) +=
1216 result_row(aux_c, col_c, row_c);
1217 }
1218 }
1219 }
1220 }
1221 }
1222 }
1223 }
1224 } catch (...) {
1225#pragma omp critical
1226 {
1227 if (!eptr_3c) {
1228 eptr_3c = std::current_exception();
1229 }
1230 }
1231 }
1232 }
1233 if (eptr_3c) {
1234 std::rethrow_exception(eptr_3c);
1235 }
1236 if (!any_nonnull_buffer_3c.load() && auxbasis.getNumofShells() > 0 &&
1237 dftbasis.getNumofShells() > 0) {
1238 throw std::runtime_error(
1239 "ComputeThreeCenterDerivativesForAtom: engine.results() returned "
1240 "a null buffer for EVERY shell triple touching this atom -- this "
1241 "libint2 build does not actually support this operator's "
1242 "derivative integrals at runtime, even though it may report "
1243 "LIBINT2_MAX_DERIV_ORDER >= 1 for other operators. Rebuild "
1244 "libint2 with this operator's derivative support enabled "
1245 "(--enable-eri3=1) to use this feature.");
1246 }
1247 return result;
1248}
1249#else // !(LIBINT_INCLUDE_ERI3)
1251 const AOBasis&,
1252 Index) {
1253 ThrowNoDerivativeSupport("ComputeThreeCenterDerivativesForAtom",
1254 "--enable-eri3=1");
1255}
1256#endif // LIBINT_INCLUDE_ERI3
1257
1258#if (LIBINT_INCLUDE_ERI3 >= 1)
1259// CORRECTED, single-pass alternative for DFTGradient::RIKGradient,
1260// replacing the earlier ComputeThreeCenterDerivativesForAtom-based,
1261// per-atom approach (natoms separate passes over the full shell loop,
1262// each with a real, measured runtime of over an hour for a real, 53-
1263// atom hybrid-functional system).
1264//
1265// The per-atom approach was chosen after a genuine ARITHMETIC ERROR:
1266// the "contract immediately against all nocc^2 occupied-orbital-pair
1267// products at once, in a single pass" alternative was originally
1268// (incorrectly) estimated at ~252 GB for the real system that
1269// motivated this whole fix and rejected as too large. Recomputed
1270// directly and carefully after the per-atom approach's own, real,
1271// measured cost turned out to be prohibitive: natoms * 3 * n_aux_bf *
1272// nocc^2 * 8 bytes = 53*3*2320*93*93*8 = ~23.8 GB for that same real
1273// system -- comfortably smaller than even the per-atom approach's own
1274// ~46 GB per-atom peak, not larger, and needing only ONE pass over the
1275// shell loop rather than natoms of them. This function implements
1276// that corrected, actually-small approach.
1277//
1278// result[a][xyz][p] is now an (nocc, nocc) matrix, NOT a (nao, nao)
1279// one -- reusing the same ThreeCenterDerivative type alias (a plain
1280// std::array<std::vector<Eigen::MatrixXd>, 3>, size-agnostic) for
1281// convenience, not because these are the same shape as
1282// ComputeThreeCenterDerivatives's own (nao, nao) result.
1283//
1284// Each shell-triple's own contribution is folded in via a rank-1
1285// (outer-product) update of the relevant (nocc, nocc) matrix --
1286// occ_mo_coeffs.row(r).transpose() * occ_mo_coeffs.row(c), scaled by
1287// the AO-basis value just computed -- rather than a naive, scalar
1288// double loop over (i,j): Eigen's own vectorized rank-1-update
1289// machinery (used here via noalias() +=) handles this efficiently,
1290// unlike an explicit i,j loop which would need the same O(nocc^2)
1291// work per element without the benefit of Eigen's own optimized
1292// kernels.
1293std::vector<ThreeCenterDerivative> ComputeThreeCenterDerivativesMOTransformed(
1294 const AOBasis& auxbasis, const AOBasis& dftbasis,
1295 const Eigen::MatrixXd& occ_mo_coeffs) {
1296 Index natoms = static_cast<Index>(dftbasis.getFuncPerAtom().size());
1297 Index nocc = occ_mo_coeffs.cols();
1298
1299 Index nthreads = OPENMP::getMaxThreads();
1300 std::vector<libint2::Shell> dftshells = dftbasis.GenerateLibintBasis();
1301 std::vector<libint2::Shell> auxshells = auxbasis.GenerateLibintBasis();
1302 std::vector<Index> shell2bf = dftbasis.getMapToBasisFunctions();
1303 std::vector<Index> auxshell2bf = auxbasis.getMapToBasisFunctions();
1304
1305 std::vector<Index> dftshell2atom;
1306 dftshell2atom.reserve(dftbasis.getNumofShells());
1307 for (Index s = 0; s < dftbasis.getNumofShells(); ++s) {
1308 dftshell2atom.push_back(dftbasis.getShell(s).getAtomIndex());
1309 }
1310 std::vector<Index> auxshell2atom;
1311 auxshell2atom.reserve(auxbasis.getNumofShells());
1312 for (Index s = 0; s < auxbasis.getNumofShells(); ++s) {
1313 auxshell2atom.push_back(auxbasis.getShell(s).getAtomIndex());
1314 }
1315
1316 Index n_aux_bf = auxbasis.AOBasisSize();
1317
1318 // result[a][xyz][p] is an (nocc, nocc) matrix -- natoms * 3 * n_aux_bf
1319 // of them, ~23.8 GB total for the real system that motivated this
1320 // fix, not the natoms*3*n_aux_bf FULL (nao,nao) matrices (~2.6 PB)
1321 // ComputeThreeCenterDerivatives itself would hold.
1322 std::vector<ThreeCenterDerivative> result(natoms);
1323 for (Index a = 0; a < natoms; ++a) {
1324 for (Index xyz = 0; xyz < 3; ++xyz) {
1325 result[a][xyz] = std::vector<Eigen::MatrixXd>(
1326 n_aux_bf, Eigen::MatrixXd::Zero(nocc, nocc));
1327 }
1328 }
1329
1330 std::vector<libint2::Engine> engines(nthreads);
1331 engines[0] = libint2::Engine(
1332 libint2::Operator::coulomb,
1333 std::max(dftbasis.getMaxNprim(), auxbasis.getMaxNprim()),
1334 static_cast<int>(std::max(dftbasis.getMaxL(), auxbasis.getMaxL())), 1);
1335 engines[0].set(libint2::BraKet::xs_xx);
1336 for (Index i = 1; i < nthreads; ++i) {
1337 engines[i] = engines[0];
1338 }
1339
1340 // Direct writes into the shared result vector -- safe for the exact
1341 // same reason as ComputeThreeCenterDerivativeContraction's own
1342 // comment on this point: global_aux is always unique per aux shell
1343 // (and therefore per thread), so concurrent writes from different
1344 // threads always land in different result[atom][xyz] VECTOR SLOTS
1345 // (indexed by global_aux) -- never the same memory location.
1346 std::exception_ptr eptr_3c = nullptr;
1347 std::atomic<bool> any_nonnull_buffer_3c{false};
1348#pragma omp parallel for schedule(dynamic)
1349 for (Index aux = 0; aux < auxbasis.getNumofShells(); ++aux) {
1350 try {
1351 libint2::Engine& engine = engines[OPENMP::getThreadId()];
1352 const libint2::Engine::target_ptr_vec& buf = engine.results();
1353
1354 const libint2::Shell& auxshell = auxshells[aux];
1355 Index aux_start = auxshell2bf[aux];
1356 Index atom_aux = auxshell2atom[aux];
1357
1358 for (Index row = 0; row < dftbasis.getNumofShells(); ++row) {
1359 const libint2::Shell& shell_row = dftshells[row];
1360 Index row_start = shell2bf[row];
1361 Index atom_row = dftshell2atom[row];
1362
1363 for (Index col = 0; col < dftbasis.getNumofShells(); ++col) {
1364 const libint2::Shell& shell_col = dftshells[col];
1365 Index col_start = shell2bf[col];
1366 Index atom_col = dftshell2atom[col];
1367
1368 engine
1369 .compute2<libint2::Operator::coulomb, libint2::BraKet::xs_xx, 1>(
1370 auxshell, libint2::Shell::unit(), shell_col, shell_row);
1371
1372 if (buf[0] == nullptr || buf[3] == nullptr || buf[6] == nullptr) {
1373 continue;
1374 }
1375 any_nonnull_buffer_3c.store(true, std::memory_order_relaxed);
1376
1377 // The relevant row-slices of occ_mo_coeffs for this shell pair
1378 // -- sliced once per shell pair, reused for every auxiliary
1379 // function and Cartesian direction within it.
1380 Index nrow = static_cast<Index>(shell_row.size());
1381 Index ncol = static_cast<Index>(shell_col.size());
1382 Eigen::MatrixXd mo_row_block =
1383 occ_mo_coeffs.middleRows(row_start, nrow);
1384 Eigen::MatrixXd mo_col_block =
1385 occ_mo_coeffs.middleRows(col_start, ncol);
1386
1387 for (Index xyz = 0; xyz < 3; ++xyz) {
1388 Eigen::TensorMap<
1389 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1390 result_aux(buf[xyz], auxshell.size(), shell_col.size(),
1391 shell_row.size());
1392 Eigen::TensorMap<
1393 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1394 result_col(buf[3 + xyz], auxshell.size(), shell_col.size(),
1395 shell_row.size());
1396 Eigen::TensorMap<
1397 Eigen::Tensor<const double, 3, Eigen::RowMajor> const>
1398 result_row(buf[6 + xyz], auxshell.size(), shell_col.size(),
1399 shell_row.size());
1400
1401 for (size_t aux_c = 0; aux_c < auxshell.size(); ++aux_c) {
1402 Index global_aux = aux_start + static_cast<Index>(aux_c);
1403
1404 // CORRECTED (see this function's own header comment):
1405 // extract this auxiliary function's own (nrow, ncol) raw
1406 // block for each of the three terms, then contract against
1407 // occ_mo_coeffs via TWO SMALL MATRIX MULTIPLICATIONS --
1408 // giving the full (nocc, nocc) contribution for this shell
1409 // pair and auxiliary function in one shot, rather than
1410 // nrow*ncol separate, full (nocc,nocc) rank-1 outer-product
1411 // updates (the ORIGINAL version of this loop, confirmed
1412 // directly, via a real run, to cost on the order of
1413 // nao^2 * n_aux_bf * nocc^2 operations -- tens of trillions
1414 // for the real system that motivated this whole fix, and
1415 // the actual, real cause of a >50 minute runtime even
1416 // though the OUTER shell-triple loop itself is already a
1417 // single pass, not a per-atom one).
1418 Eigen::MatrixXd block_aux(nrow, ncol);
1419 Eigen::MatrixXd block_col(nrow, ncol);
1420 Eigen::MatrixXd block_row(nrow, ncol);
1421 for (Index col_c = 0; col_c < ncol; ++col_c) {
1422 for (Index row_c = 0; row_c < nrow; ++row_c) {
1423 block_aux(row_c, col_c) =
1424 result_aux(aux_c, static_cast<size_t>(col_c),
1425 static_cast<size_t>(row_c));
1426 block_col(row_c, col_c) =
1427 result_col(aux_c, static_cast<size_t>(col_c),
1428 static_cast<size_t>(row_c));
1429 block_row(row_c, col_c) =
1430 result_row(aux_c, static_cast<size_t>(col_c),
1431 static_cast<size_t>(row_c));
1432 }
1433 }
1434 result[atom_aux][xyz][global_aux].noalias() +=
1435 mo_row_block.transpose() * block_aux * mo_col_block;
1436 result[atom_col][xyz][global_aux].noalias() +=
1437 mo_row_block.transpose() * block_col * mo_col_block;
1438 result[atom_row][xyz][global_aux].noalias() +=
1439 mo_row_block.transpose() * block_row * mo_col_block;
1440 }
1441 }
1442 }
1443 }
1444 } catch (...) {
1445#pragma omp critical
1446 {
1447 if (!eptr_3c) {
1448 eptr_3c = std::current_exception();
1449 }
1450 }
1451 }
1452 }
1453 if (eptr_3c) {
1454 std::rethrow_exception(eptr_3c);
1455 }
1456 if (!any_nonnull_buffer_3c.load() && auxbasis.getNumofShells() > 0 &&
1457 dftbasis.getNumofShells() > 0) {
1458 throw std::runtime_error(
1459 "ComputeThreeCenterDerivativesMOTransformed: engine.results() "
1460 "returned a null buffer for EVERY shell triple -- this libint2 "
1461 "build does not actually support this operator's derivative "
1462 "integrals at runtime, even though it may report "
1463 "LIBINT2_MAX_DERIV_ORDER >= 1 for other operators. Rebuild "
1464 "libint2 with this operator's derivative support enabled "
1465 "(--enable-eri3=1) to use this feature.");
1466 }
1467 return result;
1468}
1469#else // !(LIBINT_INCLUDE_ERI3)
1470std::vector<ThreeCenterDerivative> ComputeThreeCenterDerivativesMOTransformed(
1471 const AOBasis&, const AOBasis&, const Eigen::MatrixXd&) {
1472 ThrowNoDerivativeSupport("ComputeThreeCenterDerivativesMOTransformed",
1473 "--enable-eri3=1");
1474}
1475#endif // LIBINT_INCLUDE_ERI3
1476
1477// Energy-level (deriv_order=0) three-center integral (mu,nu|P), kept here
1478// (rather than in dftgradient.cc) so all direct libint2 API usage stays
1479// confined to this file and libint2_calls.cc -- dftgradient.cc, which
1480// needs this for the RI-J gradient assembly, consumes only the returned
1481// matrices, never touching libint2 types directly. Structurally the same
1482// shell-loop as ComputeThreeCenterDerivatives above, minus the
1483// atom/Cartesian bookkeeping that only applies at deriv_order=1, and
1484// reusing the already-existing ComputeAO3cBlock helper from
1485// libint2_calls.cc (declared there, not in any header -- forward
1486// declared here) rather than re-deriving the same shell-iteration logic
1487// a third time. UNGUARDED (unlike the functions above): deriv_order=0
1488// integrals are always supported, by any libint2 build.
1489std::vector<Eigen::MatrixXd> ComputeThreeCenterIntegrals(
1490 const AOBasis& auxbasis, const AOBasis& dftbasis) {
1491 std::vector<libint2::Shell> auxshells = auxbasis.GenerateLibintBasis();
1492 std::vector<Index> auxshell2bf = auxbasis.getMapToBasisFunctions();
1493
1494 std::vector<Eigen::MatrixXd> tensor(
1495 auxbasis.AOBasisSize(),
1496 Eigen::MatrixXd::Zero(dftbasis.AOBasisSize(), dftbasis.AOBasisSize()));
1497
1498 libint2::Engine engine(
1499 libint2::Operator::coulomb,
1500 std::max(dftbasis.getMaxNprim(), auxbasis.getMaxNprim()),
1501 static_cast<int>(std::max(dftbasis.getMaxL(), auxbasis.getMaxL())), 0);
1502 engine.set(libint2::BraKet::xs_xx);
1503
1504 for (Index aux = 0; aux < auxbasis.getNumofShells(); ++aux) {
1505 std::vector<Eigen::MatrixXd> block =
1506 ComputeAO3cBlock(auxshells[aux], dftbasis, engine);
1507 Index aux_start = auxshell2bf[aux];
1508 for (size_t i = 0; i < block.size(); ++i) {
1509 tensor[aux_start + static_cast<Index>(i)] = block[i];
1510 }
1511 }
1512 return tensor;
1513}
1514
1515// ===========================================================================
1516// Nuclear attraction (electron-nucleus Coulomb) integral derivative --
1517// d(V_ne)/dR, the piece discovered MISSING from the total gradient
1518// assembly by the first genuine end-to-end SCF+forces test
1519// (test_dftengine_forces.cc). Every earlier gradient test in this branch
1520// validated individual terms against FIXED density matrices, never the
1521// complete picture against a real total SCF energy -- which is exactly
1522// why this gap went undetected until now: kinetic derivatives were
1523// validated at the very start of this session and then never actually
1524// used in any gradient assembly, and this nuclear attraction piece was
1525// never implemented at all.
1526//
1527// V_ne_munu = -sum_A Z_A * integral[ chi_mu(r) (1/|r-R_A|) chi_nu(r) dr ]
1528//
1529// Genuinely a 3-real-center integral per point charge: the two basis
1530// function centers, PLUS the point charge's own position -- structurally
1531// identical to the already-validated 3-center RI derivative case (2
1532// basis centers + 1 external center = 3 total), just with a different
1533// operator (libint2::Operator::nuclear instead of coulomb/xs_xx) and
1534// looping over EACH atom as a single point charge at a time (via
1535// engine.set_params(make_point_charges(...)) per atom, matching the
1536// pattern in libint2's own reference example --
1537// compute_1body_ints_deriv<Operator::nuclear> in
1538// libint2/include/libint2/lcao/1body.h, used directly to build real HF
1539// forces there) rather than all atoms simultaneously.
1540//
1541// SIGN: this codebase's own existing, already-validated energy-level
1542// code confirms V_ne (AOMultipole::FillPotential's output) is negative
1543// (attractive) -- AOMultipole computes a POSITIVE-charge Fill(aobasis)
1544// per atom then does `aopotential_ -= Fill(aobasis)`, and
1545// DFTEngine::SetupH0 does `H0 = kinetic + dftAOESP` (a plain addition,
1546// no extra negation by the caller). Initially reasoned (incorrectly)
1547// that libint2::Operator::nuclear must therefore need an explicit
1548// negation to match -- a finite-difference test (exact magnitude match,
1549// opposite sign in every single element) confirmed the OPPOSITE:
1550// libint2::Operator::nuclear already returns the attractive (negative)
1551// convention directly, no negation needed. Fixed by using += throughout
1552// rather than -=. Worth remembering as a concrete example of why this
1553// branch insists on actually running things rather than trusting
1554// plausible-sounding sign reasoning alone.
1555//
1556// STATUS: buffer count/ordering (9 buffers per shell-pair-per-point-
1557// charge, [shell1 atom][shell2 atom][point-charge atom]) CONFIRMED
1558// correct by the same finite-difference test -- only the sign was
1559// wrong, now fixed. Re-run pending to confirm the fix.
1560// ===========================================================================
1561#if defined(LIBINT2_MAX_DERIV_ORDER) && LIBINT2_MAX_DERIV_ORDER >= 1 && \
1562 (LIBINT_INCLUDE_ONEBODY >= 1)
1563std::vector<AOMatrixDerivative> ComputeNuclearAttractionDerivatives(
1564 const AOBasis& aobasis, const QMMolecule& mol) {
1565 Index natoms = mol.size();
1566 Index nthreads = OPENMP::getMaxThreads();
1567 std::vector<libint2::Shell> shells = aobasis.GenerateLibintBasis();
1568 std::vector<Index> shell2bf = aobasis.getMapToBasisFunctions();
1569
1570 std::vector<Index> shell2atom;
1571 shell2atom.reserve(aobasis.getNumofShells());
1572 for (Index s = 0; s < aobasis.getNumofShells(); ++s) {
1573 shell2atom.push_back(aobasis.getShell(s).getAtomIndex());
1574 }
1575
1576 Index nbf = aobasis.AOBasisSize();
1577 std::vector<std::vector<AOMatrixDerivative>> result_thread(nthreads);
1578 for (Index t = 0; t < nthreads; ++t) {
1579 result_thread[t].resize(natoms);
1580 for (Index a = 0; a < natoms; ++a) {
1581 for (Index xyz = 0; xyz < 3; ++xyz) {
1582 result_thread[t][a][xyz] = Eigen::MatrixXd::Zero(nbf, nbf);
1583 }
1584 }
1585 }
1586
1587 std::vector<libint2::Engine> engines(nthreads);
1588 for (Index i = 0; i < nthreads; ++i) {
1589 engines[i] =
1590 libint2::Engine(libint2::Operator::nuclear, aobasis.getMaxNprim(),
1591 static_cast<int>(aobasis.getMaxL()), 1);
1592 }
1593
1594 // Parallelized over the OUTER atom (point-charge) loop, not the inner
1595 // shell-pair loop as in every other function in this file -- each
1596 // atom needs its own engine.set_params() call (mutating shared engine
1597 // state), so a per-thread engine set up fresh per atom is the natural
1598 // parallelization axis here, not shell pairs within one fixed engine
1599 // configuration.
1600 std::exception_ptr eptr_nucattr = nullptr;
1601 std::atomic<bool> any_nonnull_buffer_nucattr{false};
1602#pragma omp parallel for schedule(dynamic)
1603 for (Index A = 0; A < natoms; ++A) {
1604 try {
1605 Index thread_id = OPENMP::getThreadId();
1606 libint2::Engine& engine = engines[thread_id];
1607
1608 std::vector<libint2::Atom> single_atom(1);
1609 single_atom[0].atomic_number = static_cast<int>(mol[A].getNuccharge());
1610 single_atom[0].x = mol[A].getPos().x();
1611 single_atom[0].y = mol[A].getPos().y();
1612 single_atom[0].z = mol[A].getPos().z();
1613 engine.set_params(libint2::make_point_charges(single_atom));
1614
1615 const libint2::Engine::target_ptr_vec& buf = engine.results();
1616
1617 for (Index s1 = 0; s1 < aobasis.getNumofShells(); ++s1) {
1618 Index bf1 = shell2bf[s1];
1619 Index n1 = shells[s1].size();
1620 Index atom1 = shell2atom[s1];
1621
1622 for (Index s2 = 0; s2 <= s1; ++s2) {
1623 engine.compute(shells[s1], shells[s2]);
1624
1625 // See the detailed explanation on the analogous check in
1626 // computeOneBodyIntegralDerivatives above -- this operator
1627 // dereferences buf[3+xyz] AND buf[6+xyz] unconditionally below
1628 // too (3 real centers: shell1's atom, shell2's atom, the point
1629 // charge), so both need checking here. This exact gap is what
1630 // produced BOTH observed failure modes on different CI
1631 // architectures for this specific function: a hard segfault
1632 // (buf[3] or buf[6] actually null) on one, and a silently
1633 // all-zero result (buf[3]/buf[6] non-null but zero-filled,
1634 // caught instead by the separate norm check further below) on
1635 // another -- same underlying root cause, different libint2
1636 // build behavior for an unsupported operator.
1637 if (buf[0] == nullptr || buf[3] == nullptr || buf[6] == nullptr) {
1638 continue;
1639 }
1640 any_nonnull_buffer_nucattr.store(true, std::memory_order_relaxed);
1641
1642 Index bf2 = shell2bf[s2];
1643 Index n2 = shells[s2].size();
1644 Index atom2 = shell2atom[s2];
1645
1646 for (Index xyz = 0; xyz < 3; ++xyz) {
1647 // SIGN FIX: confirmed via finite-difference test (exact
1648 // magnitude match, opposite sign everywhere) that
1649 // libint2::Operator::nuclear already returns the attractive
1650 // (negative) V_ne convention directly -- the explicit
1651 // negation originally here (reasoned from AOMultipole's own
1652 // "positive charge in, subtract" pattern) was backwards,
1653 // double-negating an already-correctly-signed quantity. Using
1654 // += throughout now, not -=.
1655 Eigen::Map<const MatrixLibInt> buf_mat1(buf[xyz], n1, n2);
1656 result_thread[thread_id][atom1][xyz].block(bf1, bf2, n1, n2) +=
1657 buf_mat1;
1658 if (s1 != s2) {
1659 result_thread[thread_id][atom1][xyz].block(bf2, bf1, n2, n1) +=
1660 buf_mat1.transpose();
1661 }
1662
1663 Eigen::Map<const MatrixLibInt> buf_mat2(buf[3 + xyz], n1, n2);
1664 result_thread[thread_id][atom2][xyz].block(bf1, bf2, n1, n2) +=
1665 buf_mat2;
1666 if (s1 != s2) {
1667 result_thread[thread_id][atom2][xyz].block(bf2, bf1, n2, n1) +=
1668 buf_mat2.transpose();
1669 }
1670
1671 Eigen::Map<const MatrixLibInt> buf_mat3(buf[6 + xyz], n1, n2);
1672 result_thread[thread_id][A][xyz].block(bf1, bf2, n1, n2) +=
1673 buf_mat3;
1674 if (s1 != s2) {
1675 result_thread[thread_id][A][xyz].block(bf2, bf1, n2, n1) +=
1676 buf_mat3.transpose();
1677 }
1678 }
1679 }
1680 }
1681 } catch (...) {
1682#pragma omp critical
1683 {
1684 if (!eptr_nucattr) {
1685 eptr_nucattr = std::current_exception();
1686 }
1687 }
1688 }
1689 }
1690 if (eptr_nucattr) {
1691 std::rethrow_exception(eptr_nucattr);
1692 }
1693 if (!any_nonnull_buffer_nucattr.load() && natoms > 0 &&
1694 aobasis.getNumofShells() > 0) {
1695 // Distinct from the LIBINT2_MAX_DERIV_ORDER compile-time guard
1696 // above: that macro is library-wide (the maximum derivative order
1697 // ANY operator supports), but individual operators can each have
1698 // their own, separate build configuration -- a libint2 build can
1699 // report LIBINT2_MAX_DERIV_ORDER>=1 (enough for e.g. overlap/
1700 // kinetic) while this specific operator (nuclear attraction with
1701 // point-charge derivatives) was never actually generated, in which
1702 // case engine.results() silently returns null buffers for EVERY
1703 // shell pair rather than throwing -- exactly the failure mode a CI
1704 // run against such a libint2 hit: this function ran to completion
1705 // and returned an all-zero matrix, not an exception, which a
1706 // finite-difference test then correctly reports as a large,
1707 // confusing discrepancy rather than a clear "not supported"
1708 // message. Catching it here, at the one place that already knows
1709 // whether a single buffer was ever populated across the entire
1710 // computation, turns that into an explicit, actionable error.
1711 throw std::runtime_error(
1712 "ComputeNuclearAttractionDerivatives: engine.results() returned "
1713 "a null buffer for EVERY shell pair and point charge -- this "
1714 "libint2 build does not actually support nuclear-attraction "
1715 "derivative integrals at runtime, even though it may report "
1716 "LIBINT2_MAX_DERIV_ORDER >= 1 for other operators. Rebuild "
1717 "libint2 with this operator's derivative support enabled "
1718 "(--enable-1body=1, which nuclear attraction is part of) to "
1719 "use this feature.");
1720 }
1721
1722 // SECOND, COMPLEMENTARY CHECK: the check above only catches every
1723 // buffer coming back NULL. A real CI run on a different architecture
1724 // showed that check did NOT fire, yet the assembled result was still
1725 // exactly, precisely all-zero -- some libint2 builds apparently
1726 // return valid, non-null buffer POINTERS for this operator, but the
1727 // underlying computation for it was never actually generated, so the
1728 // pointed-to memory is zero-filled rather than containing a real
1729 // result. A genuine nuclear attraction derivative for any real
1730 // molecule with nonzero nuclear charges and a non-empty basis can
1731 // never be exactly zero everywhere.
1732 double total_norm_sq_nucattr = 0.0;
1733 for (Index a = 0; a < natoms; ++a) {
1734 for (Index xyz = 0; xyz < 3; ++xyz) {
1735 for (Index t = 0; t < nthreads; ++t) {
1736 total_norm_sq_nucattr += result_thread[t][a][xyz].squaredNorm();
1737 }
1738 }
1739 }
1740 if (total_norm_sq_nucattr < 1.e-20 && natoms > 0 &&
1741 aobasis.getNumofShells() > 0) {
1742 throw std::runtime_error(
1743 "ComputeNuclearAttractionDerivatives: the assembled result is "
1744 "exactly zero everywhere, which is physically impossible for a "
1745 "real molecule -- this libint2 build likely returns valid but "
1746 "zero-filled buffers for this operator (rather than either "
1747 "computing it correctly or returning null, which the separate, "
1748 "earlier check in this function already handles). Rebuild "
1749 "libint2 with this operator's derivative support enabled "
1750 "(--enable-1body=1) to use this feature.");
1751 }
1752
1753 std::vector<AOMatrixDerivative> result(natoms);
1754 for (Index a = 0; a < natoms; ++a) {
1755 for (Index xyz = 0; xyz < 3; ++xyz) {
1756 result[a][xyz] = Eigen::MatrixXd::Zero(nbf, nbf);
1757 for (Index t = 0; t < nthreads; ++t) {
1758 result[a][xyz] += result_thread[t][a][xyz];
1759 }
1760 }
1761 }
1762 return result;
1763}
1764#else // !(LIBINT_INCLUDE_ONEBODY)
1765std::vector<AOMatrixDerivative> ComputeNuclearAttractionDerivatives(
1766 const AOBasis&, const QMMolecule&) {
1767 ThrowNoDerivativeSupport("ComputeNuclearAttractionDerivatives",
1768 "--enable-1body=1");
1769}
1770#endif // LIBINT_INCLUDE_ONEBODY
1771
1772// Small, deliberately libint2-header-free helper: lets other files (e.g.
1773// dftengine.cc, which does not otherwise include any libint2 header at
1774// all) check whether this build's linked libint2 supports derivative
1775// integrals, WITHOUT needing to include <libint2.hpp> themselves just
1776// for this one macro. Used by DFTEngine::ComputeAndStoreForces(UKS) to
1777// skip cleanly (log and return) rather than ever calling the throwing
1778// stubs above at all.
1780 // Unlike the per-function compile guards above (each checking only
1781 // its OWN specific category), this helper answers "can the full
1782 // forces pipeline run at all" -- DFTEngine::ComputeAndStoreForces
1783 // (UKS) unconditionally needs ComputeOverlapDerivatives/
1784 // ComputeKineticDerivatives/ComputeNuclearAttractionDerivatives
1785 // (ONEBODY), DFTGradient::RIJGradient/RIKGradient's own calls into
1786 // ComputeCoulombMetricDerivatives (ERI2), AND their calls into
1787 // ComputeThreeCenterDerivatives (ERI3, confirmed used at two call
1788 // sites in dftgradient.cc) -- so all three, independent categories
1789 // must be available, not just one.
1790#if defined(LIBINT2_MAX_DERIV_ORDER) && LIBINT2_MAX_DERIV_ORDER >= 1 && \
1791 (LIBINT_INCLUDE_ONEBODY >= 1) && (LIBINT_INCLUDE_ERI2 >= 1) && \
1792 (LIBINT_INCLUDE_ERI3 >= 1)
1793 return true;
1794#else
1795 return false;
1796#endif
1797}
1798
1799} // namespace xtp
1800} // namespace votca
Container to hold Basisfunctions for all atoms.
Definition aobasis.h:42
Index getMaxL() const
Definition aobasis.cc:29
Index AOBasisSize() const
Definition aobasis.h:46
std::vector< Index > getMapToBasisFunctions() const
Definition aobasis.cc:45
Index getNumofShells() const
Definition aobasis.h:56
Index getMaxNprim() const
Definition aobasis.cc:37
std::vector< libint2::Shell > GenerateLibintBasis() const
Definition aobasis.cc:119
Index getMaxThreads()
Definition eigen.h:128
Index getThreadId()
Definition eigen.h:143
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor > MatrixLibInt
Definition aoecp.cc:56
std::vector< AOMatrixDerivative > ComputeNuclearAttractionDerivatives(const AOBasis &aobasis, const QMMolecule &mol)
std::vector< AOMatrixDerivative > ComputeKineticDerivatives(const AOBasis &aobasis)
std::vector< AOMatrixDerivative > ComputeCoulombMetricDerivatives(const AOBasis &aobasis)
std::vector< AOMatrixDerivative > ComputeOverlapDerivatives(const AOBasis &aobasis)
std::vector< ThreeCenterDerivative > ComputeThreeCenterDerivatives(const AOBasis &auxbasis, const AOBasis &dftbasis)
std::vector< ThreeCenterDerivative > ComputeThreeCenterDerivativesMOTransformed(const AOBasis &auxbasis, const AOBasis &dftbasis, const Eigen::MatrixXd &occ_mo_coeffs)
std::vector< Eigen::MatrixXd > ComputeThreeCenterIntegrals(const AOBasis &auxbasis, const AOBasis &dftbasis)
std::vector< Eigen::MatrixXd > ComputeAO3cBlock(const libint2::Shell &auxshell, const AOBasis &dftbasis, libint2::Engine &engine)
std::vector< Eigen::MatrixXd > ComputeThreeCenterDerivativeContraction(const AOBasis &auxbasis, const AOBasis &dftbasis, const Eigen::MatrixXd &density)
std::array< std::vector< Eigen::MatrixXd >, 3 > ThreeCenterDerivative
std::array< Eigen::MatrixXd, 3 > AOMatrixDerivative
Definition dftengine.cc:413
ThreeCenterDerivative ComputeThreeCenterDerivativesForAtom(const AOBasis &auxbasis, const AOBasis &dftbasis, Index target_atom)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26