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// Energy-level (deriv_order=0) three-center integral (mu,nu|P), kept here
871// (rather than in dftgradient.cc) so all direct libint2 API usage stays
872// confined to this file and libint2_calls.cc -- dftgradient.cc, which
873// needs this for the RI-J gradient assembly, consumes only the returned
874// matrices, never touching libint2 types directly. Structurally the same
875// shell-loop as ComputeThreeCenterDerivatives above, minus the
876// atom/Cartesian bookkeeping that only applies at deriv_order=1, and
877// reusing the already-existing ComputeAO3cBlock helper from
878// libint2_calls.cc (declared there, not in any header -- forward
879// declared here) rather than re-deriving the same shell-iteration logic
880// a third time. UNGUARDED (unlike the functions above): deriv_order=0
881// integrals are always supported, by any libint2 build.
882std::vector<Eigen::MatrixXd> ComputeThreeCenterIntegrals(
883 const AOBasis& auxbasis, const AOBasis& dftbasis) {
884 std::vector<libint2::Shell> auxshells = auxbasis.GenerateLibintBasis();
885 std::vector<Index> auxshell2bf = auxbasis.getMapToBasisFunctions();
886
887 std::vector<Eigen::MatrixXd> tensor(
888 auxbasis.AOBasisSize(),
889 Eigen::MatrixXd::Zero(dftbasis.AOBasisSize(), dftbasis.AOBasisSize()));
890
891 libint2::Engine engine(
892 libint2::Operator::coulomb,
893 std::max(dftbasis.getMaxNprim(), auxbasis.getMaxNprim()),
894 static_cast<int>(std::max(dftbasis.getMaxL(), auxbasis.getMaxL())), 0);
895 engine.set(libint2::BraKet::xs_xx);
896
897 for (Index aux = 0; aux < auxbasis.getNumofShells(); ++aux) {
898 std::vector<Eigen::MatrixXd> block =
899 ComputeAO3cBlock(auxshells[aux], dftbasis, engine);
900 Index aux_start = auxshell2bf[aux];
901 for (size_t i = 0; i < block.size(); ++i) {
902 tensor[aux_start + static_cast<Index>(i)] = block[i];
903 }
904 }
905 return tensor;
906}
907
908// ===========================================================================
909// Nuclear attraction (electron-nucleus Coulomb) integral derivative --
910// d(V_ne)/dR, the piece discovered MISSING from the total gradient
911// assembly by the first genuine end-to-end SCF+forces test
912// (test_dftengine_forces.cc). Every earlier gradient test in this branch
913// validated individual terms against FIXED density matrices, never the
914// complete picture against a real total SCF energy -- which is exactly
915// why this gap went undetected until now: kinetic derivatives were
916// validated at the very start of this session and then never actually
917// used in any gradient assembly, and this nuclear attraction piece was
918// never implemented at all.
919//
920// V_ne_munu = -sum_A Z_A * integral[ chi_mu(r) (1/|r-R_A|) chi_nu(r) dr ]
921//
922// Genuinely a 3-real-center integral per point charge: the two basis
923// function centers, PLUS the point charge's own position -- structurally
924// identical to the already-validated 3-center RI derivative case (2
925// basis centers + 1 external center = 3 total), just with a different
926// operator (libint2::Operator::nuclear instead of coulomb/xs_xx) and
927// looping over EACH atom as a single point charge at a time (via
928// engine.set_params(make_point_charges(...)) per atom, matching the
929// pattern in libint2's own reference example --
930// compute_1body_ints_deriv<Operator::nuclear> in
931// libint2/include/libint2/lcao/1body.h, used directly to build real HF
932// forces there) rather than all atoms simultaneously.
933//
934// SIGN: this codebase's own existing, already-validated energy-level
935// code confirms V_ne (AOMultipole::FillPotential's output) is negative
936// (attractive) -- AOMultipole computes a POSITIVE-charge Fill(aobasis)
937// per atom then does `aopotential_ -= Fill(aobasis)`, and
938// DFTEngine::SetupH0 does `H0 = kinetic + dftAOESP` (a plain addition,
939// no extra negation by the caller). Initially reasoned (incorrectly)
940// that libint2::Operator::nuclear must therefore need an explicit
941// negation to match -- a finite-difference test (exact magnitude match,
942// opposite sign in every single element) confirmed the OPPOSITE:
943// libint2::Operator::nuclear already returns the attractive (negative)
944// convention directly, no negation needed. Fixed by using += throughout
945// rather than -=. Worth remembering as a concrete example of why this
946// branch insists on actually running things rather than trusting
947// plausible-sounding sign reasoning alone.
948//
949// STATUS: buffer count/ordering (9 buffers per shell-pair-per-point-
950// charge, [shell1 atom][shell2 atom][point-charge atom]) CONFIRMED
951// correct by the same finite-difference test -- only the sign was
952// wrong, now fixed. Re-run pending to confirm the fix.
953// ===========================================================================
954#if defined(LIBINT2_MAX_DERIV_ORDER) && LIBINT2_MAX_DERIV_ORDER >= 1 && \
955 (LIBINT_INCLUDE_ONEBODY >= 1)
956std::vector<AOMatrixDerivative> ComputeNuclearAttractionDerivatives(
957 const AOBasis& aobasis, const QMMolecule& mol) {
958 Index natoms = mol.size();
959 Index nthreads = OPENMP::getMaxThreads();
960 std::vector<libint2::Shell> shells = aobasis.GenerateLibintBasis();
961 std::vector<Index> shell2bf = aobasis.getMapToBasisFunctions();
962
963 std::vector<Index> shell2atom;
964 shell2atom.reserve(aobasis.getNumofShells());
965 for (Index s = 0; s < aobasis.getNumofShells(); ++s) {
966 shell2atom.push_back(aobasis.getShell(s).getAtomIndex());
967 }
968
969 Index nbf = aobasis.AOBasisSize();
970 std::vector<std::vector<AOMatrixDerivative>> result_thread(nthreads);
971 for (Index t = 0; t < nthreads; ++t) {
972 result_thread[t].resize(natoms);
973 for (Index a = 0; a < natoms; ++a) {
974 for (Index xyz = 0; xyz < 3; ++xyz) {
975 result_thread[t][a][xyz] = Eigen::MatrixXd::Zero(nbf, nbf);
976 }
977 }
978 }
979
980 std::vector<libint2::Engine> engines(nthreads);
981 for (Index i = 0; i < nthreads; ++i) {
982 engines[i] =
983 libint2::Engine(libint2::Operator::nuclear, aobasis.getMaxNprim(),
984 static_cast<int>(aobasis.getMaxL()), 1);
985 }
986
987 // Parallelized over the OUTER atom (point-charge) loop, not the inner
988 // shell-pair loop as in every other function in this file -- each
989 // atom needs its own engine.set_params() call (mutating shared engine
990 // state), so a per-thread engine set up fresh per atom is the natural
991 // parallelization axis here, not shell pairs within one fixed engine
992 // configuration.
993 std::exception_ptr eptr_nucattr = nullptr;
994 std::atomic<bool> any_nonnull_buffer_nucattr{false};
995#pragma omp parallel for schedule(dynamic)
996 for (Index A = 0; A < natoms; ++A) {
997 try {
998 Index thread_id = OPENMP::getThreadId();
999 libint2::Engine& engine = engines[thread_id];
1000
1001 std::vector<libint2::Atom> single_atom(1);
1002 single_atom[0].atomic_number = static_cast<int>(mol[A].getNuccharge());
1003 single_atom[0].x = mol[A].getPos().x();
1004 single_atom[0].y = mol[A].getPos().y();
1005 single_atom[0].z = mol[A].getPos().z();
1006 engine.set_params(libint2::make_point_charges(single_atom));
1007
1008 const libint2::Engine::target_ptr_vec& buf = engine.results();
1009
1010 for (Index s1 = 0; s1 < aobasis.getNumofShells(); ++s1) {
1011 Index bf1 = shell2bf[s1];
1012 Index n1 = shells[s1].size();
1013 Index atom1 = shell2atom[s1];
1014
1015 for (Index s2 = 0; s2 <= s1; ++s2) {
1016 engine.compute(shells[s1], shells[s2]);
1017
1018 // See the detailed explanation on the analogous check in
1019 // computeOneBodyIntegralDerivatives above -- this operator
1020 // dereferences buf[3+xyz] AND buf[6+xyz] unconditionally below
1021 // too (3 real centers: shell1's atom, shell2's atom, the point
1022 // charge), so both need checking here. This exact gap is what
1023 // produced BOTH observed failure modes on different CI
1024 // architectures for this specific function: a hard segfault
1025 // (buf[3] or buf[6] actually null) on one, and a silently
1026 // all-zero result (buf[3]/buf[6] non-null but zero-filled,
1027 // caught instead by the separate norm check further below) on
1028 // another -- same underlying root cause, different libint2
1029 // build behavior for an unsupported operator.
1030 if (buf[0] == nullptr || buf[3] == nullptr || buf[6] == nullptr) {
1031 continue;
1032 }
1033 any_nonnull_buffer_nucattr.store(true, std::memory_order_relaxed);
1034
1035 Index bf2 = shell2bf[s2];
1036 Index n2 = shells[s2].size();
1037 Index atom2 = shell2atom[s2];
1038
1039 for (Index xyz = 0; xyz < 3; ++xyz) {
1040 // SIGN FIX: confirmed via finite-difference test (exact
1041 // magnitude match, opposite sign everywhere) that
1042 // libint2::Operator::nuclear already returns the attractive
1043 // (negative) V_ne convention directly -- the explicit
1044 // negation originally here (reasoned from AOMultipole's own
1045 // "positive charge in, subtract" pattern) was backwards,
1046 // double-negating an already-correctly-signed quantity. Using
1047 // += throughout now, not -=.
1048 Eigen::Map<const MatrixLibInt> buf_mat1(buf[xyz], n1, n2);
1049 result_thread[thread_id][atom1][xyz].block(bf1, bf2, n1, n2) +=
1050 buf_mat1;
1051 if (s1 != s2) {
1052 result_thread[thread_id][atom1][xyz].block(bf2, bf1, n2, n1) +=
1053 buf_mat1.transpose();
1054 }
1055
1056 Eigen::Map<const MatrixLibInt> buf_mat2(buf[3 + xyz], n1, n2);
1057 result_thread[thread_id][atom2][xyz].block(bf1, bf2, n1, n2) +=
1058 buf_mat2;
1059 if (s1 != s2) {
1060 result_thread[thread_id][atom2][xyz].block(bf2, bf1, n2, n1) +=
1061 buf_mat2.transpose();
1062 }
1063
1064 Eigen::Map<const MatrixLibInt> buf_mat3(buf[6 + xyz], n1, n2);
1065 result_thread[thread_id][A][xyz].block(bf1, bf2, n1, n2) +=
1066 buf_mat3;
1067 if (s1 != s2) {
1068 result_thread[thread_id][A][xyz].block(bf2, bf1, n2, n1) +=
1069 buf_mat3.transpose();
1070 }
1071 }
1072 }
1073 }
1074 } catch (...) {
1075#pragma omp critical
1076 {
1077 if (!eptr_nucattr) {
1078 eptr_nucattr = std::current_exception();
1079 }
1080 }
1081 }
1082 }
1083 if (eptr_nucattr) {
1084 std::rethrow_exception(eptr_nucattr);
1085 }
1086 if (!any_nonnull_buffer_nucattr.load() && natoms > 0 &&
1087 aobasis.getNumofShells() > 0) {
1088 // Distinct from the LIBINT2_MAX_DERIV_ORDER compile-time guard
1089 // above: that macro is library-wide (the maximum derivative order
1090 // ANY operator supports), but individual operators can each have
1091 // their own, separate build configuration -- a libint2 build can
1092 // report LIBINT2_MAX_DERIV_ORDER>=1 (enough for e.g. overlap/
1093 // kinetic) while this specific operator (nuclear attraction with
1094 // point-charge derivatives) was never actually generated, in which
1095 // case engine.results() silently returns null buffers for EVERY
1096 // shell pair rather than throwing -- exactly the failure mode a CI
1097 // run against such a libint2 hit: this function ran to completion
1098 // and returned an all-zero matrix, not an exception, which a
1099 // finite-difference test then correctly reports as a large,
1100 // confusing discrepancy rather than a clear "not supported"
1101 // message. Catching it here, at the one place that already knows
1102 // whether a single buffer was ever populated across the entire
1103 // computation, turns that into an explicit, actionable error.
1104 throw std::runtime_error(
1105 "ComputeNuclearAttractionDerivatives: engine.results() returned "
1106 "a null buffer for EVERY shell pair and point charge -- this "
1107 "libint2 build does not actually support nuclear-attraction "
1108 "derivative integrals at runtime, even though it may report "
1109 "LIBINT2_MAX_DERIV_ORDER >= 1 for other operators. Rebuild "
1110 "libint2 with this operator's derivative support enabled "
1111 "(--enable-1body=1, which nuclear attraction is part of) to "
1112 "use this feature.");
1113 }
1114
1115 // SECOND, COMPLEMENTARY CHECK: the check above only catches every
1116 // buffer coming back NULL. A real CI run on a different architecture
1117 // showed that check did NOT fire, yet the assembled result was still
1118 // exactly, precisely all-zero -- some libint2 builds apparently
1119 // return valid, non-null buffer POINTERS for this operator, but the
1120 // underlying computation for it was never actually generated, so the
1121 // pointed-to memory is zero-filled rather than containing a real
1122 // result. A genuine nuclear attraction derivative for any real
1123 // molecule with nonzero nuclear charges and a non-empty basis can
1124 // never be exactly zero everywhere.
1125 double total_norm_sq_nucattr = 0.0;
1126 for (Index a = 0; a < natoms; ++a) {
1127 for (Index xyz = 0; xyz < 3; ++xyz) {
1128 for (Index t = 0; t < nthreads; ++t) {
1129 total_norm_sq_nucattr += result_thread[t][a][xyz].squaredNorm();
1130 }
1131 }
1132 }
1133 if (total_norm_sq_nucattr < 1.e-20 && natoms > 0 &&
1134 aobasis.getNumofShells() > 0) {
1135 throw std::runtime_error(
1136 "ComputeNuclearAttractionDerivatives: the assembled result is "
1137 "exactly zero everywhere, which is physically impossible for a "
1138 "real molecule -- this libint2 build likely returns valid but "
1139 "zero-filled buffers for this operator (rather than either "
1140 "computing it correctly or returning null, which the separate, "
1141 "earlier check in this function already handles). Rebuild "
1142 "libint2 with this operator's derivative support enabled "
1143 "(--enable-1body=1) to use this feature.");
1144 }
1145
1146 std::vector<AOMatrixDerivative> result(natoms);
1147 for (Index a = 0; a < natoms; ++a) {
1148 for (Index xyz = 0; xyz < 3; ++xyz) {
1149 result[a][xyz] = Eigen::MatrixXd::Zero(nbf, nbf);
1150 for (Index t = 0; t < nthreads; ++t) {
1151 result[a][xyz] += result_thread[t][a][xyz];
1152 }
1153 }
1154 }
1155 return result;
1156}
1157#else // !(LIBINT_INCLUDE_ONEBODY)
1158std::vector<AOMatrixDerivative> ComputeNuclearAttractionDerivatives(
1159 const AOBasis&, const QMMolecule&) {
1160 ThrowNoDerivativeSupport("ComputeNuclearAttractionDerivatives",
1161 "--enable-1body=1");
1162}
1163#endif // LIBINT_INCLUDE_ONEBODY
1164
1165// Small, deliberately libint2-header-free helper: lets other files (e.g.
1166// dftengine.cc, which does not otherwise include any libint2 header at
1167// all) check whether this build's linked libint2 supports derivative
1168// integrals, WITHOUT needing to include <libint2.hpp> themselves just
1169// for this one macro. Used by DFTEngine::ComputeAndStoreForces(UKS) to
1170// skip cleanly (log and return) rather than ever calling the throwing
1171// stubs above at all.
1173 // Unlike the per-function compile guards above (each checking only
1174 // its OWN specific category), this helper answers "can the full
1175 // forces pipeline run at all" -- DFTEngine::ComputeAndStoreForces
1176 // (UKS) unconditionally needs ComputeOverlapDerivatives/
1177 // ComputeKineticDerivatives/ComputeNuclearAttractionDerivatives
1178 // (ONEBODY), DFTGradient::RIJGradient/RIKGradient's own calls into
1179 // ComputeCoulombMetricDerivatives (ERI2), AND their calls into
1180 // ComputeThreeCenterDerivatives (ERI3, confirmed used at two call
1181 // sites in dftgradient.cc) -- so all three, independent categories
1182 // must be available, not just one.
1183#if defined(LIBINT2_MAX_DERIV_ORDER) && LIBINT2_MAX_DERIV_ORDER >= 1 && \
1184 (LIBINT_INCLUDE_ONEBODY >= 1) && (LIBINT_INCLUDE_ERI2 >= 1) && \
1185 (LIBINT_INCLUDE_ERI3 >= 1)
1186 return true;
1187#else
1188 return false;
1189#endif
1190}
1191
1192} // namespace xtp
1193} // 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< 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::array< std::vector< Eigen::MatrixXd >, 3 > ThreeCenterDerivative
std::array< Eigen::MatrixXd, 3 > AOMatrixDerivative
Definition dftengine.cc:409
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26