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