diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 719e10d26..e10e10a3c 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -45,6 +45,15 @@ jobs: build_shared_libs: ON os: macos-15 do_fortran: ON + # exercise a center-dependent AM table for the 3-center RKB ERIs and + # 2nd geometric derivatives of the 4-center ERIs in one lane only + - build_type: Release + os: macos-15 + subproject: OFF + rkb_eri3_max_am: 3 + eri_deriv_order: 2 + max_am: '2;2;1' + eri_max_am: '2;2;1' name: "Repo • ${{ matrix.os }}: ${{ matrix.cxx }} ${{ matrix.build_type }} shared=${{ matrix.build_shared_libs }} fetch=${{ matrix.subproject }}" runs-on: ${{ matrix.os }} @@ -61,11 +70,14 @@ jobs: -D BUILD_SHARED_LIBS=${{ matrix.build_shared_libs }} -D LIBINT2_BUILD_LIBRARY_AS_SUBPROJECT=${{ matrix.subproject }} -D CMAKE_INSTALL_PREFIX=${{github.workspace}}/build/library - -D LIBINT2_MAX_AM=2;2 - -D LIBINT2_ERI_MAX_AM=2;2 + -D LIBINT2_MAX_AM=${{ matrix.max_am || '2;2' }} + -D LIBINT2_ERI_MAX_AM=${{ matrix.eri_max_am || '2;2' }} -D LIBINT2_ERI3_MAX_AM=3;2 - -D LIBINT2_ENABLE_ERI=1 + -D LIBINT2_ENABLE_ERI=${{ matrix.eri_deriv_order || 1 }} -D LIBINT2_ENABLE_ERI3=1 + -D LIBINT2_ENABLE_RKB_ERI=0 + -D LIBINT2_ENABLE_RKB_ERI3=0 + -D LIBINT2_RKB_ERI3_MAX_AM=${{ matrix.rkb_eri3_max_am || 2 }} -D LIBINT2_ENABLE_ONEBODY=1 -D LIBINT2_DISABLE_ONEBODY_PROPERTY_DERIVS=${{ matrix.disable_onebody_property_derivs || 'ON' }} -D LIBINT2_MULTIPOLE_MAX_ORDER=2 diff --git a/CHANGES b/CHANGES index 3cbd46742..e76e390c8 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,37 @@ Following is a brief summary of changes made in each release of Libint. - unreleased: + - PR #399: relativistic restricted kinetic balance (RKB) integrals: 2-body Operator::coulomb_opop, + Operator::coulomb_op, Operator::opop_coulomb_opop, Operator::op_coulomb_op (enabled by + LIBINT2_ENABLE_RKB_ERI and LIBINT2_ENABLE_RKB_ERI3, off by default; no geometric derivatives yet) + and 1-body Operator::opemuop. N.B. Operator::opemuop is inserted after the other 1-body + operators, hence the numeric values of all 2-body Operator enumerators increase by 1. + Engine::compute() now throws Engine::missing_kernel if the library was not configured to + compute the requested operator/braket/derivative order (previously the result was undefined), + and std::invalid_argument if given a cartesian fitting shell with l > 1 when the library was + configured with LIBINT2_ERI3_PURE_SH (previously a Debug-only assert). The Libint compiler + now requires C++17; the generated library still requires C++11 only. + Other changes that affect all libraries, not just the RKB integrals: + - BraKet::xx_xs is now usable for 3-center energies (as the alias of BraKet::xs_xx with the + fitting function last); previously Engine::compute() aborted for it. + - the geometric-derivative kernels of all 2-body Coulomb integrals (ERI, ERI3, ERI2) are now + generated by DerivGaussV2 (comp_deriv_gauss_v2.h), which shares one function between all + shell sets with the same differentiated shell, instead of by CR_DerivGauss; the generated + code is validated against finite differences for every shell set in the unit tests. With + LIBINT2_ENABLE_GENERIC_CODE the generic kernel is used when the differentiated shell is + beyond LIBINT2_OPT_AM (previously: when the average AM of the shell set is). + - Engine::initialize() no longer asserts (in Debug builds only) that the library includes the + braket type; missing kernels are reported by Engine::missing_kernel from compute(). + - the validation of LIBINT2__MAX_AM in cmake/modules/int_am.cmake now actually runs + (it referred to undefined variables); a value of 0 is accepted, negative values are rejected. + - Engine::compute() throws Engine::lmax_exceeded in all builds if a shell exceeds the angular + momentum limit of its center (e.g. an AO shell beyond LIBINT2_MAX_AM given to a 3-center Engine + whose max_l is within LIBINT2_ERI3_MAX_AM); previously a Debug-only assert, and Release builds + dispatched the kernel of another shell set. + - Engine::set(BraKet) and Engine::set_max_l() rebuild the core integral evaluator, whose order + depends on the rank of the braket and on the maximum angular momentum. + - the Fortran ERI test and example and the hartree-fock++ test honor a lower angular momentum + limit of the derivative kernels (e.g. LIBINT2_MAX_AM="2;2;1"). - PR #447: TennoGmEval (Yukawa/Slater core integrals) fixed for U < 1e-7 at small T (#444) and no longer returns NaN for large U and T; the slow, thread-unsafe Gaussian-fit fallback is replaced by series expansions diff --git a/CMakeLists.txt b/CMakeLists.txt index 81c3bfebb..1324e6fa8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,19 @@ option_with_default(LIBINT2_ENABLE_ERI3 "Compile with support for up to N-th derivatives of 3-center electron repulsion integrals (-1 for OFF)" -1) option_with_default(LIBINT2_ENABLE_ERI2 "Compile with support for up to N-th derivatives of 2-center electron repulsion integrals (-1 for OFF)" -1) +option_with_default(LIBINT2_ENABLE_RKB_ERI + "Compile with support for up to N-th derivatives of relativistic restricted kinetic + balance (RKB) 4-center electron repulsion integrals (-1 for OFF). Only N=0 is currently supported." -1) +option_with_default(LIBINT2_ENABLE_RKB_ERI3 + "Compile with support for up to N-th derivatives of relativistic restricted kinetic + balance (RKB) 3-center electron repulsion integrals (-1 for OFF). + σ·p acts on the two paired (AO) centers; the unpaired/fitting center is a spectator. + Only N=0 is currently supported." -1) +foreach(_cls RKB_ERI;RKB_ERI3) + if (LIBINT2_ENABLE_${_cls} GREATER 0) + message(FATAL_ERROR "LIBINT2_ENABLE_${_cls}=${LIBINT2_ENABLE_${_cls}}: geometric derivatives of RKB integrals are not yet implemented; use 0 (or -1 for OFF)") + endif() +endforeach() option_with_default(LIBINT2_ENABLE_G12 "Compile with support for N-th derivatives of MP2-F12 energies with Gaussian factors (-1 for OFF)" -1) option_with_default(LIBINT2_ENABLE_G12DKH @@ -219,10 +232,25 @@ option_with_default(LIBINT2_ERI_OPT_AM "Optimize 4-center ERIs maximally for up to angular momentum N (N <= max-am). Can specify values for each derivative level as a semicolon-separated string (default: (max_am/2)+1)" -1) +option_with_default(LIBINT2_RKB_ERI_MAX_AM + "Support relativistic restricted kinetic balance (RKB) 4-center ERIs for Gaussians of angular momentum up to N. + Can specify values for each derivative level as a semicolon-separated string. (default: max_am)" -1) +option_with_default(LIBINT2_RKB_ERI_OPT_AM + "Optimize relativistic restricted kinetic balance (RKB) 4-center ERIs maximally for up to angular momentum N (N <= max-am). + Can specify values for each derivative level as a semicolon-separated string (default: (max_am/2)+1)" -1) + +option_with_default(LIBINT2_RKB_ERI3_MAX_AM + "Support relativistic restricted kinetic balance (RKB) 3-center ERIs for Gaussians of angular momentum up to N. + Can specify values for each derivative level as a semicolon-separated string. (default: max_am) + This sets the max AM of the density fitting (auxiliary/DF) basis on the single fitting center. The paired AO centers (on which σ·p acts) use LIBINT2_MAX_AM." -1) +option_with_default(LIBINT2_RKB_ERI3_OPT_AM + "Optimize relativistic restricted kinetic balance (RKB) 3-center ERIs maximally for up to angular momentum N (N <= max-am). + Can specify values for each derivative level as a semicolon-separated string (default: (max_am/2)+1)" -1) + option_with_default(LIBINT2_ERI3_MAX_AM "Support 3-center ERIs for Gaussians of angular momentum up to N. Can specify values for each derivative level as a semicolon-separated string. (default: max_am) - This option controls only the single fitting center. The paired centers use LIBINT2_MAX_AM." -1) + This sets the max AM of the density fitting (auxiliary/DF) basis on the single fitting center. The paired AO centers use LIBINT2_MAX_AM." -1) option_with_default(LIBINT2_ERI3_OPT_AM "Optimize 3-center ERIs maximally for up to angular momentum N (N <= max-am). Can specify values for each derivative level as a semicolon-separated string. (default: (max_am/2)+1)" -1) @@ -307,7 +335,8 @@ include(int_am) # see #445). Map unset/empty values to the -1 sentinel. foreach(_v MAX_AM OPT_AM ONEBODY_MAX_AM ONEBODY_OPT_AM ERI_MAX_AM ERI_OPT_AM ERI3_MAX_AM ERI3_OPT_AM - ERI2_MAX_AM ERI2_OPT_AM G12_MAX_AM G12_OPT_AM G12DKH_MAX_AM G12DKH_OPT_AM) + ERI2_MAX_AM ERI2_OPT_AM RKB_ERI_MAX_AM RKB_ERI_OPT_AM RKB_ERI3_MAX_AM RKB_ERI3_OPT_AM + G12_MAX_AM G12_OPT_AM G12DKH_MAX_AM G12DKH_OPT_AM) if ("${LIBINT_${_v}}" STREQUAL "") set(LIBINT_${_v} -1) endif() diff --git a/INSTALL.md b/INSTALL.md index 86bcb833e..d96fd8014 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -113,7 +113,7 @@ Use combined targets like `cmake --target check install` to avoid some unnecessa * `🔸` required or recommended, but there's a path forward without * `-` not involved -[^1]: C++ compiler that supports C++11 standard. C++11 standard is the fourth most recent international standard for C++, hence most modern compilers support it fully. A common compiler flag is `-std=c++11`, which CMake will impose on the compilation. +[^1]: C++ compiler that supports C++11 standard (the Libint compiler itself requires C++17). C++11 standard is the fourth most recent international standard for C++, hence most modern compilers support it fully. A common compiler flag is `-std=c++11`, which CMake will impose on the compilation. [^2]: Fortran 2003 compiler to enable Fortran bindings generation. @@ -169,6 +169,8 @@ Use combined targets like `cmake --target check install` to avoid some unnecessa * `LIBINT2_ENABLE_ERI` - G - Compile with support for up to N-th derivatives of 4-center electron repulsion integrals. Use -1 for OFF. [Default=0] * `LIBINT2_ENABLE_ERI3` - G - Compile with support for up to N-th derivatives of 3-center electron repulsion integrals. Use -1 for OFF. [Default=-1] * `LIBINT2_ENABLE_ERI2` - G - Compile with support for up to N-th derivatives of 2-center electron repulsion integrals. Use -1 for OFF. [Default=-1] +* `LIBINT2_ENABLE_RKB_ERI` - G - Compile with support for up to N-th derivatives of relativistic restricted kinetic balance (RKB) 4-center electron repulsion integrals. Use -1 for OFF. Derivatives are not yet implemented, i.e. only N=0 is supported. [Default=-1] +* `LIBINT2_ENABLE_RKB_ERI3` - G - Compile with support for up to N-th derivatives of relativistic restricted kinetic balance (RKB) 3-center electron repulsion integrals. Use -1 for OFF. σ·p acts on the two paired (AO) centers; the unpaired/fitting (density-fitting/auxiliary) center is a spectator. Derivatives are not yet implemented, i.e. only N=0 is supported. [Default=-1] * `LIBINT2_ENABLE_G12` - G - Compile with support for N-th derivatives of MP2-F12 energies with Gaussian factors. Use -1 for OFF. [Default=-1] * `LIBINT2_ENABLE_G12DKH` - G - Compile with support for N-th derivatives of DKH-MP2-F12 energies with Gaussian factors. Use -1 for OFF. [Default=-1] @@ -255,6 +257,12 @@ Note that options, docs, and CMake components are focused on the C++ interface, * `LIBINT2_ERI2_OPT_AM` - G - Optimize 2-center ERIs maximally for up to angular momentum N (N <= max-am). Can specify values for each derivative level as a semicolon-separated string. [Default=-1 -> `LIBINT2_OPT_AM`] * `LIBINT2_ERI2_PURE_SH` - G - Assume the 2-center ERIs will be transformed to pure solid harmonics. [Default=OFF] +* `LIBINT2_RKB_ERI_MAX_AM` - G - Support RKB 4-center ERIs for Gaussians of angular momentum up to N. Can specify values for each derivative level as a semicolon-separated string. [Default=-1 -> `LIBINT2_MAX_AM`] +* `LIBINT2_RKB_ERI_OPT_AM` - G - Optimize RKB 4-center ERIs maximally for up to angular momentum N (N <= max-am). Can specify values for each derivative level as a semicolon-separated string. [Default=-1 -> `LIBINT2_OPT_AM`] + +* `LIBINT2_RKB_ERI3_MAX_AM` - G - Support RKB 3-center ERIs for Gaussians of angular momentum up to N. Can specify values for each derivative level as a semicolon-separated string. This option sets the AM of the density-fitting (auxiliary) basis on the single fitting center; the paired (AO) centers, on which σ·p acts, use LIBINT2_MAX_AM. [Default=-1 -> `LIBINT2_MAX_AM`] +* `LIBINT2_RKB_ERI3_OPT_AM` - G - Optimize RKB 3-center ERIs maximally for up to angular momentum N (N <= max-am). Can specify values for each derivative level as a semicolon-separated string. [Default=-1 -> `LIBINT2_OPT_AM`] + * `LIBINT2_G12_MAX_AM` - G - Support integrals for G12 methods of angular momentum up to N. No specification with per-derivative list. [Default=-1 -> `LIBINT2_MAX_AM`] * `LIBINT2_G12_OPT_AM` - G - Optimize G12 integrals for up to angular momentum N (N <= max-am). No specification with per-derivative list. [Default=-1 `LIBINT2_OPT_AM`] diff --git a/cmake/modules/int_am.cmake b/cmake/modules/int_am.cmake index c1d61cd55..cabcc18bd 100644 --- a/cmake/modules/int_am.cmake +++ b/cmake/modules/int_am.cmake @@ -61,7 +61,7 @@ message(STATUS "Processing integrals classes ...") # <<< overall derivatives level >>> -set(_glob_classes_derivs ${LIBINT2_ENABLE_ONEBODY};${LIBINT2_ENABLE_ERI};${LIBINT2_ENABLE_ERI3};${LIBINT2_ENABLE_ERI2};${LIBINT2_ENABLE_G12};${LIBINT2_ENABLE_G12DKH}) +set(_glob_classes_derivs ${LIBINT2_ENABLE_ONEBODY};${LIBINT2_ENABLE_ERI};${LIBINT2_ENABLE_ERI3};${LIBINT2_ENABLE_ERI2};${LIBINT2_ENABLE_RKB_ERI};${LIBINT2_ENABLE_RKB_ERI3};${LIBINT2_ENABLE_G12};${LIBINT2_ENABLE_G12DKH}) numerical_max_of_list(_max_deriv "${_glob_classes_derivs}") message(STATUS "Preparing highest derivative level ${_max_deriv}") @@ -231,14 +231,14 @@ macro(process_integrals_class class) set(LIBINT_${class}_MAX_AM ${LIBINT2_${class}_MAX_AM}) foreach(_d RANGE ${LIBINT_INCLUDE_${class}}) - set(_candidate_${class}_d${_d} ${${class}_MAX_AM}) + set(_candidate_${class}_d${_d} ${LIBINT_${class}_MAX_AM}) message(VERBOSE "setting _candidate_${class}_d${_d}=${_candidate_${class}_d${_d}}") endforeach() - if (${class}_MAX_AM GREATER_EQUAL ${LIBINT_HARD_MAX_AM}) - message(WARNING "Value for ${class}_MAX_AM too high (${${class}_MAX_AM} >= ${LIBINT_HARD_MAX_AM}). Are you sure you know what you are doing?") - elseif (${class}_MAX_AM LESS_EQUAL 0) - message(FATAL_ERROR "Invalid value for ${class}_MAX_AM (${${class}_MAX_AM} <= 0).") + if (LIBINT_${class}_MAX_AM GREATER_EQUAL ${LIBINT_HARD_MAX_AM}) + message(WARNING "Value for LIBINT2_${class}_MAX_AM too high (${LIBINT_${class}_MAX_AM} >= ${LIBINT_HARD_MAX_AM}). Are you sure you know what you are doing?") + elseif (LIBINT_${class}_MAX_AM LESS 0) + message(FATAL_ERROR "Invalid value for LIBINT2_${class}_MAX_AM (${LIBINT_${class}_MAX_AM} < 0).") endif() endif() endif() @@ -247,7 +247,7 @@ macro(process_integrals_class class) else() set(_msg ${LIBINT_MAX_AM}) endif() - message(STATUS "Enabling integrals class ${class} to max AM ${${class}_MAX_AM}${${class}_MAX_AM_LIST} (else ${_msg})") + message(STATUS "Enabling integrals class ${class} to max AM ${LIBINT_${class}_MAX_AM}${LIBINT_${class}_MAX_AM_LIST} (else ${_msg})") list(LENGTH LIBINT2_${class}_OPT_AM _ntokens) if (_ntokens GREATER 1) @@ -262,7 +262,7 @@ macro(process_integrals_class class) if (LIBINT2_${class}_OPT_AM EQUAL -1) set(LIBINT_${class}_OPT_AM "") else() - set($LIBINT_{class}_OPT_AM ${LIBINT2_${class}_OPT_AM}) + set(LIBINT_${class}_OPT_AM ${LIBINT2_${class}_OPT_AM}) endif() endif() if (LIBINT_OPT_AM_LIST) @@ -270,7 +270,7 @@ macro(process_integrals_class class) else() set(_msg ${LIBINT_OPT_AM}) endif() - message(STATUS "Enabling integrals class ${class} to opt AM ${${class}_OPT_AM}${${class}_OPT_AM_LIST} (else ${_msg})") + message(STATUS "Enabling integrals class ${class} to opt AM ${LIBINT_${class}_OPT_AM}${LIBINT_${class}_OPT_AM_LIST} (else ${_msg})") endif() endmacro() @@ -323,14 +323,14 @@ macro(process_integrals_class_alt class) set(LIBINT_${class}_MAX_AM ${LIBINT2_${class}_MAX_AM}) foreach(_d RANGE ${LIBINT_INCLUDE_${class}}) - set(_candidate_${class}_d${_d} ${${class}_MAX_AM}) + set(_candidate_${class}_d${_d} ${LIBINT_${class}_MAX_AM}) message(VERBOSE "setting _candidate_${class}_d${_d}=${_candidate_${class}_d${_d}}") endforeach() - if (${class}_MAX_AM GREATER_EQUAL ${LIBINT_HARD_MAX_AM}) - message(WARNING "Value for ${class}_MAX_AM too high (${${class}_MAX_AM} >= ${LIBINT_HARD_MAX_AM}). Are you sure you know what you are doing?") - elseif (${class}_MAX_AM LESS_EQUAL 0) - message(FATAL_ERROR "Invalid value for ${class}_MAX_AM (${${class}_MAX_AM} <= 0).") + if (LIBINT_${class}_MAX_AM GREATER_EQUAL ${LIBINT_HARD_MAX_AM}) + message(WARNING "Value for LIBINT2_${class}_MAX_AM too high (${LIBINT_${class}_MAX_AM} >= ${LIBINT_HARD_MAX_AM}). Are you sure you know what you are doing?") + elseif (LIBINT_${class}_MAX_AM LESS 0) + message(FATAL_ERROR "Invalid value for LIBINT2_${class}_MAX_AM (${LIBINT_${class}_MAX_AM} < 0).") endif() endif() endif() @@ -357,6 +357,8 @@ endmacro() process_integrals_class(ONEBODY) process_integrals_class(ERI) +process_integrals_class(RKB_ERI) +process_integrals_class(RKB_ERI3) process_integrals_class(ERI3) process_integrals_class(ERI2) # unlike above, these classes (1) don't do AM_LIST and (2) require value in config.h if enabled @@ -380,6 +382,7 @@ add_feature_info( set(Libint2_ERI_COMPONENTS "") set(_amlist "") set(_eri3_impure_sh "") +set(_rkb_eri3_components "") set(_eri2_impure_sh "") add_feature_info( "integral class MULTIPOLE derivative 0" @@ -396,7 +399,7 @@ list(REVERSE _amlist) list(APPEND Libint2_ERI_COMPONENTS "${_amlist}") message(VERBOSE "setting components ${_amlist}") -foreach(_cls ONEBODY;ERI;ERI3;ERI2;G12;G12DKH) +foreach(_cls ONEBODY;ERI;RKB_ERI;RKB_ERI3;ERI3;ERI2;G12;G12DKH) if((_cls STREQUAL G12) OR (_cls STREQUAL G12DKH)) add_feature_info( "integral class ${_cls}" @@ -433,6 +436,8 @@ foreach(_cls ONEBODY;ERI;ERI3;ERI2;G12;G12DKH) list(APPEND _amlist "onebody_${_am${_l}}${_am${_l}}_d${_d}") elseif (_cls STREQUAL "G12") list(APPEND _amlist "g12_${_am${_l}}${_am${_l}}${_am${_l}}${_am${_l}}_d${_d}") + elseif (_cls STREQUAL "RKB_ERI") + list(APPEND _amlist "rkberi_${_am${_l}}${_am${_l}}${_am${_l}}${_am${_l}}_d${_d}") endif() endforeach() if (_cls STREQUAL "ERI3") @@ -445,6 +450,19 @@ foreach(_cls ONEBODY;ERI;ERI3;ERI2;G12;G12DKH) endforeach() endforeach() endif() + if (_cls STREQUAL "RKB_ERI3") + # Mirror ERI3 component naming: fitting (single) center on bra, + # paired (AO) centers on ket where σ·p acts. Paired-center AM + # tracks LIBINT_MAX_AM via _eri3_candidate0_d${_d}. + foreach(_lfit RANGE ${LIBINT_HARD_MIN_AM} ${_candidate_${_cls}_d${_d}}) # LIBINT_RKB_ERI3_MAX_AM[_LIST], fitting + foreach(_lpr RANGE ${LIBINT_HARD_MIN_AM} ${_eri3_candidate0_d${_d}}) # LIBINT_MAX_AM[_LIST], paired + if (_lfit GREATER_EQUAL _lpr) + list(APPEND _amlist "rkberi_${_am${_lpr}}${_am${_lpr}}${_AM${_lfit}}_d${_d}") + list(APPEND _pureamlist "rkberi_${_am${_lpr}}${_am${_lpr}}${_am${_lfit}}_d${_d}") + endif() + endforeach() + endforeach() + endif() list(REVERSE _amlist) list(APPEND Libint2_ERI_COMPONENTS "${_amlist}") message(VERBOSE "setting components ${_amlist}") @@ -453,11 +471,16 @@ foreach(_cls ONEBODY;ERI;ERI3;ERI2;G12;G12DKH) list(APPEND _eri2_impure_sh "${_pureamlist}") elseif (_cls STREQUAL "ERI3") list(APPEND _eri3_impure_sh "${_pureamlist}") + elseif (_cls STREQUAL "RKB_ERI3") + list(APPEND _rkb_eri3_components "${_pureamlist}") endif() endforeach() if ((_cls STREQUAL "ERI3") AND NOT LIBINT2_ERI3_PURE_SH) list(APPEND Libint2_ERI_COMPONENTS "${_eri3_impure_sh}") message(VERBOSE "setting components ${_eri3_impure_sh}") + elseif (_cls STREQUAL "RKB_ERI3") # RKB 3-center kernels never assume a solid harmonics fitting center + list(APPEND Libint2_ERI_COMPONENTS "${_rkb_eri3_components}") + message(VERBOSE "setting components ${_rkb_eri3_components}") elseif ((_cls STREQUAL "ERI2") AND NOT LIBINT2_ERI2_PURE_SH) list(APPEND Libint2_ERI_COMPONENTS "${_eri2_impure_sh}") message(VERBOSE "setting components ${_eri2_impure_sh}") diff --git a/export/fortran/fortran_example.F90 b/export/fortran/fortran_example.F90 index 25689a8b2..7343254a4 100644 --- a/export/fortran/fortran_example.F90 +++ b/export/fortran/fortran_example.F90 @@ -10,10 +10,10 @@ PROGRAM fortran_example #ifdef LIBINT_INCLUDE_ERI USE libint_f, ONLY: libint2_init_eri, libint2_cleanup_eri #if LIBINT_INCLUDE_ERI >= 1 - USE libint_f, ONLY: libint2_init_eri1, libint2_cleanup_eri1 + USE libint_f, ONLY: libint2_init_eri1, libint2_cleanup_eri1, libint2_max_am_eri1 #endif #if LIBINT_INCLUDE_ERI >= 2 - USE libint_f, ONLY: libint2_init_eri2, libint2_cleanup_eri2 + USE libint_f, ONLY: libint2_init_eri2, libint2_cleanup_eri2, libint2_max_am_eri2 #endif #endif @@ -68,7 +68,14 @@ PROGRAM fortran_example CALL libint2_cleanup_eri(erieval) #if LIBINT_INCLUDE_ERI >= 1 + ! the derivative kernels may stop at a lower angular momentum than the + ! energy kernels (e.g. LIBINT2_MAX_AM="2;2;1"): clamp the shells to it deriv_order = 1 + am1 = MINVAL([1, libint2_max_am_eri1]) + am2 = MINVAL([0, libint2_max_am_eri1]) + am3 = MINVAL([2, libint2_max_am_eri1]) + am4 = MINVAL([0, libint2_max_am_eri1]) + max_am = MAXVAL([am1, am2, am3, am4]) CALL libint2_init_eri1(erieval, max_am, C_NULL_PTR) CALL compute_eri_f(1, deriv_order, am1, c1, alpha1, A, & am2, c2, alpha2, B, & @@ -80,6 +87,11 @@ PROGRAM fortran_example #endif #if LIBINT_INCLUDE_ERI >= 2 deriv_order = 2 + am1 = MINVAL([1, libint2_max_am_eri2]) + am2 = MINVAL([0, libint2_max_am_eri2]) + am3 = MINVAL([2, libint2_max_am_eri2]) + am4 = MINVAL([0, libint2_max_am_eri2]) + max_am = MAXVAL([am1, am2, am3, am4]) CALL libint2_init_eri2(erieval, max_am, C_NULL_PTR) CALL compute_eri_f(1, deriv_order, am1, c1, alpha1, A, & am2, c2, alpha2, B, & diff --git a/export/fortran/test-eri.cc b/export/fortran/test-eri.cc index d25381474..50cc86306 100644 --- a/export/fortran/test-eri.cc +++ b/export/fortran/test-eri.cc @@ -580,23 +580,24 @@ TEST_CASE("Fortran ERI", "[eri]") { double c4[] = {rsqset.coef[3][0][0], rsqset.coef[3][0][1]}; #ifdef LIBINT_INCLUDE_ERI - int deriv_order = 0; - REQUIRE(test_eri_c_f(contrdepth, am1, c1, alpha1, A, am2, c2, alpha2, B, am3, - c3, alpha3, C, am4, c4, alpha4, D, deriv_order, - threshold)); - + // the derivative kernels may stop at a lower angular momentum than the + // energy kernels (e.g. LIBINT2_MAX_AM="2;2;1"): clamp the shells of each + // derivative order to the limit of its kernels + auto test_deriv_order = [&](int deriv_order, int lmax) { + int am1_d = std::min(am1, lmax); + int am2_d = std::min(am2, lmax); + int am3_d = std::min(am3, lmax); + int am4_d = std::min(am4, lmax); + return test_eri_c_f(contrdepth, am1_d, c1, alpha1, A, am2_d, c2, alpha2, B, + am3_d, c3, alpha3, C, am4_d, c4, alpha4, D, deriv_order, + threshold); + }; + REQUIRE(test_deriv_order(0, LIBINT2_MAX_AM_eri)); #if LIBINT_INCLUDE_ERI >= 1 - deriv_order = 1; - REQUIRE(test_eri_c_f(contrdepth, am1, c1, alpha1, A, am2, c2, alpha2, B, am3, - c3, alpha3, C, am4, c4, alpha4, D, deriv_order, - threshold)); + REQUIRE(test_deriv_order(1, LIBINT2_MAX_AM_eri1)); #endif - #if LIBINT_INCLUDE_ERI >= 2 - deriv_order = 2; - REQUIRE(test_eri_c_f(contrdepth, am1, c1, alpha1, A, am2, c2, alpha2, B, am3, - c3, alpha3, C, am4, c4, alpha4, D, deriv_order, - threshold)); + REQUIRE(test_deriv_order(2, LIBINT2_MAX_AM_eri2)); #endif #endif // LIBINT_INCLUDE_ERI } diff --git a/export/tests/CMakeLists.txt b/export/tests/CMakeLists.txt index f5a24c5a2..94a0b74f5 100644 --- a/export/tests/CMakeLists.txt +++ b/export/tests/CMakeLists.txt @@ -150,6 +150,8 @@ if (LIBINT2_REQUIRE_CXX_API) FIXTURES_REQUIRED LIBINT2_UNIT_TESTS_EXEC ) + # the Hartree-Fock examples need 4-center Coulomb integrals + if (NOT "${Libint2_MAX_AM_ERI}" STREQUAL "") add_executable( hf-libint2 EXCLUDE_FROM_ALL @@ -240,6 +242,7 @@ if (LIBINT2_REQUIRE_CXX_API) PROPERTIES FIXTURES_REQUIRED LIBINT2_HFXXTEST_EXEC ) + endif() # 4-center ERI endif (LIBINT2_REQUIRE_CXX_API) diff --git a/export/tests/hartree-fock/hartree-fock++-validate.py b/export/tests/hartree-fock/hartree-fock++-validate.py index 030525dc5..95be09631 100644 --- a/export/tests/hartree-fock/hartree-fock++-validate.py +++ b/export/tests/hartree-fock/hartree-fock++-validate.py @@ -137,6 +137,13 @@ def validate(label, data, refdata, tolerance, textline): match13 = re.match(r'\*\* Hartree-Fock hessian =' + pat_numbers(45), line) match14 = re.match(r'\*\* sph edipole =' + pat_numbers(3), line) match15 = re.match(r'\*\* sph equadrupole =' + pat_numbers(5), line) + # the derivative ERI kernels of the library may stop below the AM of the + # basis (e.g. LIBINT2_MAX_AM="2;2;1"); hartree-fock++ then says so instead + # of printing the quantity, and there is nothing to validate + match16 = re.match(r'\*\* 2-body forces skipped', line) + match17 = re.match(r'\*\* Hartree-Fock forces skipped', line) + match18 = re.match(r'\*\* 2-body hessian skipped', line) + match19 = re.match(r'\*\* Hartree-Fock hessian skipped', line) if match1: eok = validate("HF energy", match1.groups(), eref, etol, line) elif match2: @@ -167,6 +174,18 @@ def validate(label, data, refdata, tolerance, textline): smuok = validate("spherical electric dipole moment", match14.groups(), smuref, smutol, line) elif match15: sQok = validate("spherical electric quadrupole moment", match15.groups(), sQref, sQtol, line) + elif match16: + F2ok = True + print(line,end="") + elif match17: + Fok = True + print(line,end="") + elif match18: + H2ok = True + print(line,end="") + elif match19: + Hok = True + print(line,end="") else: print(line,end="") diff --git a/export/tests/hartree-fock/hartree-fock++.cc b/export/tests/hartree-fock/hartree-fock++.cc index 13064ff9a..2e45d3b41 100644 --- a/export/tests/hartree-fock/hartree-fock++.cc +++ b/export/tests/hartree-fock/hartree-fock++.cc @@ -661,23 +661,32 @@ int main(int argc, char* argv[]) { #if LIBINT2_DERIV_ERI_ORDER // compute 2-e forces Matrix F2 = Matrix::Zero(atoms.size(), 3); - - ////////// - // two-body contributions to the forces - ////////// - auto G1 = compute_2body_fock_deriv<1>(obs, atoms, D); - for (auto atom = 0, i = 0; atom != atoms.size(); ++atom) { - for (auto xyz = 0; xyz != 3; ++xyz, ++i) { - // identity prefactor since E(HF) = trace(H + F, D) = trace(2H + G, D) - auto force = G1[i].cwiseProduct(D).sum(); - F2(atom, xyz) += force; + // the derivative kernels may stop at a lower angular momentum than the + // energy kernels (e.g. LIBINT2_MAX_AM="2;2;1"): then say so instead of + // printing the quantity (the validator accepts that) + const bool have_F2 = obs.max_l() <= LIBINT2_MAX_AM_eri1; + if (have_F2) { + ////////// + // two-body contributions to the forces + ////////// + auto G1 = compute_2body_fock_deriv<1>(obs, atoms, D); + for (auto atom = 0, i = 0; atom != atoms.size(); ++atom) { + for (auto xyz = 0; xyz != 3; ++xyz, ++i) { + // identity prefactor since E(HF) = trace(H + F, D) = trace(2H + G, + // D) + auto force = G1[i].cwiseProduct(D).sum(); + F2(atom, xyz) += force; + } } - } - std::cout << "** 2-body forces = "; - for (int atom = 0; atom != atoms.size(); ++atom) - for (int xyz = 0; xyz != 3; ++xyz) std::cout << F2(atom, xyz) << " "; - std::cout << std::endl; + std::cout << "** 2-body forces = "; + for (int atom = 0; atom != atoms.size(); ++atom) + for (int xyz = 0; xyz != 3; ++xyz) std::cout << F2(atom, xyz) << " "; + std::cout << std::endl; + } else + std::cout << "** 2-body forces skipped: the 1st-derivative ERI kernels " + "of this library support l <= " + << LIBINT2_MAX_AM_eri1 << " only" << std::endl; #endif // if support 1-e and 2-e derivatives compute nuclear repulsion force and the @@ -720,11 +729,15 @@ int main(int argc, char* argv[]) { for (int xyz = 0; xyz != 3; ++xyz) std::cout << FN(atom, xyz) << " "; std::cout << std::endl; - auto F = F1 + F_Pulay + F2 + FN; - std::cout << "** Hartree-Fock forces = "; - for (int atom = 0; atom != atoms.size(); ++atom) - for (int xyz = 0; xyz != 3; ++xyz) std::cout << F(atom, xyz) << " "; - std::cout << std::endl; + if (have_F2) { + auto F = F1 + F_Pulay + F2 + FN; + std::cout << "** Hartree-Fock forces = "; + for (int atom = 0; atom != atoms.size(); ++atom) + for (int xyz = 0; xyz != 3; ++xyz) std::cout << F(atom, xyz) << " "; + std::cout << std::endl; + } else + std::cout << "** Hartree-Fock forces skipped: no 2-body forces" + << std::endl; #endif } @@ -780,28 +793,36 @@ int main(int argc, char* argv[]) { #endif // LIBINT2_DERIV_ONEBODY_ORDER > 1 #if LIBINT2_DERIV_ERI_ORDER > 1 - // compute 2-e forces + // compute 2-e hessian Matrix H2 = Matrix::Zero(ncoords, ncoords); - - ////////// - // two-body contributions to the forces - ////////// - auto G2 = compute_2body_fock_deriv<2>(obs, atoms, D); - for (auto row = 0, i = 0; row != ncoords; ++row) { - for (auto col = row; col != ncoords; ++col, ++i) { - // identity prefactor since E(HF) = trace(H + F, D) = trace(2H + G, D) - auto hess = G2[i].cwiseProduct(D).sum(); - H2(row, col) += hess; + // as for the forces: the 2nd-derivative kernels may not support the AM + // of the basis (e.g. LIBINT2_MAX_AM="2;2;1") + const bool have_H2 = obs.max_l() <= LIBINT2_MAX_AM_eri2; + if (have_H2) { + ////////// + // two-body contributions to the hessian + ////////// + auto G2 = compute_2body_fock_deriv<2>(obs, atoms, D); + for (auto row = 0, i = 0; row != ncoords; ++row) { + for (auto col = row; col != ncoords; ++col, ++i) { + // identity prefactor since E(HF) = trace(H + F, D) = trace(2H + G, + // D) + auto hess = G2[i].cwiseProduct(D).sum(); + H2(row, col) += hess; + } } - } - std::cout << "** 2-body hessian = "; - for (auto row = 0, i = 0; row != ncoords; ++row) { - for (auto col = row; col != ncoords; ++col) { - std::cout << H2(row, col) << " "; + std::cout << "** 2-body hessian = "; + for (auto row = 0, i = 0; row != ncoords; ++row) { + for (auto col = row; col != ncoords; ++col) { + std::cout << H2(row, col) << " "; + } } - } - std::cout << std::endl; + std::cout << std::endl; + } else + std::cout << "** 2-body hessian skipped: the 2nd-derivative ERI " + "kernels of this library support l <= " + << LIBINT2_MAX_AM_eri2 << " only" << std::endl; #endif // if support 1-e and 2-e 2nd derivatives compute nuclear repulsion hessian and @@ -866,14 +887,18 @@ int main(int argc, char* argv[]) { } std::cout << std::endl; - auto H = H1 + H_Pulay + H2 + HN; - std::cout << "** Hartree-Fock hessian = "; - for (auto row = 0, i = 0; row != ncoords; ++row) { - for (auto col = row; col != ncoords; ++col) { - std::cout << H(row, col) << " "; + if (have_H2) { + auto H = H1 + H_Pulay + H2 + HN; + std::cout << "** Hartree-Fock hessian = "; + for (auto row = 0, i = 0; row != ncoords; ++row) { + for (auto col = row; col != ncoords; ++col) { + std::cout << H(row, col) << " "; + } } - } - std::cout << std::endl; + std::cout << std::endl; + } else + std::cout << "** Hartree-Fock hessian skipped: no 2-body hessian" + << std::endl; #endif } @@ -2564,7 +2589,8 @@ void api_basic_compile_test(const BasisSet& obs, } #if LIBINT2_DERIV_ERI_ORDER - { // test deriv 2-index ints + // skipped if the derivative kernels do not support the AM of the basis + if (obs.max_l() <= LIBINT2_MAX_AM_eri1) { // test deriv 2-index ints Engine eri4_engine(Operator::coulomb, obs.max_nprim(), obs.max_l(), 1); Engine eri2_engine = eri4_engine; eri2_engine.set(BraKet::xs_xs); @@ -2599,7 +2625,7 @@ void api_basic_compile_test(const BasisSet& obs, #endif #if LIBINT2_DERIV_ERI_ORDER > 1 - { // test 2nd deriv 2-index ints + if (obs.max_l() <= LIBINT2_MAX_AM_eri2) { // test 2nd deriv 2-index ints Engine eri4_engine(Operator::coulomb, obs.max_nprim(), obs.max_l(), 2); Engine eri2_engine = eri4_engine; eri2_engine.set(BraKet::xs_xs); diff --git a/export/tests/unit/test-1body.cc b/export/tests/unit/test-1body.cc index 19d991f94..0c2debd07 100644 --- a/export/tests/unit/test-1body.cc +++ b/export/tests/unit/test-1body.cc @@ -18,30 +18,35 @@ * */ +#include + #include "catch.hpp" #include "fixture.h" -#define LIBINT2_TEST_ONEBODY(scale, op_idx) \ - int m0, m1; \ - FOR_SOLIDHARM_STANDARD(l0, m0) \ - FOR_SOLIDHARM_STANDARD(l1, m1) \ - const auto i0_s = libint2::INT_SOLIDHARMINDEX_STANDARD(l0, m0); \ - const auto i1_s = libint2::INT_SOLIDHARMINDEX_STANDARD(l1, m1); \ - const auto i0i1_s = i0_s * n1 + i1_s; \ - if (libint2::solid_harmonics_ordering() == \ - libint2::SHGShellOrdering_Standard) { \ - REQUIRE(engine.results()[op_idx][i0i1_s] / scale == \ - Approx(shellset_ref_standard[i0i1_s])); \ - } else { \ - const auto i0_g = libint2::INT_SOLIDHARMINDEX_GAUSSIAN(l0, m0); \ - const auto i1_g = libint2::INT_SOLIDHARMINDEX_GAUSSIAN(l1, m1); \ - const auto i0i1_g = i0_g * n1 + i1_g; \ - REQUIRE(engine.results()[op_idx][i0i1_g] / scale == \ - Approx(shellset_ref_standard[i0i1_s])); \ - } \ - END_FOR_SOLIDHARM \ +#define LIBINT2_TEST_ONEBODY_MARGIN(scale, op_idx, abs_margin) \ + int m0, m1; \ + FOR_SOLIDHARM_STANDARD(l0, m0) \ + FOR_SOLIDHARM_STANDARD(l1, m1) \ + const auto i0_s = libint2::INT_SOLIDHARMINDEX_STANDARD(l0, m0); \ + const auto i1_s = libint2::INT_SOLIDHARMINDEX_STANDARD(l1, m1); \ + const auto i0i1_s = i0_s * n1 + i1_s; \ + if (libint2::solid_harmonics_ordering() == \ + libint2::SHGShellOrdering_Standard) { \ + REQUIRE(engine.results()[op_idx][i0i1_s] / scale == \ + Approx(shellset_ref_standard[i0i1_s]).margin(abs_margin)); \ + } else { \ + const auto i0_g = libint2::INT_SOLIDHARMINDEX_GAUSSIAN(l0, m0); \ + const auto i1_g = libint2::INT_SOLIDHARMINDEX_GAUSSIAN(l1, m1); \ + const auto i0i1_g = i0_g * n1 + i1_g; \ + REQUIRE(engine.results()[op_idx][i0i1_g] / scale == \ + Approx(shellset_ref_standard[i0i1_s]).margin(abs_margin)); \ + } \ + END_FOR_SOLIDHARM \ END_FOR_SOLIDHARM +#define LIBINT2_TEST_ONEBODY(scale, op_idx) \ + LIBINT2_TEST_ONEBODY_MARGIN(scale, op_idx, 0.0) + TEST_CASE_METHOD(libint2::unit::DefaultFixture, "electrostatic potential", "[engine][1-body]") { #if defined(LIBINT2_SUPPORT_ONEBODY) @@ -244,6 +249,331 @@ TEST_CASE_METHOD(libint2::unit::DefaultFixture, "W correctness", #endif // LIBINT2_SUPPORT_ONEBODY } +TEST_CASE_METHOD(libint2::unit::DefaultFixture, "σpeμσp correctness", + "[engine][1-body]") { +#if defined(LIBINT2_SUPPORT_ONEBODY) + // (1-body) σ·p · r · σ·p, the σ·p-on-both-sides analog of the dipole moment: + // 12 components = 3 dipole directions k (x,y,z) × 4 Pauli quaternion pieces q + // (0=trace, 1/2/3=antisym), component index = 4*k + q. + // + // Reference values are the operator output over the (d|d) shell pair below + // (regression check); all components are also validated independently at + // the end against the Pauli-quaternion fold of the dipole's geometric + // derivatives ∂A_a ∂B_b (μ|r_k|ν), obtained by finite differences. + // Components 1, 6, 11 (Pauli direction aligned with the dipole direction) + // vanish by symmetry. Layout mirrors the opVop (σpVσp) test above. + auto make_shell0 = [](std::array O) { + return Shell{{1.0, 3.0}, {{2, true, {1.0, 0.3}}}, O}; + }; + auto make_shell1 = [](std::array O) { + return Shell{{2.0, 5.0}, {{2, true, {1.0, 0.2}}}, O}; + }; + const std::array O0{{0.0, 0.0, 0.0}}; + const std::array O1{{1.0, 1.0, 1.0}}; + std::vector obs{make_shell0(O0), make_shell1(O1)}; + + const auto lmax = std::min(2, LIBINT2_MAX_AM_opemuop); + auto engine = Engine(Operator::opemuop, 2, lmax); + + // independent check of all components of (make0(OA)|make1(OB)) with the + // dipole origin at @p origin: the Pauli-quaternion fold of + // (∂_a μ|r_k|∂_b ν) = ∂A_a ∂B_b (μ|r_k|ν), with the latter evaluated by + // central finite differences of the emultipole1 integrals + typedef std::function)> shell_factory_t; + auto check_vs_fd = [&](const shell_factory_t& make0, + const shell_factory_t& make1, + const std::array& OA, + const std::array& OB, + const std::array& origin) { + engine.set_params(origin); + engine.compute(make0(OA), make1(OB)); + auto emu_engine = Engine(Operator::emultipole1, 2, lmax); + emu_engine.set_params(origin); + const auto n01 = make0(OA).size() * make1(OB).size(); + const double h = 1e-4; + // dAdB[(3*a + b)*3 + k][i] = ∂A_a ∂B_b (μ|r_k|ν)_i + std::vector> dAdB(27, std::vector(n01, 0.0)); + for (int a = 0; a != 3; ++a) { + for (int b = 0; b != 3; ++b) { + for (int sa : {-1, 1}) { + for (int sb : {-1, 1}) { + auto A = OA; + A[a] += sa * h; + auto B = OB; + B[b] += sb * h; + const auto& emu = emu_engine.compute(make0(A), make1(B)); + for (int k = 0; k != 3; ++k) + for (size_t i = 0; i != n01; ++i) + dAdB[(3 * a + b) * 3 + k][i] += + sa * sb * emu[1 + k][i] / (4 * h * h); + } + } + } + } + auto d = [&](int a, int b, int k, size_t i) { + return dAdB[(3 * a + b) * 3 + k][i]; + }; + constexpr int x = 0, y = 1, z = 2; + for (int k = 0; k != 3; ++k) { + for (size_t i = 0; i != n01; ++i) { + const double ref[4] = {d(x, x, k, i) + d(y, y, k, i) + d(z, z, k, i), + d(y, z, k, i) - d(z, y, k, i), + d(z, x, k, i) - d(x, z, k, i), + d(x, y, k, i) - d(y, x, k, i)}; + for (int q = 0; q != 4; ++q) { + INFO("l0=" << make0(OA).contr[0].l << " l1=" << make1(OB).contr[0].l + << " nprim0=" << make0(OA).nprim() + << " nprim1=" << make1(OB).nprim() << " k=" << k + << " q=" << q << " i=" << i); + REQUIRE(engine.results()[4 * k + q][i] == + Approx(ref[q]).margin(1e-6)); + } + } + } + }; + + // every pair of s, p, d shells (up to lmax), contracted and uncontracted, + // pure and cartesian, with the dipole origin away from both centers + { + auto make = [](int l, bool contracted, bool pure) { + return shell_factory_t([=](std::array O) { + if (contracted) return Shell{{1.7, 0.4}, {{l, pure, {0.6, 0.5}}}, O}; + return Shell{{0.9}, {{l, pure, {1.0}}}, O}; + }); + }; + const std::array A{{0.4, -0.3, 0.2}}; + const std::array B{{-0.5, 0.7, 1.1}}; + const std::array origin{{0.1, -0.2, 0.3}}; + for (int l0 = 0; l0 <= lmax; ++l0) + for (int l1 = 0; l1 <= lmax; ++l1) + for (bool c0 : {false, true}) + for (bool c1 : {false, true}) + for (bool pure : {false, true}) + check_vs_fd(make(l0, c0, pure), make(l1, c1, pure), A, B, origin); + } + + if (lmax < 2) return; + + constexpr int l0 = 2; + constexpr int l1 = 2; + constexpr int n1 = 2 * l1 + 1; + + engine.set_params(std::array{{0.0, 0.0, 0.0}}); + engine.compute(obs[0], obs[1]); + + // all ref are laid out in standard solids order + { + std::vector shellset_ref_standard = { + 0.09223217251311097, 0.7337253845349039, + 0.8940034778856171, 0.22249836470532133, + -0.252327005953861, 0.2544347044464152, + 0.25443470444641514, 0.0, + 0.2544347044464152, 0.0, + 0.4283068075013277, -0.4656966703842894, + -0.8368997546246189, -0.1272426104648221, + -0.10997391819356982, 0.22249836470532133, + 0.7337253845349038, -0.22848014172589765, + 0.09223217251311103, -0.9003932258975146, + -0.10035593979812935, 0.8066102940212458, + -0.10997391819356983, -0.4211025458090261, + -0.7099128120788283}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 0, 1e-10); + } + { + std::vector shellset_ref_standard = { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 1, 1e-10); + } + { + std::vector shellset_ref_standard = {0.04479855132698852, + 0.04479855132698852, + 0.0, + 0.04479855132698859, + 0.0, + 0.04479855132698847, + -0.07520852256716505, + -0.25038279554682064, + -0.268870098007082, + 0.06000353694707675, + 0.0, + -0.25038279554682064, + 0.09138051976167452, + -0.25038279554682075, + 0.0, + 0.04479855132698842, + -0.2688700980070819, + -0.2503827955468205, + -0.07520852256716526, + -0.06000353694707675, + 0.0, + 0.06000353694707682, + 0.0, + -0.06000353694707674, + -0.24683963008765036}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 2, 1e-10); + } + { + std::vector shellset_ref_standard = { + 0.07520852256716518, 0.26887009800708184, + -0.17715598508649683, -0.04479855132698844, + -0.18683609314057345, 0.268870098007082, + 0.07520852256716505, -0.07322681046032367, + -0.044798551326988484, -0.24683963008765025, + -0.17715598508649694, -0.07322681046032367, + 0.16228459262531905, 0.0, + -0.14645362092064743, -0.04479855132698858, + -0.04479855132698848, 0.0, + -0.044798551326988525, 0.0, + -0.18683609314057373, -0.24683963008765045, + -0.14645362092064743, 0.0, + -0.006825482299343409}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 3, 1e-10); + } + { + std::vector shellset_ref_standard = { + 0.09223217251311097, 0.2224983647053212, + 0.8940034778856171, 0.7337253845349039, + 0.25232700595386154, 0.2224983647053212, + 0.09223217251311096, -0.22848014172589742, + 0.7337253845349039, 0.9003932258975148, + 0.42830680750132777, -0.127242610464822, + -0.836899754624619, -0.46569667038428947, + 0.10997391819356969, 0.2544347044464152, + 0.25443470444641514, 0.0, + 0.25443470444641514, 0.0, + 0.10035593979812953, 0.42110254580902606, + 0.10997391819356978, -0.8066102940212458, + -0.7099128120788284}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 4, 1e-10); + } + { + std::vector shellset_ref_standard = {-0.04479855132698845, + -0.04479855132698845, + 0.0, + -0.04479855132698854, + 0.0, + -0.04479855132698835, + 0.07520852256716522, + 0.2503827955468206, + 0.26887009800708184, + -0.0600035369470768, + 0.0, + 0.2503827955468207, + -0.09138051976167444, + 0.2503827955468207, + 0.0, + -0.044798551326988435, + 0.268870098007082, + 0.2503827955468206, + 0.07520852256716506, + 0.060003536947076705, + 0.0, + -0.06000353694707683, + 0.0, + 0.06000353694707682, + 0.24683963008765056}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 5, 1e-10); + } + { + std::vector shellset_ref_standard = { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 6, 1e-10); + } + { + std::vector shellset_ref_standard = { + -0.07520852256716509, 0.04479855132698833, + 0.177155985086497, -0.26887009800708184, + -0.18683609314057353, 0.04479855132698844, + 0.04479855132698844, 0.0, + 0.044798551326988505, 0.0, + 0.17715598508649694, 0.0, + -0.16228459262531914, 0.07322681046032364, + -0.14645362092064745, -0.26887009800708195, + 0.044798551326988456, 0.07322681046032371, + -0.07520852256716508, -0.2468396300876503, + -0.1868360931405737, 0.0, + -0.14645362092064748, -0.24683963008765053, + 0.006825482299343263}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 7, 1e-10); + } + { + std::vector shellset_ref_standard = {0.2544347044464152, + 0.25443470444641514, + 0.0, + 0.25443470444641514, + 0.0, + 0.7337253845349039, + 0.09223217251311093, + -0.6655233361597198, + 0.2224983647053212, + 0.6480662199436535, + 0.9313933407685788, + -0.3010641970365056, + -0.6464193408059329, + -0.30106419703650567, + 0.0, + 0.7337253845349039, + 0.2224983647053212, + -0.6655233361597195, + 0.09223217251311097, + -0.6480662199436531, + 0.0, + 0.3207466060108965, + 0.0, + -0.3207466060108965, + -0.9003932258975146}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 8, 1e-10); + } + { + std::vector shellset_ref_standard = { + -0.07520852256716505, -0.268870098007082, + 0.17715598508649688, 0.04479855132698845, + 0.18683609314057348, -0.2688700980070818, + -0.07520852256716509, 0.07322681046032364, + 0.044798551326988394, 0.2468396300876503, + 0.17715598508649705, 0.07322681046032363, + -0.16228459262531927, 0.0, + 0.14645362092064737, 0.044798551326988525, + 0.04479855132698848, 0.0, + 0.044798551326988435, 0.0, + 0.18683609314057356, 0.2468396300876504, + 0.1464536209206475, 0.0, + 0.006825482299343293}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 9, 1e-10); + } + { + std::vector shellset_ref_standard = { + 0.07520852256716505, -0.04479855132698846, + -0.1771559850864969, 0.26887009800708195, + 0.1868360931405736, -0.044798551326988505, + -0.04479855132698844, 0.0, + -0.04479855132698858, 0.0, + -0.17715598508649705, 0.0, + 0.16228459262531922, -0.07322681046032384, + 0.14645362092064737, 0.26887009800708184, + -0.04479855132698835, -0.07322681046032367, + 0.07520852256716509, 0.2468396300876504, + 0.18683609314057362, 0.0, + 0.14645362092064745, 0.2468396300876504, + -0.006825482299343255}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 10, 1e-10); + } + { + std::vector shellset_ref_standard = { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + LIBINT2_TEST_ONEBODY_MARGIN(1.0, 11, 1e-10); + } + + // the independent check for the (d|d) pair above + check_vs_fd(make_shell0, make_shell1, O0, O1, + std::array{{0.0, 0.0, 0.0}}); + +#endif // LIBINT2_SUPPORT_ONEBODY +} + // Helper: max number of primitives across all centers in q_gau data. static size_t max_nprim(const libint2::GaussianPotentialCentersData& data) { size_t result = 0; diff --git a/export/tests/unit/test-2body.cc b/export/tests/unit/test-2body.cc index d25bcc243..d346135e1 100644 --- a/export/tests/unit/test-2body.cc +++ b/export/tests/unit/test-2body.cc @@ -1,5 +1,5 @@ /* - * Copyright (C) 2004-2024 Edward F. Valeev + * Copyright (C) 2004-2026 Edward F. Valeev * * This file is part of Libint library. * @@ -20,6 +20,10 @@ #include +#include +#include +#include + #include "catch.hpp" #include "fixture.h" @@ -32,6 +36,7 @@ typedef unsigned int uint; TEST_CASE("Slater/Yukawa integrals", "[engine][2-body]") { +#if defined(LIBINT2_SUPPORT_ERI) std::vector obs{ // pseudorandom d Shell{{1.0, 3.0}, {{2, true, {1.0, 0.3}}}, {{0.0, 0.0, 0.0}}}, @@ -120,6 +125,7 @@ TEST_CASE("Slater/Yukawa integrals", "[engine][2-body]") { } } } +#endif // LIBINT2_SUPPORT_ERI } TEST_CASE("delcgtga_delcgtgb integrals", "[engine][2-body]") { @@ -260,6 +266,7 @@ TEST_CASE("delcgtga_delcgtgb integrals", "[engine][2-body]") { // see https://github.com/evaleev/libint/issues/216 TEST_CASE_METHOD(libint2::unit::DefaultFixture, "bra-ket permutation", "[engine][2-body]") { +#if defined(LIBINT2_SUPPORT_ERI) if (LIBINT2_MAX_AM_eri < 2) return; using libint2::BasisSet; using libint2::Engine; @@ -321,9 +328,11 @@ TEST_CASE_METHOD(libint2::unit::DefaultFixture, "bra-ket permutation", } } } +#endif // LIBINT2_SUPPORT_ERI } TEST_CASE("eri geometric derivatives", "[engine][2-body]") { +#if defined(LIBINT2_SUPPORT_ERI) std::vector obs{ // pseudorandom p Shell{{1.0, 0.3}, {{1, false, {0.9, 0.3}}}, {{0.0, 0.0, 0.0}}}, @@ -477,9 +486,1186 @@ TEST_CASE("eri geometric derivatives", "[engine][2-body]") { } } } +#endif // LIBINT2_SUPPORT_ERI +} + +// Validates geometric derivatives of Coulomb integrals against 5-point central +// finite differences of the derivatives of the next lower order, for every +// combination of s, p, d shells (and up to f on the fitting center), with +// contracted and uncontracted shells, in all brakets with derivative kernels. +// Complements "eri geometric derivatives" above, which compares (pp|pp) against +// an independent reference: this covers every kernel in the build, including +// the code-sharing paths that only appear with spectator shells of unequal +// size, at the cost of relying on the correctness of the next lower order. +namespace { + +// index of the second derivative with respect to coordinates i and j: second +// derivatives are the upper triangle of the Hessian, in row-major order +size_t hessian_index(size_t ncoords, size_t i, size_t j) { + const auto row = std::min(i, j); + const auto col = std::max(i, j); + return row * (2 * ncoords - row - 1) / 2 + col; +} + +// all centers distinct and in general position +Shell make_fd_shell(int l, bool contracted, int center) { + const std::array O[] = {{{0.0, 0.0, 0.0}}, + {{0.7, 0.2, -0.3}}, + {{-0.4, 0.9, 0.5}}, + {{0.3, -0.6, 1.1}}}; + const double scale = 1.0 + 0.2 * center; + if (contracted) + return Shell{{2.3 * scale, 0.8 * scale, 0.31 * scale}, + {{l, false, {0.5, 0.4, 0.3}}}, + O[center]}; + return Shell{{0.65 * scale}, {{l, false, {1.0}}}, O[center]}; +} + +std::vector> compute_fd_shellsets( + Engine &engine, BraKet braket, const std::vector &shells) { + const Engine::target_ptr_vec *results = nullptr; + switch (braket) { + case BraKet::xx_xx: + results = &engine.compute(shells[0], shells[1], shells[2], shells[3]); + break; + case BraKet::xs_xx: + results = &engine.compute(shells[0], shells[1], shells[2]); + break; + case BraKet::xs_xs: + results = &engine.compute(shells[0], shells[1]); + break; + default: + FAIL("unsupported braket"); + } + size_t n = 1; + for (const auto &sh : shells) n *= sh.size(); + std::vector> shellsets; + for (const auto *target : *results) { + if (target == nullptr) // screened out + shellsets.emplace_back(n, 0.0); + else + shellsets.emplace_back(target, target + n); + } + return shellsets; +} + +// checks derivatives of order deriv_order of a shell set with the given +// angular momenta against finite differences of order deriv_order - 1; +// returns false if the library lacks the kernels for this shell set +bool check_fd_derivs(BraKet braket, int deriv_order, const std::vector &l, + const std::vector &contracted) { + const auto ncenters = l.size(); + std::vector shells; + for (size_t c = 0; c != ncenters; ++c) + shells.push_back(make_fd_shell(l[c], contracted[c], c)); + const auto nprim = libint2::max_nprim(shells); + const auto lmax = libint2::max_l(shells); + const auto eps = std::numeric_limits::epsilon(); + Engine engine, engine_lower; + try { + engine = Engine(Operator::coulomb, nprim, lmax, deriv_order, eps, + libint2::default_params(Operator::coulomb), braket); + engine_lower = Engine(Operator::coulomb, nprim, lmax, deriv_order - 1, eps, + libint2::default_params(Operator::coulomb), braket); + } catch (Engine::lmax_exceeded &) { + return false; + } + engine.set_precision(0.0); + engine_lower.set_precision(0.0); + + const auto analytic = compute_fd_shellsets(engine, braket, shells); + const auto ncoords = 3 * ncenters; + REQUIRE(analytic.size() == + libint2::num_geometrical_derivatives(ncenters, deriv_order)); + + const double h = 1e-3; + for (size_t coord1 = 0; coord1 != ncoords; ++coord1) { + // derivatives of order deriv_order - 1 at a displaced geometry; + // N.B. displace Shell::O in place, Shell's constructor would renormalize + // the contraction coefficients + auto lower = [&](double delta) { + auto displaced = shells; + displaced[coord1 / 3].O[coord1 % 3] += delta; + return compute_fd_shellsets(engine_lower, braket, displaced); + }; + const auto m2 = lower(-2 * h); + const auto m1 = lower(-h); + const auto p1 = lower(h); + const auto p2 = lower(2 * h); + + for (size_t s = 0; s != p1.size(); ++s) { + const auto deriv_index = + deriv_order == 1 ? coord1 : hessian_index(ncoords, s, coord1); + const auto &a = analytic.at(deriv_index); + double max_error = 0, max_numeric = 0; + for (size_t i = 0; i != a.size(); ++i) { + const auto numeric = + (m2[s][i] - 8 * m1[s][i] + 8 * p1[s][i] - p2[s][i]) / (12 * h); + max_error = std::max(max_error, std::abs(a[i] - numeric)); + max_numeric = std::max(max_numeric, std::abs(numeric)); + } + std::ostringstream oss; + for (size_t c = 0; c != ncenters; ++c) + oss << (contracted[c] ? "LPDF"[l[c]] : "lpdf"[l[c]]); + INFO("braket=" << static_cast(braket) + << " deriv_order=" << deriv_order + << " shells=" << oss.str() << " coord=" << coord1 + << " lower_shellset=" << s << " max_error=" << max_error); + // 5-point stencil with h=1e-3: truncation and roundoff errors are both + // ~1e-12 relative to the scale of the derivatives + REQUIRE(max_error <= 1e-9 * std::max(1.0, max_numeric)); + } + } + return true; +} + +// contraction pattern for a shell set, cycling through the patterns so that +// each angular momentum combination is tested once +std::vector fd_contraction_pattern(const std::vector &l) { + int key = 0; + for (size_t c = 0; c != l.size(); ++c) key += (c + 1) * l[c]; + std::vector contracted(l.size(), false); + switch (key % 3) { + case 0: + break; + case 1: + for (size_t c = 0; c < l.size(); c += 2) contracted[c] = true; + break; + case 2: + contracted.assign(l.size(), true); + break; + } + return contracted; +} + +} // namespace + +TEST_CASE("eri geometric derivatives vs finite differences", + "[engine][2-body][deriv]") { + size_t ntested = 0; +#if defined(LIBINT2_SUPPORT_ERI) && LIBINT2_DERIV_ERI_ORDER > 0 + SECTION("4-center") { + const int lmax = std::min(2, LIBINT2_MAX_AM_eri); + for (int d = 1; d <= LIBINT2_DERIV_ERI_ORDER; ++d) + for (int la = 0; la <= lmax; ++la) + for (int lb = 0; lb <= lmax; ++lb) + for (int lc = 0; lc <= lmax; ++lc) + for (int ld = 0; ld <= lmax; ++ld) { + const std::vector l{la, lb, lc, ld}; + if (check_fd_derivs(BraKet::xx_xx, d, l, + fd_contraction_pattern(l))) + ++ntested; + } + REQUIRE(ntested > 0); + } +#endif +#if defined(LIBINT2_SUPPORT_ERI3) && LIBINT2_DERIV_ERI3_ORDER > 0 + SECTION("3-center") { + const int lmax_fit = std::min(3, LIBINT2_MAX_AM_3eri); + const int lmax = std::min(2, LIBINT2_MAX_AM_default); + for (int d = 1; d <= LIBINT2_DERIV_ERI3_ORDER; ++d) + for (int lp = 0; lp <= lmax_fit; ++lp) + for (int lc = 0; lc <= lmax; ++lc) + for (int ld = 0; ld <= lmax; ++ld) { + const std::vector l{lp, lc, ld}; + if (check_fd_derivs(BraKet::xs_xx, d, l, fd_contraction_pattern(l))) + ++ntested; + } + REQUIRE(ntested > 0); + } +#endif +#if defined(LIBINT2_SUPPORT_ERI2) && LIBINT2_DERIV_ERI2_ORDER > 0 + SECTION("2-center") { + const int lmax = std::min(3, LIBINT2_MAX_AM_2eri); + for (int d = 1; d <= LIBINT2_DERIV_ERI2_ORDER; ++d) + for (int lp = 0; lp <= lmax; ++lp) + for (int lq = 0; lq <= lmax; ++lq) { + const std::vector l{lp, lq}; + if (check_fd_derivs(BraKet::xs_xs, d, l, fd_contraction_pattern(l))) + ++ntested; + } + REQUIRE(ntested > 0); + } +#endif +} + +// re-initializing an Engine for a task whose AM limit is below the Engine's +// lmax must throw Engine::lmax_exceeded and leave the Engine without kernels: +// the next compute() must throw Engine::missing_kernel rather than use the +// kernel table of the previous task +TEST_CASE("lmax_exceeded leaves no kernels", "[engine][2-body]") { + // @param lmax valid for the 3-center task of @p oper, too high for its + // 4-center task + // @param pure_fit whether the fitting shell is a solid harmonics shell + // (required by LIBINT2_ERI3_PURE_SH for l > 1, not + // generated for the RKB 3-center kernels) + auto check = [](Operator oper, int lmax, bool pure_fit) { + const Shell fit{{1.5}, {{lmax, pure_fit, {1.0}}}, {{-1.0, 0.5, 0.0}}}; + const Shell s{{1.0}, {{0, false, {1.0}}}, {{0.0, 0.0, 0.0}}}; + const auto eps = std::numeric_limits::epsilon(); + Engine engine(oper, 1, lmax, 0, eps, libint2::default_params(oper), + BraKet::xs_xx); + REQUIRE(engine.compute(fit, s, s)[0] != nullptr); + REQUIRE_THROWS_AS(engine.set(BraKet::xx_xx), Engine::lmax_exceeded); + // set() changes the braket before it (re)initializes the kernels, so the + // Engine is left as a kernel-less 4-center Engine: compute() takes 4 + // shells (a 3-shell call would violate the argument count, which is + // asserted in Debug builds) and has no kernels to dispatch + CHECK(engine.braket() == BraKet::xx_xx); + CHECK_THROWS_AS(engine.compute(s, s, s, s), Engine::missing_kernel); + }; +#if defined(LIBINT2_SUPPORT_ERI) && defined(LIBINT2_SUPPORT_ERI3) && \ + LIBINT2_MAX_AM_3eri > LIBINT2_MAX_AM_eri + SECTION("coulomb") { + check(Operator::coulomb, LIBINT2_MAX_AM_3eri, /* pure_fit = */ true); + } +#endif +#if LIBINT2_TASK_EXISTS_3coulomb_opop && LIBINT2_TASK_EXISTS_4coulomb_opop && \ + LIBINT2_MAX_AM_3coulomb_opop > LIBINT2_MAX_AM_coulomb_opop + SECTION("coulomb_opop") { + check(Operator::coulomb_opop, LIBINT2_MAX_AM_3coulomb_opop, + /* pure_fit = */ false); + } +#endif +} + +// likewise, if set() changes the operator to one that initialize() rejects, +// the next compute() must not use the kernel table of the previous operator +TEST_CASE("rejected set() leaves no kernels", "[engine][2-body]") { +#if defined(LIBINT2_SUPPORT_ERI) && LIBINT2_DERIV_ERI_ORDER > 0 + const Shell s{{1.0}, {{0, false, {1.0}}}, {{0.0, 0.0, 0.0}}}; + Engine engine(Operator::coulomb, 1, 0, 1); + REQUIRE(engine.compute(s, s, s, s)[0] != nullptr); + REQUIRE_THROWS_AS(engine.set(Operator::coulomb_opop), std::invalid_argument); + CHECK_THROWS_AS(engine.compute(s, s, s, s), Engine::missing_kernel); +#endif +} + +TEST_CASE("RKB Coulomb integrals", "[engine][2-body]") { + const std::vector all_shells{ + // pseudorandom s + Shell{{1.0}, {{0, false, {1.0}}}, {{0.0, 0.0, 0.0}}}, + // pseudorandom p + Shell{{2.0}, {{1, false, {1.0}}}, {{1.0, 1.0, 1.0}}}, + // pseudorandom contracted p + Shell{{3.0, 0.7}, {{1, false, {0.4, 0.8}}}, {{-0.5, 0.2, 0.9}}}, + // pseudorandom d + Shell{{1.2}, {{2, false, {1.0}}}, {{0.3, -0.8, 0.4}}}}; + // the subset of all_shells supported by a kernel with max AM lmax + auto shells_upto = [&all_shells](int lmax) { + std::vector result; + for (const auto &sh : all_shells) + if (sh.contr[0].l <= lmax) result.push_back(sh); + return result; + }; + + typedef std::array der_idx; + + // e.g. d_xx maps the derivative index of derivative w.r.t x + // coord of ket1 and x coord of ket2 in Chemist notation. + // deriv indices for (LL|SS) + der_idx d_xx = {0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0}; + der_idx d_yy = {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0}; + der_idx d_zz = {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; + der_idx d_yz = {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1}; + der_idx d_zy = {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0}; + der_idx d_zx = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0}; + der_idx d_xz = {0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; + der_idx d_xy = {0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0}; + der_idx d_yx = {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0}; + +#if LIBINT2_TASK_EXISTS_4coulomb_opop && LIBINT2_TASK_EXISTS_4opop_coulomb_opop + SECTION("Coulombσpσp and σpσpCoulombσpσp") { + // the reference is expensive: skip the uncontracted p shell (the + // contracted p shell covers p) + auto obs = shells_upto(std::min(LIBINT2_MAX_AM_coulomb_opop, + LIBINT2_MAX_AM_opop_coulomb_opop)); + obs.erase(std::remove_if(obs.begin(), obs.end(), + [](const Shell &sh) { + return sh.contr[0].l == 1 && sh.nprim() == 1; + }), + obs.end()); + const auto max_nprim = libint2::max_nprim(obs); + const auto max_l = libint2::max_l(obs); + Engine engine_llss(Operator::coulomb_opop, max_nprim, max_l, 0); + Engine engine_ssss(Operator::opop_coulomb_opop, max_nprim, max_l, 0); + + const auto nshell = obs.size(); + for (int s0 = 0; s0 != nshell; ++s0) { + for (int s1 = 0; s1 != nshell; ++s1) { + for (int s2 = 0; s2 != nshell; ++s2) { + for (int s3 = 0; s3 != nshell; ++s3) { + // the reference is expensive, include at most 1 d shell + if ((obs[s0].contr[0].l > 1) + (obs[s1].contr[0].l > 1) + + (obs[s2].contr[0].l > 1) + (obs[s3].contr[0].l > 1) > + 1) + continue; + const auto &results_llss = + engine_llss.compute(obs[s0], obs[s1], obs[s2], obs[s3]); + const auto &results_ssss = + engine_ssss.compute(obs[s0], obs[s1], obs[s2], obs[s3]); + // 4 buffers for single-spin quaternion components + REQUIRE(results_llss.size() == 4); + // 16 buffers for two independent spin-space quaternions + REQUIRE(results_ssss.size() == 16); + + LIBINT2_REF_REALTYPE Aref[3]; + for (int i = 0; i < 3; ++i) Aref[i] = obs[s0].O[i]; + LIBINT2_REF_REALTYPE Bref[3]; + for (int i = 0; i < 3; ++i) Bref[i] = obs[s1].O[i]; + LIBINT2_REF_REALTYPE Cref[3]; + for (int i = 0; i < 3; ++i) Cref[i] = obs[s2].O[i]; + LIBINT2_REF_REALTYPE Dref[3]; + for (int i = 0; i < 3; ++i) Dref[i] = obs[s3].O[i]; + + int ijkl = 0; + + int l0, m0, n0; + FOR_CART(l0, m0, n0, obs[s0].contr[0].l) + + int l1, m1, n1; + FOR_CART(l1, m1, n1, obs[s1].contr[0].l) + + int l2, m2, n2; + FOR_CART(l2, m2, n2, obs[s2].contr[0].l) + + int l3, m3, n3; + FOR_CART(l3, m3, n3, obs[s3].contr[0].l) + + std::array ref_coulomb_opop{0.0, 0.0, 0.0, + 0.0}; + std::array ref_opop_coulomb_opop{}; + ref_opop_coulomb_opop.fill(0.0); + uint p0123 = 0; + for (uint p0 = 0; p0 < obs[s0].nprim(); p0++) { + for (uint p1 = 0; p1 < obs[s1].nprim(); p1++) { + for (uint p2 = 0; p2 < obs[s2].nprim(); p2++) { + for (uint p3 = 0; p3 < obs[s3].nprim(); p3++, p0123++) { + const LIBINT2_REF_REALTYPE alpha0 = obs[s0].alpha[p0]; + const LIBINT2_REF_REALTYPE alpha1 = obs[s1].alpha[p1]; + const LIBINT2_REF_REALTYPE alpha2 = obs[s2].alpha[p2]; + const LIBINT2_REF_REALTYPE alpha3 = obs[s3].alpha[p3]; + + const LIBINT2_REF_REALTYPE c0 = obs[s0].contr[0].coeff[p0]; + const LIBINT2_REF_REALTYPE c1 = obs[s1].contr[0].coeff[p1]; + const LIBINT2_REF_REALTYPE c2 = obs[s2].contr[0].coeff[p2]; + const LIBINT2_REF_REALTYPE c3 = obs[s3].contr[0].coeff[p3]; + const LIBINT2_REF_REALTYPE c0123 = c0 * c1 * c2 * c3; + + auto eri_drrrr = [&](der_idx d_rrrr) { + return eri(d_rrrr.data(), l0, m0, n0, alpha0, Aref, l1, + m1, n1, alpha1, Bref, l2, m2, n2, alpha2, Cref, + l3, m3, n3, alpha3, Dref, 0); + }; + + // helper: build der_idx from 4 derivative directions + // (0=x, 1=y, 2=z) for centers A, B, C, D + constexpr int X = 0, Y = 1, Z = 2; + auto didx = [](int a, int b, int c, int d) -> der_idx { + der_idx r = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + r[a] = 1; + r[3 + b] = 1; + r[6 + c] = 1; + r[9 + d] = 1; + return r; + }; + // shorthand: evaluate derivative ERI from 4 directions + auto D = [&](int a, int b, int c, int d) { + return eri_drrrr(didx(a, b, c, d)); + }; + + // (LL|SS) + ref_coulomb_opop[0] += + c0123 * + (eri_drrrr(d_xx) + eri_drrrr(d_yy) + eri_drrrr(d_zz)); + ref_coulomb_opop[1] += + c0123 * (eri_drrrr(d_yz) - eri_drrrr(d_zy)); + ref_coulomb_opop[2] += + c0123 * (eri_drrrr(d_zx) - eri_drrrr(d_xz)); + ref_coulomb_opop[3] += + c0123 * (eri_drrrr(d_xy) - eri_drrrr(d_yx)); + + // (SS|SS) — 16 components, Option A: index = 4*bra + ket + // 0:SS 1:SX 2:SY 3:SZ + ref_opop_coulomb_opop[0] += + c0123 * (D(X, X, X, X) + D(X, X, Y, Y) + D(X, X, Z, Z) + + D(Y, Y, X, X) + D(Y, Y, Y, Y) + D(Y, Y, Z, Z) + + D(Z, Z, X, X) + D(Z, Z, Y, Y) + D(Z, Z, Z, Z)); + ref_opop_coulomb_opop[1] += + c0123 * (D(X, X, Y, Z) - D(X, X, Z, Y) + D(Y, Y, Y, Z) - + D(Y, Y, Z, Y) + D(Z, Z, Y, Z) - D(Z, Z, Z, Y)); + ref_opop_coulomb_opop[2] += + c0123 * (D(X, X, Z, X) - D(X, X, X, Z) + D(Y, Y, Z, X) - + D(Y, Y, X, Z) + D(Z, Z, Z, X) - D(Z, Z, X, Z)); + ref_opop_coulomb_opop[3] += + c0123 * (D(X, X, X, Y) - D(X, X, Y, X) + D(Y, Y, X, Y) - + D(Y, Y, Y, X) + D(Z, Z, X, Y) - D(Z, Z, Y, X)); + // 4:XS 5:XX 6:XY 7:XZ + ref_opop_coulomb_opop[4] += + c0123 * (D(Y, Z, X, X) - D(Z, Y, X, X) + D(Y, Z, Y, Y) - + D(Z, Y, Y, Y) + D(Y, Z, Z, Z) - D(Z, Y, Z, Z)); + ref_opop_coulomb_opop[5] += + c0123 * (-D(Y, Z, Y, Z) + D(Y, Z, Z, Y) + + D(Z, Y, Y, Z) - D(Z, Y, Z, Y)); + ref_opop_coulomb_opop[6] += + c0123 * (-D(Y, Z, Z, X) + D(Y, Z, X, Z) + + D(Z, Y, Z, X) - D(Z, Y, X, Z)); + ref_opop_coulomb_opop[7] += + c0123 * (-D(Y, Z, X, Y) + D(Y, Z, Y, X) + + D(Z, Y, X, Y) - D(Z, Y, Y, X)); + // 8:YS 9:YX 10:YY 11:YZ + ref_opop_coulomb_opop[8] += + c0123 * (D(Z, X, X, X) - D(X, Z, X, X) + D(Z, X, Y, Y) - + D(X, Z, Y, Y) + D(Z, X, Z, Z) - D(X, Z, Z, Z)); + ref_opop_coulomb_opop[9] += + c0123 * (-D(Z, X, Y, Z) + D(Z, X, Z, Y) + + D(X, Z, Y, Z) - D(X, Z, Z, Y)); + ref_opop_coulomb_opop[10] += + c0123 * (-D(Z, X, Z, X) + D(Z, X, X, Z) + + D(X, Z, Z, X) - D(X, Z, X, Z)); + ref_opop_coulomb_opop[11] += + c0123 * (-D(Z, X, X, Y) + D(Z, X, Y, X) + + D(X, Z, X, Y) - D(X, Z, Y, X)); + // 12:ZS 13:ZX 14:ZY 15:ZZ + ref_opop_coulomb_opop[12] += + c0123 * (D(X, Y, X, X) - D(Y, X, X, X) + D(X, Y, Y, Y) - + D(Y, X, Y, Y) + D(X, Y, Z, Z) - D(Y, X, Z, Z)); + ref_opop_coulomb_opop[13] += + c0123 * (-D(X, Y, Y, Z) + D(X, Y, Z, Y) + + D(Y, X, Y, Z) - D(Y, X, Z, Y)); + ref_opop_coulomb_opop[14] += + c0123 * (-D(X, Y, Z, X) + D(X, Y, X, Z) + + D(Y, X, Z, X) - D(Y, X, X, Z)); + ref_opop_coulomb_opop[15] += + c0123 * (-D(X, Y, X, Y) + D(X, Y, Y, X) + + D(Y, X, X, Y) - D(Y, X, Y, X)); + } + } + } + } + + const double ABSOLUTE_DEVIATION_THRESHOLD = 5.0E-14; + const double RELATIVE_DEVIATION_THRESHOLD = + 1.0E-9; // For more detail on choice of these thresholds, see + // the comments in the TEST_CASE "eri geometric + // derivatives" + + std::array abs_errs_llss; + std::array rel_abs_errs_llss; + + // (LL|SS) has 4 components + for (auto comp = 0; comp < 4; ++comp) { + abs_errs_llss[comp] = + abs(ref_coulomb_opop[comp] - results_llss[comp][ijkl]); + rel_abs_errs_llss[comp] = + abs(abs_errs_llss[comp] / ref_coulomb_opop[comp]); + + bool llss_not_ok = + rel_abs_errs_llss[comp] > RELATIVE_DEVIATION_THRESHOLD && + abs_errs_llss[comp] > ABSOLUTE_DEVIATION_THRESHOLD; + + if (llss_not_ok) { + std::cout << "(l0 l1| l2 l3) = " + << "(" << s0 << " " << s1 << " | " << s2 << " " << s3 + << ") " + << "Elem " << ijkl << " comp= " << comp + << " : ref = " << ref_coulomb_opop[comp] + << " libint = " << results_llss[comp][ijkl] + << " relabs_error = " << rel_abs_errs_llss[comp] + << " abs_error = " << abs_errs_llss[comp] + << std::endl; + } + REQUIRE(!llss_not_ok); + } + + // (SS|SS) has 16 components (two independent spin spaces); the + // reference involves 4th derivatives and loses more precision + const double SSSS_ABSOLUTE_DEVIATION_THRESHOLD = 1.0E-12; + for (auto comp = 0; comp < 16; ++comp) { + auto abs_err_ssss = + abs(ref_opop_coulomb_opop[comp] - results_ssss[comp][ijkl]); + auto rel_abs_err_ssss = + abs(abs_err_ssss / ref_opop_coulomb_opop[comp]); + + bool ssss_not_ok = + rel_abs_err_ssss > RELATIVE_DEVIATION_THRESHOLD && + abs_err_ssss > SSSS_ABSOLUTE_DEVIATION_THRESHOLD; + + if (ssss_not_ok) { + std::cout << "(l0 l1| l2 l3) = " + << "(" << s0 << " " << s1 << " | " << s2 << " " << s3 + << ") " + << "Elem " << ijkl << " comp= " << comp + << " : ref = " << ref_opop_coulomb_opop[comp] + << " libint = " << results_ssss[comp][ijkl] + << " relabs_error = " << rel_abs_err_ssss + << " abs_error = " << abs_err_ssss << std::endl; + } + REQUIRE(!ssss_not_ok); + } + + ++ijkl; + END_FOR_CART + END_FOR_CART + END_FOR_CART + END_FOR_CART + } + } + } + } + } + +#endif // 4-center Coulombσpσp and σpσpCoulombσpσp + +#if LIBINT2_TASK_EXISTS_4op_coulomb_op + SECTION("op_coulomb_op") { + const auto obs = shells_upto(LIBINT2_MAX_AM_op_coulomb_op); + const auto max_nprim = libint2::max_nprim(obs); + const auto max_l = libint2::max_l(obs); + Engine engine_opCop(Operator::op_coulomb_op, max_nprim, max_l, 0); + + const auto nshell = obs.size(); + for (int s0 = 0; s0 != nshell; ++s0) { + for (int s1 = 0; s1 != nshell; ++s1) { + for (int s2 = 0; s2 != nshell; ++s2) { + for (int s3 = 0; s3 != nshell; ++s3) { + const auto &results = + engine_opCop.compute(obs[s0], obs[s1], obs[s2], obs[s3]); + REQUIRE(results.size() == 9); + + LIBINT2_REF_REALTYPE Aref[3], Bref[3], Cref[3], Dref[3]; + for (int i = 0; i < 3; ++i) Aref[i] = obs[s0].O[i]; + for (int i = 0; i < 3; ++i) Bref[i] = obs[s1].O[i]; + for (int i = 0; i < 3; ++i) Cref[i] = obs[s2].O[i]; + for (int i = 0; i < 3; ++i) Dref[i] = obs[s3].O[i]; + + int ijkl = 0; + + int l0, m0, n0; + FOR_CART(l0, m0, n0, obs[s0].contr[0].l) + int l1, m1, n1; + FOR_CART(l1, m1, n1, obs[s1].contr[0].l) + int l2, m2, n2; + FOR_CART(l2, m2, n2, obs[s2].contr[0].l) + int l3, m3, n3; + FOR_CART(l3, m3, n3, obs[s3].contr[0].l) + + // Raw 3x3 dyadic T_{ab} = (a b∂_a | c d∂_b), accumulated in + // chemist-notation index 3*a+b. After the primitive loop we + // project into the 9 SO(3) irreducible components that the engine + // returns: Scalar, AntisymX/Y/Z, SymTLDiagA/B, SymTLOffXY/XZ/YZ. + std::array ref_raw{}; + ref_raw.fill(0.0); + + for (uint p0 = 0; p0 < obs[s0].nprim(); p0++) { + for (uint p1 = 0; p1 < obs[s1].nprim(); p1++) { + for (uint p2 = 0; p2 < obs[s2].nprim(); p2++) { + for (uint p3 = 0; p3 < obs[s3].nprim(); p3++) { + const LIBINT2_REF_REALTYPE alpha0 = obs[s0].alpha[p0]; + const LIBINT2_REF_REALTYPE alpha1 = obs[s1].alpha[p1]; + const LIBINT2_REF_REALTYPE alpha2 = obs[s2].alpha[p2]; + const LIBINT2_REF_REALTYPE alpha3 = obs[s3].alpha[p3]; + const LIBINT2_REF_REALTYPE c0 = obs[s0].contr[0].coeff[p0]; + const LIBINT2_REF_REALTYPE c1 = obs[s1].contr[0].coeff[p1]; + const LIBINT2_REF_REALTYPE c2 = obs[s2].contr[0].coeff[p2]; + const LIBINT2_REF_REALTYPE c3 = obs[s3].contr[0].coeff[p3]; + const LIBINT2_REF_REALTYPE c0123 = c0 * c1 * c2 * c3; + + // Deriv on ν (center B, index 1) and on λ (center D, idx + // 3). + auto didx_bd = [](int a, int b) -> der_idx { + der_idx r = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + r[3 + a] = 1; + r[9 + b] = 1; + return r; + }; + auto D = [&](int a, int b) { + auto di = didx_bd(a, b); + return eri(di.data(), l0, m0, n0, alpha0, Aref, l1, m1, + n1, alpha1, Bref, l2, m2, n2, alpha2, Cref, l3, + m3, n3, alpha3, Dref, 0); + }; + for (int a = 0; a < 3; ++a) + for (int b = 0; b < 3; ++b) + ref_raw[3 * a + b] += c0123 * D(a, b); + } + } + } + } + + // Project raw dyadic into the 9 SO(3) irrep components used by + // op_coulomb_op (must match σpCoulombσp_Descr::Component order). + const auto Txx = ref_raw[0]; + const auto Txy = ref_raw[1]; + const auto Txz = ref_raw[2]; + const auto Tyx = ref_raw[3]; + const auto Tyy = ref_raw[4]; + const auto Tyz = ref_raw[5]; + const auto Tzx = ref_raw[6]; + const auto Tzy = ref_raw[7]; + const auto Tzz = ref_raw[8]; + std::array ref_op_coulomb_op{ + Txx + Tyy + Tzz, // Scalar + Tyz - Tzy, // AntisymX + Tzx - Txz, // AntisymY + Txy - Tyx, // AntisymZ + Txx - Tyy, // SymTLDiagA + 2.0 * Tzz - Txx - Tyy, // SymTLDiagB + Txy + Tyx, // SymTLOffXY + Txz + Tzx, // SymTLOffXZ + Tyz + Tzy, // SymTLOffYZ + }; + + const double ABSOLUTE_DEVIATION_THRESHOLD = 5.0E-14; + const double RELATIVE_DEVIATION_THRESHOLD = 1.0E-9; + for (auto comp = 0; comp < 9; ++comp) { + auto abs_err = abs(ref_op_coulomb_op[comp] - results[comp][ijkl]); + auto rel_abs_err = abs(abs_err / ref_op_coulomb_op[comp]); + bool not_ok = rel_abs_err > RELATIVE_DEVIATION_THRESHOLD && + abs_err > ABSOLUTE_DEVIATION_THRESHOLD; + if (not_ok) { + std::cout << "(l0 l1| l2 l3) = (" << s0 << " " << s1 << " | " + << s2 << " " << s3 << ") Elem " << ijkl + << " comp= " << comp + << " : ref = " << ref_op_coulomb_op[comp] + << " libint = " << results[comp][ijkl] + << " relabs_error = " << rel_abs_err + << " abs_error = " << abs_err << std::endl; + } + REQUIRE(!not_ok); + } + + ++ijkl; + END_FOR_CART + END_FOR_CART + END_FOR_CART + END_FOR_CART + } + } + } + } + } + +#endif // 4-center op_coulomb_op + +// The 3-center RKB sections below exercise kernels that exist only when +// LIBINT2_ENABLE_RKB_ERI3 >= 0 (else compute() throws Engine::missing_kernel). +// Guard at compile time on the generated task-exists macros. +#if LIBINT2_TASK_EXISTS_3coulomb_opop && LIBINT2_TASK_EXISTS_4coulomb_opop + SECTION("Coulombσpσp 3-center xs_xx") { + // 3-center RKB Coulombσpσp integral, (P | σ·p_μ σ·p_ν / r12) with P on + // the bra (fitting/DF) and σ·p acting on the AO pair (μ,ν) on the ket. + // + // Reference: the same operator and BraKet computed via the 4-center + // engine with Shell::unit() at the dummy bra position. The 4-center + // engine path is already validated by the "Coulombσpσp and + // σpσpCoulombσpσp" SECTION above against a primitive-eri reference, so + // by transitivity the 4-center engine is a trustworthy reference for the + // 3-center engine. Both code paths share the dummy-shell trick + // internally, but they are independently generated (different task + // labels, different dispatch tables), so this catches dispatch-table / + // codegen wiring mistakes in the new 3-center path. + + Shell dfsh{{1.5}, {{0, false, {1.0}}}, {{-1.0, 0.5, 0.0}}}; + std::vector dfs{dfsh}; + const auto &unitshell = libint2::Shell::unit(); + + const auto obs = shells_upto( + std::min(LIBINT2_MAX_AM_3coulomb_opop, LIBINT2_MAX_AM_coulomb_opop)); + const auto max_nprim = libint2::max_nprim(obs); + const auto max_l = libint2::max_l(obs); + Engine engine_3c(Operator::coulomb_opop, + std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0); + engine_3c.set(BraKet::xs_xx); + // 4-center reference — same operator, default BraKet::xx_xx + Engine engine_4c_ref(Operator::coulomb_opop, + std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0); + + const auto nshell = obs.size(); + for (int sa = 0; sa != nshell; ++sa) { + for (int sb = 0; sb != nshell; ++sb) { + const auto &results_3c = engine_3c.compute(dfsh, obs[sa], obs[sb]); + // 4-center reference: (dfsh, unit | sh_a, sh_b). σ·p still acts on + // (C, D) = (sh_a, sh_b), matching the 3-center xs_xx mapping. + const auto &results_4c_ref = + engine_4c_ref.compute(dfsh, unitshell, obs[sa], obs[sb]); + REQUIRE(results_3c.size() == 4); + REQUIRE(results_4c_ref.size() == 4); + + const auto n_df = dfsh.size(); + const auto n_a = obs[sa].size(); + const auto n_b = obs[sb].size(); + const auto n_total = n_df * n_a * n_b; + // 4-center buffer is (df, unit, sa, sb); n_unit = 1; so the linear + // layout coincides with the 3-center (df, sa, sb) layout. + + const double ABS_TOL = 5.0E-14; + const double REL_TOL = 1.0E-9; + for (auto comp = 0; comp < 4; ++comp) { + for (size_t k = 0; k < n_total; ++k) { + const auto v_3c = results_3c[comp][k]; + const auto v_ref = results_4c_ref[comp][k]; + const auto abs_err = std::abs(v_3c - v_ref); + const auto rel_err = + std::abs(v_ref) > 1e-30 ? std::abs(abs_err / v_ref) : 0.0; + bool not_ok = rel_err > REL_TOL && abs_err > ABS_TOL; + if (not_ok) { + std::cout << "(df | sa=" << sa << " sb=" << sb + << ") comp=" << comp << " elem=" << k << ": 3c=" << v_3c + << " 4c_ref=" << v_ref << " abs_err=" << abs_err + << " rel_err=" << rel_err << std::endl; + } + REQUIRE(!not_ok); + } + } + } + } + } + + SECTION("Coulombσpσp 3-center xx_xs alias") { + // The xx_xs braket re-routes through the same xs_xx kernel via the + // Engine::compute2 bra↔ket swap. Verify the alias yields the same + // integral values, with the output buffer in the user-requested + // (sh_a, sh_b | dfsh) index layout. + + Shell dfsh{{1.5}, {{0, false, {1.0}}}, {{-1.0, 0.5, 0.0}}}; + std::vector dfs{dfsh}; + + // the AO pair is limited by LIBINT2_MAX_AM, the fitting center by + // LIBINT2_RKB_ERI3_MAX_AM + const auto obs = shells_upto( + std::min(LIBINT2_MAX_AM_3coulomb_opop, LIBINT2_MAX_AM_default)); + const auto max_nprim = libint2::max_nprim(obs); + const auto max_l = libint2::max_l(obs); + // pass the braket to the constructor: the default (4-center) braket may + // not support max_l if LIBINT2_RKB_ERI_MAX_AM < LIBINT2_RKB_ERI3_MAX_AM + const auto eps = std::numeric_limits::epsilon(); + Engine eng_xs( + Operator::coulomb_opop, std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0, eps, + libint2::default_params(Operator::coulomb_opop), BraKet::xs_xx); + Engine eng_xxs( + Operator::coulomb_opop, std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0, eps, + libint2::default_params(Operator::coulomb_opop), BraKet::xx_xs); + + for (size_t sa = 0; sa != obs.size(); ++sa) { + for (size_t sb = 0; sb != obs.size(); ++sb) { + const auto n_df = dfsh.size(); + const auto n_a = obs[sa].size(); + const auto n_b = obs[sb].size(); + + const auto &res_xs = eng_xs.compute(dfsh, obs[sa], obs[sb]); + // re-set xs braket each loop in case Catch2 SECTION re-entry resets + eng_xxs.set(BraKet::xx_xs); + const auto &res_xxs = eng_xxs.compute(obs[sa], obs[sb], dfsh); + + for (auto c = 0; c < 4; ++c) { + for (size_t i_df = 0; i_df < n_df; ++i_df) { + for (size_t i_a = 0; i_a < n_a; ++i_a) { + for (size_t i_b = 0; i_b < n_b; ++i_b) { + const auto v_xs = res_xs[c][i_df * n_a * n_b + i_a * n_b + i_b]; + const auto v_xxs = + res_xxs[c][i_a * n_b * n_df + i_b * n_df + i_df]; + const auto abs_err = abs(v_xs - v_xxs); + const auto rel_err = + std::abs(v_xs) > 1e-30 ? double(abs(abs_err / v_xs)) : 0.0; + bool not_ok = rel_err > 1.0E-9 && abs_err > 5.0E-14; + if (not_ok) { + std::cout + << "xx_xs alias mismatch: (sa=" << sa << " sb=" << sb + << ") comp=" << c << " idx(df=" << i_df << ",a=" << i_a + << ",b=" << i_b << "): xs=" << v_xs << " xxs=" << v_xxs + << " abs_err=" << abs_err << std::endl; + } + REQUIRE(!not_ok); + } + } + } + } + } + } + } +#endif // 3-center Coulombσpσp + +#if LIBINT2_TASK_EXISTS_3coulomb_op + SECTION("Coulombσp 3-center xs_xx") { + // 3-center single-σ·p integral + // (P | 1/r12 | μ, σ·p ν), + // with the fitting function P on the bra (BraKet::xs_xx) and a single σ·p + // acting on the 2nd ket AO function ν only. The 3 Cartesian components + // (x,y,z) are the bare gradient of ν, so the reference is the 3-center ERI + // with a *single first derivative on ν* (the shell that carries σ·p) — the + // other geometric derivatives are not needed. + // + // The 3-center ERI is evaluated with the primitive eri() reference by + // placing a unit s-shell (alpha=0, at the origin) at the dummy bra-2 + // position, exactly as the engine does internally via Shell::unit(): the + // 3-arg compute(df, μ, ν) maps to the 4-center order (df, unit, μ, ν) = + // (A,B,C,D), so σ·p on ν is the derivative on center D (der_idx slots + // 9..11). See the "Coulombσpσp and σpσpCoulombσpσp" SECTION for the + // (un-derivatived) primitive-eri reference pattern. + + Shell dfsh{{1.5}, {{0, false, {1.0}}}, {{-1.0, 0.5, 0.0}}}; + std::vector dfs{dfsh}; + + const auto obs = shells_upto( + std::min(LIBINT2_MAX_AM_3coulomb_op, LIBINT2_MAX_AM_default)); + const auto max_nprim = libint2::max_nprim(obs); + const auto max_l = libint2::max_l(obs); + Engine engine_3c(Operator::coulomb_op, + std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0); + engine_3c.set(BraKet::xs_xx); + // coulomb_op has no 4-center kernel, hence default_braket(coulomb_op) + // must be the 3-center xs_xx, so engine_3c_default — built WITHOUT an + // explicit set() — must produce results identical to engine_3c. + CHECK(default_braket(Operator::coulomb_op) == BraKet::xs_xx); + Engine engine_3c_default(Operator::coulomb_op, + std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0); + // no 4-center coulomb_op kernel -> must throw rather than silently + // computing plain Coulomb integrals + { + Engine engine_4c(Operator::coulomb_op, + std::max(max_nprim, libint2::max_nprim(dfs)), + std::max(max_l, libint2::max_l(dfs)), 0); + engine_4c.set(BraKet::xx_xx); + CHECK_THROWS_AS(engine_4c.compute(obs[0], obs[0], obs[0], obs[0]), + Engine::missing_kernel); + } + + // unit (dummy) shell at the bra-2 position: alpha=0, l=0, at the origin. + LIBINT2_REF_REALTYPE Bref[3] = {0.0, 0.0, 0.0}; + const LIBINT2_REF_REALTYPE alpha_unit = 0.0; + + const auto nshell = obs.size(); + for (int sa = 0; sa != nshell; ++sa) { + for (int sb = 0; sb != nshell; ++sb) { + const auto &results_3c = engine_3c.compute(dfsh, obs[sa], obs[sb]); + REQUIRE(results_3c.size() == 3); + // default-braket engine (no set()) must match the explicit-xs_xx engine + const auto &results_3c_default = + engine_3c_default.compute(dfsh, obs[sa], obs[sb]); + REQUIRE(results_3c_default.size() == 3); + + LIBINT2_REF_REALTYPE Aref[3], Cref[3], Dref[3]; + for (int i = 0; i < 3; ++i) Aref[i] = dfsh.O[i]; + for (int i = 0; i < 3; ++i) Cref[i] = obs[sa].O[i]; + for (int i = 0; i < 3; ++i) Dref[i] = obs[sb].O[i]; + + int ijk = 0; + + int l0, m0, n0; + FOR_CART(l0, m0, n0, dfsh.contr[0].l) // P (fitting, s) + int l2, m2, n2; + FOR_CART(l2, m2, n2, obs[sa].contr[0].l) // μ + int l3, m3, n3; + FOR_CART(l3, m3, n3, obs[sb].contr[0].l) // ν (σ·p acts here) + + std::array ref_coulomb_op{0.0, 0.0, 0.0}; + + for (uint p0 = 0; p0 < dfsh.nprim(); p0++) { + for (uint p2 = 0; p2 < obs[sa].nprim(); p2++) { + for (uint p3 = 0; p3 < obs[sb].nprim(); p3++) { + const LIBINT2_REF_REALTYPE alpha0 = dfsh.alpha[p0]; + const LIBINT2_REF_REALTYPE alpha2 = obs[sa].alpha[p2]; + const LIBINT2_REF_REALTYPE alpha3 = obs[sb].alpha[p3]; + const LIBINT2_REF_REALTYPE c0 = dfsh.contr[0].coeff[p0]; + const LIBINT2_REF_REALTYPE c2 = obs[sa].contr[0].coeff[p2]; + const LIBINT2_REF_REALTYPE c3 = obs[sb].contr[0].coeff[p3]; + const LIBINT2_REF_REALTYPE c023 = c0 * c2 * c3; // unit coeff = 1 + + // single first derivative on ν (center D), direction k. + auto eri_dD = [&](int k) { + der_idx d = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + d[9 + k] = 1; // slots 9,10,11 = D_x, D_y, D_z + return eri(d.data(), l0, m0, n0, alpha0, Aref, 0, 0, 0, + alpha_unit, Bref, l2, m2, n2, alpha2, Cref, l3, m3, + n3, alpha3, Dref, 0); + }; + + for (int k = 0; k < 3; ++k) ref_coulomb_op[k] += c023 * eri_dD(k); + } + } + } + + const double ABS_TOL = 5.0E-14; + const double REL_TOL = 1.0E-9; + for (auto comp = 0; comp < 3; ++comp) { + const auto v_3c = results_3c[comp][ijk]; + CHECK(results_3c_default[comp][ijk] == v_3c); + const auto v_ref = ref_coulomb_op[comp]; + const auto abs_err = std::abs(v_3c - v_ref); + const auto rel_err = + std::abs(v_ref) > 1e-30 ? std::abs(abs_err / v_ref) : 0.0; + bool not_ok = rel_err > REL_TOL && abs_err > ABS_TOL; + if (not_ok) { + std::cout << "(df | sa=" << sa << " sb=" << sb << ") comp=" << comp + << " elem=" << ijk << ": 3c=" << v_3c << " ref=" << v_ref + << " abs_err=" << abs_err << " rel_err=" << rel_err + << std::endl; + } + REQUIRE(!not_ok); + } + + ++ijk; + END_FOR_CART + END_FOR_CART + END_FOR_CART + } + } + } +#endif // LIBINT2_TASK_EXISTS_3coulomb_op +} + +// RKB integrals over solid harmonics shells. The RKB reference tests above use +// cartesian shells only; the engine's cartesian-to-solid-harmonics transform +// shares the scratch/permute path with the per-component sign corrections of +// the RKB operators, so verify here that the two compose: the result for pure +// shells must equal the cartesian result transformed with the library's own +// solid harmonics routines. Also covers the xx_xs alias of coulomb_op and a +// solid harmonics fitting center. +namespace { + +typedef std::vector> rkb_shellsets_t; + +rkb_shellsets_t rkb_copy_results(const Engine::target_ptr_vec &results, + const std::vector &shells) { + size_t n = 1; + for (const auto &sh : shells) n *= sh.size(); + rkb_shellsets_t sets; + for (const auto *target : results) { + REQUIRE(target != nullptr); + sets.emplace_back(target, target + n); + } + return sets; +} + +/// transforms shell sets computed over @c src_shells to the basis of +/// @c tgt_shells; the two differ only in which shells are pure +rkb_shellsets_t rkb_to_pure(const rkb_shellsets_t &sets, + const std::vector &src_shells, + const std::vector &tgt_shells) { + REQUIRE(src_shells.size() == tgt_shells.size()); + const auto nsh = src_shells.size(); + rkb_shellsets_t result; + for (const auto &set : sets) { + auto src = set; + for (size_t k = 0; k != nsh; ++k) { + const auto l = src_shells[k].contr[0].l; + REQUIRE(l == tgt_shells[k].contr[0].l); + if (src_shells[k].contr[0].pure == tgt_shells[k].contr[0].pure || l == 0) + continue; + REQUIRE(tgt_shells[k].contr[0].pure); + // dimensions before k are already transformed, those after are not + size_t n1 = 1, n2 = 1; + for (size_t i = 0; i != k; ++i) n1 *= tgt_shells[i].size(); + for (size_t i = k + 1; i != nsh; ++i) n2 *= src_shells[i].size(); + std::vector tgt(n1 * (2 * l + 1) * n2); + if (k == 0) + libint2::solidharmonics::transform_first(l, n2, src.data(), tgt.data()); + else if (k + 1 == nsh) + libint2::solidharmonics::transform_last(n1, l, src.data(), tgt.data()); + else + libint2::solidharmonics::transform_inner(n1, l, n2, src.data(), + tgt.data()); + src = std::move(tgt); + } + result.push_back(std::move(src)); + } + return result; +} + +void rkb_require_equal(const rkb_shellsets_t &a, const rkb_shellsets_t &b, + const std::string &what) { + REQUIRE(a.size() == b.size()); + for (size_t c = 0; c != a.size(); ++c) { + REQUIRE(a[c].size() == b[c].size()); + for (size_t i = 0; i != a[c].size(); ++i) { + const auto abs_err = std::abs(a[c][i] - b[c][i]); + const auto rel_err = + std::abs(b[c][i]) > 1e-30 ? abs_err / std::abs(b[c][i]) : 0.0; + INFO(what << " comp=" << c << " elem=" << i << " value=" << a[c][i] + << " ref=" << b[c][i]); + REQUIRE(!(rel_err > 1e-10 && abs_err > 1e-13)); + } + } +} + +Shell rkb_shell(const Shell &sh, bool pure) { + auto result = sh; + result.contr[0].pure = pure; + return result; +} + +} // namespace + +TEST_CASE("RKB integrals with solid harmonics shells", "[engine][2-body]") { + // this TU includes test_eri/eri.h, which declares libint2::parity; make sure + // the solid harmonics coefficients computed from this TU's instantiation + // (which wins at link time on some toolchains) do not pick it up + REQUIRE(libint2::solidharmonics::SolidHarmonicsCoefficients::coeff( + 2, 0, 2, 0, 0) == Approx(-0.5)); + // cartesian s, p, contracted p, d; the solid harmonics variants of the same + const std::vector cart_shells{ + Shell{{1.0}, {{0, false, {1.0}}}, {{0.0, 0.0, 0.0}}}, + Shell{{2.0}, {{1, false, {1.0}}}, {{1.0, 1.0, 1.0}}}, + Shell{{3.0, 0.7}, {{1, false, {0.4, 0.8}}}, {{-0.5, 0.2, 0.9}}}, + Shell{{1.2}, {{2, false, {1.0}}}, {{0.3, -0.8, 0.4}}}}; + std::vector pure_shells; + for (const auto &sh : cart_shells) pure_shells.push_back(rkb_shell(sh, true)); + const auto nprim = libint2::max_nprim(cart_shells); + + auto upto = [](const std::vector &shells, int lmax) { + std::vector result; + for (const auto &sh : shells) + if (sh.contr[0].l <= lmax) result.push_back(sh); + return result; + }; + + // 4-center: compare pure vs transformed cartesian for every quartet + auto check_4center = [&](Operator oper, int lmax_oper) { + const auto cart = upto(cart_shells, lmax_oper); + const auto pure = upto(pure_shells, lmax_oper); + Engine engine(oper, nprim, libint2::max_l(cart), 0); + const auto nsh = cart.size(); + for (size_t a = 0; a != nsh; ++a) + for (size_t b = 0; b != nsh; ++b) + for (size_t c = 0; c != nsh; ++c) + for (size_t d = 0; d != nsh; ++d) { + const std::vector cs{cart[a], cart[b], cart[c], cart[d]}; + const std::vector ps{pure[a], pure[b], pure[c], pure[d]}; + const auto ref = + rkb_to_pure(rkb_copy_results( + engine.compute(cs[0], cs[1], cs[2], cs[3]), cs), + cs, ps); + const auto val = rkb_copy_results( + engine.compute(ps[0], ps[1], ps[2], ps[3]), ps); + std::ostringstream oss; + oss << "oper=" << static_cast(oper) << " (" << a << b << "|" + << c << d << ")"; + rkb_require_equal(val, ref, oss.str()); + } + }; + +#if LIBINT2_TASK_EXISTS_4coulomb_opop + SECTION("coulomb_opop 4-center") { + check_4center(Operator::coulomb_opop, LIBINT2_MAX_AM_coulomb_opop); + } +#endif +#if LIBINT2_TASK_EXISTS_4opop_coulomb_opop + SECTION("opop_coulomb_opop 4-center") { + check_4center(Operator::opop_coulomb_opop, + LIBINT2_MAX_AM_opop_coulomb_opop); + } +#endif +#if LIBINT2_TASK_EXISTS_4op_coulomb_op + SECTION("op_coulomb_op 4-center") { + check_4center(Operator::op_coulomb_op, LIBINT2_MAX_AM_op_coulomb_op); + } +#endif + + // 3-center: fitting shells s and d, AO shells s, p, d, cartesian vs pure, + // both brakets + auto check_3center = [&](Operator oper, int lmax_fit, int lmax_ao) { + const std::vector fit_pure{ + Shell{{1.5}, {{0, true, {1.0}}}, {{-1.0, 0.5, 0.0}}}, + Shell{{0.9}, {{2, true, {1.0}}}, {{-1.0, 0.5, 0.0}}}}; + const auto fit = upto(fit_pure, lmax_fit); + const auto cart = upto(cart_shells, lmax_ao); + const auto pure = upto(pure_shells, lmax_ao); + const auto nprim_3c = std::max(nprim, libint2::max_nprim(fit)); + const auto lmax_3c = std::max(libint2::max_l(cart), libint2::max_l(fit)); + const auto eps = std::numeric_limits::epsilon(); + Engine eng_xs(oper, nprim_3c, lmax_3c, 0, eps, + libint2::default_params(oper), BraKet::xs_xx); + Engine eng_xxs(oper, nprim_3c, lmax_3c, 0, eps, + libint2::default_params(oper), BraKet::xx_xs); + + for (size_t p = 0; p != fit.size(); ++p) { + const auto fit_ref = rkb_shell(fit[p], false); + for (size_t a = 0; a != cart.size(); ++a) + for (size_t b = 0; b != cart.size(); ++b) { + const std::vector rs{fit_ref, cart[a], cart[b]}; + const std::vector ps{fit[p], pure[a], pure[b]}; + const auto ref = rkb_to_pure( + rkb_copy_results(eng_xs.compute(rs[0], rs[1], rs[2]), rs), rs, + ps); + const auto val = + rkb_copy_results(eng_xs.compute(ps[0], ps[1], ps[2]), ps); + std::ostringstream oss; + oss << "oper=" << static_cast(oper) << " (" << p << "|" << a << b + << ")"; + rkb_require_equal(val, ref, oss.str() + " xs_xx"); + + // xx_xs alias: same integrals, (a b | fit) layout + const auto val_xxs = + rkb_copy_results(eng_xxs.compute(ps[1], ps[2], ps[0]), + std::vector{ps[1], ps[2], ps[0]}); + const auto n_fit = ps[0].size(), n_a = ps[1].size(), + n_b = ps[2].size(); + rkb_shellsets_t val_xxs_as_xs(val_xxs.size()); + for (size_t c = 0; c != val_xxs.size(); ++c) { + val_xxs_as_xs[c].resize(n_fit * n_a * n_b); + for (size_t i_f = 0; i_f != n_fit; ++i_f) + for (size_t i_a = 0; i_a != n_a; ++i_a) + for (size_t i_b = 0; i_b != n_b; ++i_b) + val_xxs_as_xs[c][(i_f * n_a + i_a) * n_b + i_b] = + val_xxs[c][(i_a * n_b + i_b) * n_fit + i_f]; + } + rkb_require_equal(val_xxs_as_xs, val, oss.str() + " xx_xs"); + } + } + }; + +#if LIBINT2_TASK_EXISTS_3coulomb_opop + SECTION("coulomb_opop 3-center") { + check_3center(Operator::coulomb_opop, LIBINT2_MAX_AM_3coulomb_opop, + LIBINT2_MAX_AM_default); + } +#if LIBINT2_TASK_EXISTS_4coulomb_opop + SECTION("coulomb_opop 3-center vs 4-center, pure fitting shell") { + // the 3-center kernel is generated with a dummy shell next to the fitting + // shell; check against the 4-center kernel, which has no such structure + const Shell fit{{0.9}, {{2, true, {1.0}}}, {{-1.0, 0.5, 0.0}}}; + const auto lmax_ao = std::min( + LIBINT2_MAX_AM_default, + std::min(LIBINT2_MAX_AM_3coulomb_opop, LIBINT2_MAX_AM_coulomb_opop)); + if (LIBINT2_MAX_AM_3coulomb_opop >= 2 && LIBINT2_MAX_AM_coulomb_opop >= 2) { + const auto pure = upto(pure_shells, lmax_ao); + const auto eps = std::numeric_limits::epsilon(); + Engine eng_3c(Operator::coulomb_opop, nprim, 2, 0, eps, + libint2::default_params(Operator::coulomb_opop), + BraKet::xs_xx); + Engine eng_4c(Operator::coulomb_opop, nprim, 2, 0); + for (size_t a = 0; a != pure.size(); ++a) + for (size_t b = 0; b != pure.size(); ++b) { + const std::vector ps{fit, pure[a], pure[b]}; + const auto val = + rkb_copy_results(eng_3c.compute(fit, pure[a], pure[b]), ps); + const auto ref = rkb_copy_results( + eng_4c.compute(fit, Shell::unit(), pure[a], pure[b]), ps); + std::ostringstream oss; + oss << "(d|" << a << b << ") 3c vs 4c"; + rkb_require_equal(val, ref, oss.str()); + } + } + } +#endif +#endif +#if LIBINT2_TASK_EXISTS_3coulomb_op + SECTION("coulomb_op 3-center") { + check_3center(Operator::coulomb_op, LIBINT2_MAX_AM_3coulomb_op, + LIBINT2_MAX_AM_default); + } +#endif } TEST_CASE("Erfx_Coulomb integrals", "[engine][2-body]") { +#if defined(LIBINT2_SUPPORT_ERI) // pseudorandom s shells std::vector obs{ Shell{{1.0}, {{0, true, {1.0}}}, {{0.0, 0.0, 0.0}}}, @@ -509,12 +1695,12 @@ TEST_CASE("Erfx_Coulomb integrals", "[engine][2-body]") { REQUIRE(results[0] != nullptr); switch (k) { /* VALIDATION WOLFRAM CODE: -(* Integral of Coulomb kernel damped by (\[Lambda] Erf[\[Omega] r] + \ -\[Sigma] Erfc[\[Omega] r]), over unit-normalized s functions, \ -see Eq 52 in DOI 10.1039/b605188j *) -F0[T_] := If[T == 0, 1, Sqrt[\[Pi]/T]*Erf[Sqrt[T]]/2]; -sN[a_] := ((2 a)/\[Pi])^(3/4); -VVeeErfx[\[Alpha]1_, A1_List, \[Alpha]2_, A2_List, \[Beta]1_, + (* Integral of Coulomb kernel damped by (\[Lambda] Erf[\[Omega] r] + \ + \[Sigma] Erfc[\[Omega] r]), over unit-normalized s functions, \ + see Eq 52 in DOI 10.1039/b605188j *) + F0[T_] := If[T == 0, 1, Sqrt[\[Pi]/T]*Erf[Sqrt[T]]/2]; + sN[a_] := ((2 a)/\[Pi])^(3/4); + VVeeErfx[\[Alpha]1_, A1_List, \[Alpha]2_, A2_List, \[Beta]1_, B1_List, \[Beta]2_, B2_List, \[Omega]_, \[Lambda]_, \[Sigma]_] := Module[{\[Gamma]1, \[Gamma]2, P1, P2, K1, K2, T, result, \[Rho]}, \[Gamma]1 = \[Alpha]1 + \[Beta]1; @@ -532,13 +1718,13 @@ VVeeErfx[\[Alpha]1_, A1_List, \[Alpha]2_, A2_List, \[Beta]1_, T]) sN[\[Alpha]1] sN[\[Alpha]2] sN[\[Beta]1] sN[\[Beta]2]; Return[result]; ]; -Print[CForm[ + Print[CForm[ N[VVeeErfx[1, {0, 0, 0}, 3, {2, 2, 2}, 2, {1, 1, 1}, 4, {3, 3, 3}, 1.1, 1, 0], 20]]] -Print[CForm[ + Print[CForm[ N[VVeeErfx[1, {0, 0, 0}, 3, {2, 2, 2}, 2, {1, 1, 1}, 4, {3, 3, 3}, 1.1, 0, 1], 20]]] -Print[CForm[ + Print[CForm[ N[VVeeErfx[1, {0, 0, 0}, 3, {2, 2, 2}, 2, {1, 1, 1}, 4, {3, 3, 3}, 1.1, 2, 3], 20]]] */ @@ -554,4 +1740,5 @@ Print[CForm[ } } } +#endif // LIBINT2_SUPPORT_ERI } diff --git a/include/libint2/config.h.cmake.in b/include/libint2/config.h.cmake.in index 2e686c147..b0d0c790e 100644 --- a/include/libint2/config.h.cmake.in +++ b/include/libint2/config.h.cmake.in @@ -77,6 +77,19 @@ #undef LIBINT_INCLUDE_ERI #endif +/* Support 4-center RKB ERI derivatives up to this order */ +#define LIBINT_INCLUDE_RKB_ERI @LIBINT_INCLUDE_RKB_ERI@ +#if @LIBINT_INCLUDE_RKB_ERI@ == -1 +#undef LIBINT_INCLUDE_RKB_ERI +#endif + +/* Support 3-center RKB ERI derivatives up to this order */ +#define LIBINT_INCLUDE_RKB_ERI3 @LIBINT_INCLUDE_RKB_ERI3@ +#if @LIBINT_INCLUDE_RKB_ERI3@ == -1 +#undef LIBINT_INCLUDE_RKB_ERI3 +#endif + + /* Support 3-center ERI derivatives up to this order */ #define LIBINT_INCLUDE_ERI3 @LIBINT_INCLUDE_ERI3@ #if @LIBINT_INCLUDE_ERI3@ == -1 @@ -140,7 +153,46 @@ /* Max optimized AM for ERI and its derivatives */ #cmakedefine LIBINT_ERI_OPT_AM_LIST "@LIBINT_ERI_OPT_AM_LIST@" -/* Max AM for 3-center ERI (same for all derivatives; if not defined see LIBINT_ERI3_MAX_AM_LIST) */ +/* Max AM for RKB_ERI (same for all derivatives; if not defined see LIBINT_RKB_ERI_MAX_AM_LIST) */ +#define LIBINT_RKB_ERI_MAX_AM @LIBINT_RKB_ERI_MAX_AM@ +#if @LIBINT_RKB_ERI_MAX_AM@ == -1 +#undef LIBINT_RKB_ERI_MAX_AM +#endif + +/* Max AM for RKB_ERI and its derivatives */ +#cmakedefine LIBINT_RKB_ERI_MAX_AM_LIST "@LIBINT_RKB_ERI_MAX_AM_LIST@" + +/* Max optimized AM for RKB_ERI (same for all derivatives; if not defined see LIBINT_RKB_ERI_OPT_AM_LIST) */ +#define LIBINT_RKB_ERI_OPT_AM @LIBINT_RKB_ERI_OPT_AM@ +#if @LIBINT_RKB_ERI_OPT_AM@ == -1 +#undef LIBINT_RKB_ERI_OPT_AM +#endif + +/* Max optimized AM for RKB_ERI and its derivatives */ +#cmakedefine LIBINT_RKB_ERI_OPT_AM_LIST "@LIBINT_RKB_ERI_OPT_AM_LIST@" + +/* Max AM for the 3-center RKB ERI density fitting (auxiliary/DF) basis on the single fitting center + (same for all derivatives; if not defined see LIBINT_RKB_ERI3_MAX_AM_LIST). + The paired AO centers (on which σ·p acts) use LIBINT_MAX_AM. */ +#define LIBINT_RKB_ERI3_MAX_AM @LIBINT_RKB_ERI3_MAX_AM@ +#if @LIBINT_RKB_ERI3_MAX_AM@ == -1 +#undef LIBINT_RKB_ERI3_MAX_AM +#endif + +/* Max AM for 3-center RKB ERI and its derivatives */ +#cmakedefine LIBINT_RKB_ERI3_MAX_AM_LIST "@LIBINT_RKB_ERI3_MAX_AM_LIST@" + +/* Max optimized AM for 3-center RKB ERI (same for all derivatives; if not defined see LIBINT_RKB_ERI3_OPT_AM_LIST) */ +#define LIBINT_RKB_ERI3_OPT_AM @LIBINT_RKB_ERI3_OPT_AM@ +#if @LIBINT_RKB_ERI3_OPT_AM@ == -1 +#undef LIBINT_RKB_ERI3_OPT_AM +#endif + +/* Max optimized AM for 3-center RKB ERI and its derivatives */ +#cmakedefine LIBINT_RKB_ERI3_OPT_AM_LIST "@LIBINT_RKB_ERI3_OPT_AM_LIST@" + +/* Max AM for the 3-center ERI density fitting (auxiliary/DF) basis on the single fitting center + (same for all derivatives; if not defined see LIBINT_ERI3_MAX_AM_LIST). The paired AO centers use LIBINT_MAX_AM. */ #define LIBINT_ERI3_MAX_AM @LIBINT_ERI3_MAX_AM@ #if @LIBINT_ERI3_MAX_AM@ == -1 #undef LIBINT_ERI3_MAX_AM diff --git a/include/libint2/cxxapi.h b/include/libint2/cxxapi.h index 3e376cbcd..b906590ae 100644 --- a/include/libint2/cxxapi.h +++ b/include/libint2/cxxapi.h @@ -38,11 +38,12 @@ "C++11 API does not support with user-defined real types yet; omit --with-real-type when configuring" #endif -#if !defined(LIBINT_INCLUDE_ONEBODY) || \ - !(defined(LIBINT_INCLUDE_ERI) || defined(LIBINT_INCLUDE_ERI3) || \ - defined(LIBINT_INCLUDE_ERI2)) +#if !defined(LIBINT_INCLUDE_ONEBODY) || \ + !(defined(LIBINT_INCLUDE_ERI) || defined(LIBINT_INCLUDE_ERI3) || \ + defined(LIBINT_INCLUDE_ERI2) || defined(LIBINT_INCLUDE_RKB_ERI) || \ + defined(LIBINT_INCLUDE_RKB_ERI3)) #error \ - "C++ API is only supported if both 1-body and some (eri, eri3, eri2) 2-body integrals are enabled" + "C++ API is only supported if both 1-body and some (eri, eri3, eri2, rkb_eri, rkb_eri3) 2-body integrals are enabled" #endif #include diff --git a/include/libint2/deriv_map.h b/include/libint2/deriv_map.h index 31d6aefb2..aaf82be05 100644 --- a/include/libint2/deriv_map.h +++ b/include/libint2/deriv_map.h @@ -32,6 +32,7 @@ #include #include #include +#include #include namespace libint2 { @@ -94,11 +95,15 @@ class DerivMapGenerator { return braket_xs_xx()[deriv_order_ - 1]; } default: - assert(false && - "Derivative index permutation maps for this braket type not yet " - "supported."); + // Derivative index-permutation maps are implemented only for xx_xx and + // xs_xx. Other brakets (e.g. xx_xs) are accepted for energies — where + // they canonicalize to xs_xx — but have no derivative map, so fail with + // a catchable error rather than std::abort()-ing the host process. + throw std::logic_error( + "DerivMapGenerator::instance(): derivative index permutation maps " + "are only implemented for the xx_xx and xs_xx brakets; " + "deriv_order > 0 is not supported for this braket."); } - std::abort(); } private: diff --git a/include/libint2/engine.h b/include/libint2/engine.h index bbb04274a..badd60654 100644 --- a/include/libint2/engine.h +++ b/include/libint2/engine.h @@ -192,6 +192,15 @@ enum class Operator { /// \sa operator_traits /// \sa Surjuse, Deng, Asadchev, Valeev, arXiv:2603.16989 (2025) op_q_gau_op, + /// (1-body) σp . r . σp, the σ·p-on-both-sides analog of the dipole moment. + /// Produces 12 components = 3 dipole directions × 4 Pauli quaternion + /// components (trace + 3 antisym), indexed as `4*k + q` with `k ∈ {x,y,z}` + /// the dipole direction and `q ∈ {0=trace, 1=σ_x, 2=σ_y, 3=σ_z}` the Pauli + /// piece. Origin set via `engine.set_params(std::array)`. + /// + /// Engine-enum names are ASCII (`op` = σ·p); the σ·p·r·σ·p math name σpeμσp + /// lives on the compiler side (σpeμσp_Descr) and in the codegen label. + opemuop, /// \f$ \delta(\vec{r}_1 - \vec{r}_2) \f$ delta, /// (2-body) Coulomb operator = \f$ r_{12}^{-1} \f$ @@ -231,18 +240,105 @@ enum class Operator { /// is equivalent to delcgtg2 with \f$ g \f$ ); params (no defaults, must be /// provided) are std::array{g_a, g_b} delcgtga_delcgtgb, + /// (2-body) \f$ r_{12}^{-1} (σ.p_{k1})(σ.p_{k2})\f$ where k1 & k2 are + /// centers of ket1 and ket2, respectively. Produces 4 components (see + /// \ref rkb_operators). For 3-center integrals the σ·p pair is the pair of + /// AO functions, whichever side of the braket it is on: with BraKet::xs_xx + /// σ·p acts on ket1 and ket2, with BraKet::xx_xs (the alias of xs_xx that + /// only changes the order of the arguments and of the result) σ·p acts on + /// bra1 and bra2 and the fitting function is ket1. + coulomb_opop, + /// (2-body) \f$ r_{12}^{-1} (σ.p_{k2}) \f$ where k2 is the center of ket2; + /// the 3-center single-σ·p integral (P | μ, σ·p ν). σ·p acts on + /// the 2nd ket AO function only. Produces 3 Cartesian components (x=0, y=1, + /// z=2), the derivatives of ν with respect to its center, i.e. the SO(3) + /// vector irrep without σ·σ folding (cf. coulomb_opop); the user multiplies + /// them by +i (see \ref rkb_operators). The + /// fitting function sits on the bra (BraKet::xs_xx); 3-center only. With + /// BraKet::xx_xs (the alias of xs_xx) σ·p acts on bra2, the 2nd AO + /// function, and the fitting function is ket1. + coulomb_op, + /// (2-body) \f$ (σ.p_{b1})(σ.p_{b2}) r_{12}^{-1} (σ.p_{k1})(σ.p_{k2})\f$ + /// where b1 & b2 are centers of bra1 and bra2 and k1 & k2 are centers of + /// ket1 and ket2, respectively + opop_coulomb_opop, + /// (2-body) \f$ (σ.p_{b2}) r_{12}^{-1} (σ.p_{k2}) \f$ where b2 is the center + /// of bra2 and k2 is the center of ket2; Gaunt LS "bilinear" integral. + /// Produces 9 components: the SO(3) irreducible decomposition of the 3×3 + /// derivative dyadic T_ab (a = bra-side ∇, b = ket-side ∇), NOT a raw + /// `3*a + b` Cartesian dyadic. Component order matches + /// `σpCoulombσp_Descr::Component`: Scalar trace (0); antisymmetric + /// (curl-curl) AntisymX,Y,Z (1-3); symmetric-traceless SymTLDiagA,SymTLDiagB + /// (4,5) and SymTLOffXY,SymTLOffXZ,SymTLOffYZ (6-8). Unlike coulomb_opop, + /// these components are not σ·σ-folded; the user multiplies them by -1 + /// (see \ref rkb_operators). + op_coulomb_op, // do not modify this invalid = -1, // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!keep this // updated!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! first_1body_oper = overlap, - last_1body_oper = op_q_gau_op, + last_1body_oper = opemuop, first_2body_oper = delta, - last_2body_oper = delcgtga_delcgtgb, + last_2body_oper = op_coulomb_op, first_oper = first_1body_oper, last_oper = last_2body_oper }; +// clang-format off +/** + \page rkb_operators Conventions for the relativistic (RKB) 2-body operators + + The four operators built from σ·p and 1/r₁₂ expose their components in + different ways; this table is the single reference for their layouts and + prefactors. + + All kernels evaluate integrals over derivatives of the basis functions with + respect to their *centers* (libint's derivative convention): for a function + φ_X centered at X, ∂_{X,k} φ_X ≡ ∂φ_X/∂X_k = −∂φ_X/∂r_k. Hence + p_k φ_X = −i ∂φ_X/∂r_k = +i ∂_{X,k} φ_X, and (p_k φ_X)* = −i ∂_{X,k} φ_X for + a real φ_X. In chemists' notation (ab|cd) the first function of each pair + (a, c) is complex conjugated; a σ·p that acts on a or c therefore contributes + −i, one that acts on b or d contributes +i. The "prefactor" column is what + the user multiplies the kernel's center-derivative integrals with to obtain + the σ·p integrals, with σ₁ the Pauli matrices in the spin space of electron 1 + (the pair ab) and σ₂ those of electron 2 (the pair cd). + + "Folded" means that the two σ of the same electron were contracted using + σ_i σ_j = δ_ij + i ε_ijk σ_k into a scalar (component 0) and a vector + (components 1-3) part; the user assembles the 2×2 spin block of that electron + as q₀·1 + i (q₁ σ_x + q₂ σ_y + q₃ σ_z) and must not apply the Pauli matrices + again. "Not folded" means the result is the spatial tensor and the user + contracts it with the Pauli matrices. + + | Engine (C++ API) | Compiler | task label | #comp | σ·p acts on | component index | prefactor | + |---------------------|-------------------|---------------------|-------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `coulomb_opop` | `Coulombσpσp` | `coulomb_opop` | 4 | c and d | 0: ∇_c·∇_d, 1-3: (∇_c×∇_d)_{x,y,z} (folded) | 1: (σ₂·p c)† (σ₂·p d) = Σ_ij (−i ∂_{C,i} c)(+i ∂_{D,j} d) σ_i σ_j = Σ_ij ∂_{C,i} c ∂_{D,j} d σ_i σ_j, i.e. the spin block is q₀ + i σ₂·q | + | `coulomb_op` | `Coulombσp` | `coulomb_op` | 3 | d | ∂_{D,x}, ∂_{D,y}, ∂_{D,z} of d (3-center only; not folded) | +i: (σ₂·p d) = +i Σ_k σ_{2,k} ∂_{D,k} d | + | `opop_coulomb_opop` | `σpσpCoulombσpσp` | `opop_coulomb_opop` | 16 | a, b, c, d | 4·q₁ + q₂, with q₁ the folded component of (σ₁·p a)† (σ₁·p b) and q₂ that of (σ₂·p c)† (σ₂·p d), each ∈ {0: ∇·∇, 1-3: (∇×∇)_{x,y,z}}; the sign of i² is folded into the q₁,q₂ ≥ 1 block | 1 for q₁ = q₂ = 0, i σ_{2,q₂} for q₁ = 0, i σ_{1,q₁} for q₂ = 0, σ_{1,q₁} σ_{2,q₂} for q₁,q₂ ≥ 1 (the −1 of i² is in the kernel); the center derivatives contribute no prefactor, as for `coulomb_opop` | + | `op_coulomb_op` | `σpCoulombσp` | `op_coulomb_op` | 9 | b and d | SO(3) irreps of T_ij = (a ∂_{B,i} b \| c ∂_{D,j} d): 0: trace, 1-3: T_yz−T_zy, T_zx−T_xz, T_xy−T_yx, 4: T_xx−T_yy, 5: 2T_zz−T_xx−T_yy, 6-8: T_xy+T_yx, T_xz+T_zx, T_yz+T_zy (not folded) | −1: (a σ₁·p b \| c σ₂·p d) = Σ_ij σ_{1,i} σ_{2,j} (+i ∂_{B,i} b)(+i ∂_{D,j} d) = −Σ_ij σ_{1,i} σ_{2,j} T_ij | + + For the 3-center operators (`coulomb_opop` and `coulomb_op`) the kernels + are generated for BraKet::xs_xx, with the fitting function as bra1 and the + AO pair as the ket; the Engine accepts BraKet::xx_xs as an alias that only + changes the order of the arguments of compute() and the layout of the + result (fitting function last), hence with xx_xs σ·p acts on the bra. + + The 1-body operators `opVop`, `op_q_gau_op` and `opemuop` (compiler: + `σpVσp`, `σpeμσp`) follow the convention of `coulomb_opop`: (σ·p a)† O (σ·p b) + is folded into q₀ + i σ·q with no further prefactor. `opemuop` has + 12 = 3 (dipole direction, slow) × 4 (folded Pauli component, fast) + components. + + The behavior of the components under the permutations Engine uses to + canonicalize shell sets is encoded in operator_traits: + pair_swap, braket_swap, pair_swap_phase(), braket_swap_phase() and + braket_swap_remap(). The generated library covers exactly the shell sets that + are canonical under these permutations (see the matching descriptor traits + in the compiler). + */ +// clang-format on + /// @param[in] oper an Operator object /// @return the particle rank of \c oper /// @throw std::logic_error if an invalid @c oper given @@ -268,6 +364,19 @@ inline constexpr bool is_nuclear_type(Operator oper) { oper == Operator::op_q_gau_op; } +/// which swaps of the two functions within the bra and within the ket of a +/// 2-body integral Engine may perform to canonicalize a shell set; the +/// generated kernels cover exactly the shell sets that are canonical with +/// respect to these swaps +enum class PairSwap { + /// bra and ket are sorted independently of each other + independent, + /// bra and ket are swapped together, or not at all (the ket is sorted) + coupled, + /// neither bra nor ket may be swapped + none +}; + namespace detail { struct default_operator_traits { typedef struct { @@ -277,6 +386,21 @@ struct default_operator_traits { // N.B.: Below field means we *should* template specialize operator_traits for // Operator::kinetic, but L2 doesn't use that anywhere. static constexpr auto intrinsic_deriv_order = 0u; + /// which swaps within bra and within ket of a 2-body integral the engine + /// may perform to canonicalize a shell set + static constexpr auto pair_swap = PairSwap::independent; + /// whether the engine may swap bra and ket (particles 1 and 2) of a 2-body + /// integral to canonicalize a shell set + static constexpr bool braket_swap = true; + /// @return the phase acquired by component @p c when the two functions of + /// the ket (and, if pair_swap is PairSwap::coupled, of the bra) are swapped + static constexpr int pair_swap_phase(unsigned int /* c */) { return 1; } + /// @return the phase acquired by component @p c when bra and ket are + /// swapped + static constexpr int braket_swap_phase(unsigned int /* c */) { return 1; } + /// @return the index of the component that component @p c becomes when bra + /// and ket are swapped + static constexpr unsigned int braket_swap_remap(unsigned int c) { return c; } struct _core_eval_type { template static std::shared_ptr instance(params...) { @@ -308,6 +432,7 @@ struct operator_traits typedef const libint2::FmEval_Reference core_eval_type; #endif }; + /// opVop: 4 quaternionic components (scalar, x, y, z) of (σ·p)V(σ·p). /// Inherits point-charge params from nuclear; uses Boys function core eval. template <> @@ -432,6 +557,13 @@ struct operator_traits (LIBINT_MULTIPOLE_MAX_ORDER + 1) * (LIBINT_MULTIPOLE_MAX_ORDER + 1); }; +template <> +struct operator_traits + : public operator_traits { + static constexpr auto nopers = 12; + static constexpr auto intrinsic_deriv_order = 2; +}; + template <> struct operator_traits : public detail::default_operator_traits { @@ -441,6 +573,72 @@ struct operator_traits typedef const libint2::FmEval_Reference core_eval_type; #endif }; + +template <> +struct operator_traits + : public operator_traits { + static constexpr auto nopers = 4; + static constexpr auto intrinsic_deriv_order = 2; + /// σ·p acts on the ket only, hence bra and ket cannot be swapped + static constexpr bool braket_swap = false; + /// swapping the ket functions swaps the two ∇ in ∇_c × ∇_d: the vector + /// components (1-3) change sign, the scalar (0) does not + static constexpr int pair_swap_phase(unsigned int c) { + return c > 0 ? -1 : 1; + } +}; +template <> +struct operator_traits + : public operator_traits { + /// 3 Cartesian components of the single σ·p applied to the 2nd ket function. + /// index = a, a ∈ {x=0, y=1, z=2}. One intrinsic derivative (σ·p = one ∇). + static constexpr auto nopers = 3; + static constexpr auto intrinsic_deriv_order = 1; + /// σ·p acts on one specific function, hence no swaps (the kernels are + /// generated for every ordering of the paired centers) + static constexpr auto pair_swap = PairSwap::none; + static constexpr bool braket_swap = false; +}; +template <> +struct operator_traits + : public operator_traits { + /// 16 components: tensor product of two independent spin-space quaternions + /// index = 4 * bra_spin + ket_spin, where spin in {S=0, X=1, Y=2, Z=3} + static constexpr auto nopers = 16; + static constexpr auto intrinsic_deriv_order = 4; + /// (ab|cd) = (cd|ab) = (ba|dc): the functions of bra and ket can only be + /// swapped together + static constexpr auto pair_swap = PairSwap::coupled; + /// swapping both pairs changes the sign of a component with exactly one + /// vector (spin ≠ S) factor + static constexpr int pair_swap_phase(unsigned int c) { + return ((c / 4 > 0) != (c % 4 > 0)) ? -1 : 1; + } + /// swapping bra and ket transposes the (bra_spin, ket_spin) index + static constexpr unsigned int braket_swap_remap(unsigned int c) { + return 4 * (c % 4) + c / 4; + } +}; +template <> +struct operator_traits + : public operator_traits { + /// 9 components: the SO(3) irreducible decomposition of the 3×3 derivative + /// dyadic T_ab (NOT a raw `3*a + b` Cartesian dyadic). Order matches + /// `σpCoulombσp_Descr::Component`: Scalar trace (0); antisymmetric + /// AntisymX,Y,Z (1-3); symmetric-traceless SymTLDiagA,SymTLDiagB (4,5) and + /// SymTLOffXY,SymTLOffXZ,SymTLOffYZ (6-8). + static constexpr auto nopers = 9; + static constexpr auto intrinsic_deriv_order = 2; + /// σ·p acts on one specific function of each pair, hence no swaps within + /// bra or ket + static constexpr auto pair_swap = PairSwap::none; + /// swapping bra and ket transposes T_ab: the antisymmetric components (1-3) + /// change sign, the scalar (0) and symmetric-traceless (4-8) do not + static constexpr int braket_swap_phase(unsigned int c) { + return (c >= 1 && c <= 3) ? -1 : 1; + } +}; + namespace detail { template struct cgtg_operator_traits : public detail::default_operator_traits { @@ -566,14 +764,40 @@ inline constexpr int rank(BraKet braket) { /// @return the default braket for @c oper /// @throw std::logic_error if invalid @c oper given inline constexpr BraKet default_braket(Operator oper) { - return (rank(oper) == 1) - ? BraKet::x_x - : ((rank(oper) == 2) - ? BraKet::xx_xx - : throw std::logic_error( - "default_braket(Operator): invalid operator given")); + // coulomb_op (3-center single-σ·p) has no 4-center kernel; its only + // valid geometry is the 3-center xs_xx braket. Default to that rather than + // the rank-based xx_xx, which has no generated kernel and would silently + // dispatch to the plain-Coulomb `default` task (wrong integrals, no error in + // release). + return (oper == Operator::coulomb_op) + ? BraKet::xs_xx + : ((rank(oper) == 1) + ? BraKet::x_x + : ((rank(oper) == 2) + ? BraKet::xx_xx + : throw std::logic_error( + "default_braket(Operator): invalid operator " + "given"))); } +namespace detail { +/// @return true if @c oper is one of the 2-body relativistic (restricted +/// kinetic balance, RKB) operators +inline constexpr bool is_rkb_2body(Operator oper) { + return oper == Operator::coulomb_opop || oper == Operator::coulomb_op || + oper == Operator::opop_coulomb_opop || oper == Operator::op_coulomb_op; +} + +/// whether the library was generated assuming that the fitting (unpaired) +/// center of plain 3-center ERIs will be transformed to solid harmonics +/// (LIBINT2_ERI3_PURE_SH) +#ifdef LIBINT_ERI3_PURE_SH +constexpr bool eri3_pure_sh = true; +#else +constexpr bool eri3_pure_sh = false; +#endif +} // namespace detail + constexpr size_t nopers_2body = static_cast(Operator::last_2body_oper) - static_cast(Operator::first_2body_oper) + 1; @@ -683,6 +907,7 @@ class Engine { lmax_(other.lmax_), hard_lmax_(other.hard_lmax_), hard_default_lmax_(other.hard_default_lmax_), + task_name_(other.task_name_), deriv_order_(other.deriv_order_), precision_(other.precision_), ln_precision_(other.ln_precision_), @@ -737,6 +962,7 @@ class Engine { lmax_ = other.lmax_; hard_lmax_ = other.hard_lmax_; hard_default_lmax_ = other.hard_default_lmax_; + task_name_ = other.task_name_; deriv_order_ = other.deriv_order_; precision_ = other.precision_; ln_precision_ = other.ln_precision_; @@ -825,6 +1051,8 @@ class Engine { if (braket_ != new_braket) { braket_ = new_braket; initialize(); + // the order of the core evaluator depends on the rank of the braket + core_eval_pack_ = make_core_eval_pack(oper_); } return *this; } @@ -870,6 +1098,8 @@ class Engine { if (L >= static_cast(lmax_)) { lmax_ = L; initialize(); + // the order of the core evaluator depends on lmax_ + core_eval_pack_ = make_core_eval_pack(oper_); } return *this; } @@ -946,16 +1176,16 @@ class Engine { const Shell& ket2, const ShellPair* spbra, const ShellPair* spket); // clang-format off - /** this specifies target precision for computing the integrals, i.e. - * the target absolute (i.e., not relative) error of the integrals. - * It is used to screen out primitive integrals. For some screening - * methods precision can be almost guaranteed (due to finite precision - * of the precomputed interpolation tables used to evaluate the core integrals - * it is not in general possible to guarantee precision rigorously). - * - * @param[in] prec the target precision - * @sa ScreeningMethod - */ + /** this specifies target precision for computing the integrals, i.e. + * the target absolute (i.e., not relative) error of the integrals. + * It is used to screen out primitive integrals. For some screening + * methods precision can be almost guaranteed (due to finite precision + * of the precomputed interpolation tables used to evaluate the core integrals + * it is not in general possible to guarantee precision rigorously). + * + * @param[in] prec the target precision + * @sa ScreeningMethod + */ // clang-format on Engine& set_precision(scalar_type prec) { if (prec <= 0.) { @@ -1066,6 +1296,19 @@ class Engine { size_t lmax_requested_; }; + /// Exception class to be used when the library does not contain the kernels + /// for the operator/braket/derivative order this Engine is set up for (e.g. + /// the corresponding integral class was not enabled when configuring the + /// library) + class missing_kernel : virtual public std::logic_error { + public: + missing_kernel() + : std::logic_error( + "Engine::missing_kernel -- this library was not configured to " + "compute integrals of this operator/braket/derivative order") {} + ~missing_kernel() noexcept {} + }; + /// Exception class to be used when using default-initialized Engine class using_default_initialized : virtual public std::logic_error { public: @@ -1084,13 +1327,17 @@ class Engine { size_t stack_size_; // amount allocated by libint2_init_xxx in // primdata_[0].stack int lmax_; - int hard_lmax_; // max L supported by library for this operator type (i.e. - // LIBINT2_MAX_AM_) + 1 - int hard_default_lmax_; // max L supported by library for default operator - // type (i.e. LIBINT2_MAX_AM_default) + - // 1 this is only set and used if - // LIBINT2_CENTER_DEPENDENT_MAX_AM_ - // == 1 + // max L supported by library for this operator type (i.e. + // LIBINT2_MAX_AM_) + 1; 0 until _initialize() finds + // the kernels + int hard_lmax_ = 0; + // max L supported by library for default operator type (i.e. + // LIBINT2_MAX_AM_default) + 1; this is only set and used if + // LIBINT2_CENTER_DEPENDENT_MAX_AM_ == 1 + int hard_default_lmax_ = 0; + // the label of the task whose kernels this Engine dispatches (a string + // literal, for error messages); set by _initialize() + const char* task_name_ = ""; int deriv_order_; scalar_type precision_; scalar_type ln_precision_; @@ -1129,7 +1376,8 @@ class Engine { // scratch2_ points to second such block. It could point into scratch_ or at // primdata_[0].stack typedef void (*buildfnptr_t)(const Libint_t*); - buildfnptr_t* buildfnptrs_; + /// null if the library has no kernels for this operator/braket/deriv order + buildfnptr_t* buildfnptrs_ = nullptr; /// reports the number of shell sets that each call to compute() produces. unsigned int compute_nshellsets() const { @@ -1191,6 +1439,12 @@ class Engine { unsigned int nparams() const; unsigned int nopers() const; unsigned int intrinsic_deriv_order() const; + /// operator_traits::pair_swap etc. (see \ref rkb_operators) + PairSwap pair_swap() const; + bool braket_swap() const; + int pair_swap_phase(unsigned int c) const; + int braket_swap_phase(unsigned int c) const; + unsigned int braket_swap_remap(unsigned int c) const; /// if Params == operator_traits::oper_params_type, will return /// \c any(params) /// else will set return \c any initialized with default value for diff --git a/include/libint2/engine.impl.h b/include/libint2/engine.impl.h index 95a54b16b..e1a90f254 100644 --- a/include/libint2/engine.impl.h +++ b/include/libint2/engine.impl.h @@ -70,41 +70,46 @@ typename std::remove_all_extents::type* to_ptr1(T (&a)[N]) { /// These MUST appear in the same order as in Operator. /// You must also update BOOST_PP_NBODY_OPERATOR_LAST_ONEBODY_INDEX when you add /// one-body ints -#define BOOST_PP_NBODY_OPERATOR_LIST \ - (overlap, /* overlap */ \ - (kinetic, /* kinetic */ \ - (elecpot, /* nuclear */ \ - (elecpot, /* erf_nuclear */ \ - (elecpot, /* erfc_nuclear */ \ - (elecpot, /* erfx_nuclear */ \ - (elecpot, /* q_gau */ \ - (1emultipole, /* emultipole1 */ \ - (2emultipole, /* emultipole2 */ \ - (3emultipole, /* emultipole3 */ \ - (sphemultipole, /* sphemultipole */ \ - (opVop, /* opVop */ \ - (opVop, /* op_q_gau_op */ \ - (eri, /* delta */ \ - (eri, /* coulomb */ \ - (eri, /* cgtg */ \ - (eri, /* cgtg_x_coulomb */ \ - (eri, /* delcgtg2 */ \ - (eri, /* r12 */ \ - (eri, /* erf_coulomb */ \ - (eri, /* erfc_coulomb */ \ - (eri, /* erfx_coulomb */ \ - (eri, /* stg */ \ - (eri, /* yukawa */ \ - (eri, /* delcgtga_delcgtgb */ \ - BOOST_PP_NIL))))))))))))))))))))))))) +#define BOOST_PP_NBODY_OPERATOR_LIST \ + (overlap, /* overlap */ \ + (kinetic, /* kinetic */ \ + (elecpot, /* nuclear */ \ + (elecpot, /* erf_nuclear */ \ + (elecpot, /* erfc_nuclear */ \ + (elecpot, /* erfx_nuclear */ \ + (elecpot, /* q_gau */ \ + (1emultipole, /* emultipole1 */ \ + (2emultipole, /* emultipole2 */ \ + (3emultipole, /* emultipole3 */ \ + (sphemultipole, /* sphemultipole */ \ + (opVop, /* opVop */ \ + (opVop, /* op_q_gau_op */ \ + (opemuop, /* opemuop */ \ + (eri, /* delta */ \ + (eri, /* coulomb */ \ + (eri, /* cgtg */ \ + (eri, /* cgtg_x_coulomb */ \ + (eri, /* delcgtg2 */ \ + (eri, /* r12 */ \ + (eri, /* erf_coulomb */ \ + (eri, /* erfc_coulomb */ \ + (eri, /* erfx_coulomb */ \ + (eri, /* stg */ \ + (eri, /* yukawa */ \ + (eri, /* delcgtga_delcgtgb */ \ + (coulomb_opop, /* coulomb_opop */ \ + (coulomb_op, /* coulomb_op */ \ + (opop_coulomb_opop, /* opop_coulomb_opop */ \ + (op_coulomb_op, /* op_coulomb_op */ \ + BOOST_PP_NIL)))))))))))))))))))))))))))))) #define BOOST_PP_NBODY_OPERATOR_INDEX_TUPLE \ BOOST_PP_MAKE_TUPLE(BOOST_PP_LIST_SIZE(BOOST_PP_NBODY_OPERATOR_LIST)) #define BOOST_PP_NBODY_OPERATOR_INDEX_LIST \ BOOST_PP_TUPLE_TO_LIST(BOOST_PP_NBODY_OPERATOR_INDEX_TUPLE) #define BOOST_PP_NBODY_OPERATOR_LAST_ONEBODY_INDEX \ - 12 // op_q_gau_op, the 13th member of BOOST_PP_NBODY_OPERATOR_LIST, is the - // last 1-body operator + 13 // opemuop, the 14th member of BOOST_PP_NBODY_OPERATOR_LIST, is the last + // 1-body operator // make list of braket indices for n-body ints #define BOOST_PP_NBODY_BRAKET_INDEX_TUPLE \ @@ -171,9 +176,22 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute( if (nargs == 2) return (this->*compute_ptr)(shells[0], Shell::unit(), shells[1], Shell::unit(), nullptr, nullptr); - if (nargs == 3) + if (nargs == 3) { + // The 3-arg user form depends on which 3-center BraKet was set: + // xs_xx — bra = (fitting, unit), ket = (AO, AO). + // User passes (fitting, AO, AO), kernel sees + // (fitting, unit, AO, AO). + // xx_xs — bra = (AO, AO), ket = (fitting, unit). + // User passes (AO, AO, fitting), kernel sees + // (AO, AO, fitting, unit). + // The non-xx_xs branch is the default (covers xs_xx and any future + // 3-center variants that follow the xs_xx convention). + if (braket_ == BraKet::xx_xs) + return (this->*compute_ptr)(shells[0], shells[1], shells[2], + Shell::unit(), nullptr, nullptr); return (this->*compute_ptr)(shells[0], Shell::unit(), shells[1], shells[2], nullptr, nullptr); + } if (nargs == 4) return (this->*compute_ptr)(shells[0], shells[1], shells[2], shells[3], nullptr, nullptr); @@ -189,6 +207,8 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute( /// \note resulting shell sets are stored in row-major order __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute1( const libint2::Shell& s1, const libint2::Shell& s2) { + if (buildfnptrs_ == nullptr) throw missing_kernel(); + // can only handle 1 contraction at a time assert((s1.ncontr() == 1 && s2.ncontr() == 1) && "generally-contracted shells not yet supported"); @@ -607,6 +627,22 @@ __libint2_engine_inline void Engine::_initialize() { if (static_cast(oper_) == BOOST_PP_TUPLE_ELEM(3, 0, product) && \ static_cast(rank(braket_)) == BOOST_PP_TUPLE_ELEM(3, 1, product) && \ deriv_order_ == BOOST_PP_TUPLE_ELEM(3, 2, product)) { \ + buildfnptrs_ = to_ptr1(LIBINT2_PREFIXED_NAME(BOOST_PP_CAT( \ + libint2_build_, BOOST_PP_NBODYENGINE_MCR3_TASK(product)))); \ + /* missing tasks resolve to the placeholder `default` task, i.e. there */ \ + /* are no kernels for this task; compute*() will throw missing_kernel */ \ + if (buildfnptrs_ == \ + to_ptr1(LIBINT2_PREFIXED_NAME(libint2_build_default))) { \ + if (stack_size_ > 0) \ + LIBINT2_PREFIXED_NAME(libint2_cleanup_default)(&primdata_[0]); \ + stack_size_ = 0; \ + buildfnptrs_ = nullptr; \ + targets_.clear(); \ + scratch_.clear(); \ + scratch2_ = nullptr; \ + return; \ + } \ + task_name_ = BOOST_PP_STRINGIZE(BOOST_PP_NBODYENGINE_MCR3_TASK(product)); \ hard_lmax_ = BOOST_PP_CAT(LIBINT2_MAX_AM_, \ BOOST_PP_NBODYENGINE_MCR3_TASK(product)) + \ 1; \ @@ -623,6 +659,7 @@ __libint2_engine_inline void Engine::_initialize() { BOOST_PP_NBODYENGINE_MCR3_task(product))), \ std::max(hard_lmax_, hard_default_lmax_), hard_lmax_); \ if (lmax_ >= lmax) { \ + buildfnptrs_ = nullptr; /* leave no table for kernels we cannot use */ \ throw Engine::lmax_exceeded(BOOST_PP_STRINGIZE(BOOST_PP_NBODYENGINE_MCR3_TASK(product)), lmax, lmax_); \ } \ if (stack_size_ > 0) \ @@ -637,8 +674,6 @@ __libint2_engine_inline void Engine::_initialize() { LIBINT2_PREFIXED_NAME(libint2_init_flopcounter)( \ &primdata_[0], primdata_.size()), \ BOOST_PP_EMPTY()); \ - buildfnptrs_ = to_ptr1(LIBINT2_PREFIXED_NAME(BOOST_PP_CAT( \ - libint2_build_, BOOST_PP_NBODYENGINE_MCR3_TASK(product)))); \ reset_scratch(); \ return; \ } @@ -655,30 +690,29 @@ __libint2_engine_inline void Engine::_initialize() { __libint2_engine_inline void Engine::initialize(size_t max_nprim) { assert(libint2::initialized() && "libint is not initialized"); + // if the arguments are rejected leave no table for the previous + // operator/braket (set() has already changed them), so that compute() + // throws missing_kernel instead of dispatching it + if (detail::is_rkb_2body(oper_) && deriv_order_ > 0) { + buildfnptrs_ = nullptr; + throw std::invalid_argument( + "Engine::initialize -- geometric derivatives of RKB integrals are not " + "yet supported"); + } + // energies canonicalize xx_xs to xs_xx, but DerivMapGenerator has no + // derivative index map for xx_xs; fail here rather than inside compute2 + if (braket_ == BraKet::xx_xs && deriv_order_ > 0) { + buildfnptrs_ = nullptr; + throw std::invalid_argument( + "Engine::initialize -- geometric derivatives are not supported for " + "BraKet::xx_xs, use BraKet::xs_xx"); + } assert(deriv_order_ <= LIBINT2_MAX_DERIV_ORDER && "exceeded the max derivative order of the library"); - // validate braket -#ifndef LIBINT_INCLUDE_ONEBODY - assert(braket_ != BraKet::x_x && - "this braket type not supported by the library; give --enable-1body " - "to configure"); -#endif -#ifndef LIBINT_INCLUDE_ERI - assert(braket_ != BraKet::xx_xx && - "this braket type not supported by the library; give --enable-eri to " - "configure"); -#endif -#ifndef LIBINT_INCLUDE_ERI3 - assert((braket_ != BraKet::xs_xx && braket_ != BraKet::xx_xs) && - "this braket type not supported by the library; give --enable-eri3 to " - "configure"); -#endif -#ifndef LIBINT_INCLUDE_ERI2 - assert(braket_ != BraKet::xs_xs && - "this braket type not supported by the library; give --enable-eri2 to " - "configure"); -#endif + // N.B. whether the library supports this operator/braket/deriv order + // is determined by _initialize(); if it does not, compute*() throw + // missing_kernel // make sure it's no default initialized if (lmax_ < 0) throw using_default_initialized(); @@ -699,6 +733,7 @@ __libint2_engine_inline void Engine::initialize(size_t max_nprim) { // target indices. const auto permutable_targets = deriv_order_ > 0 && + (braket_ == BraKet::xx_xx || braket_ == BraKet::xs_xx || braket_ == BraKet::xx_xs); if (permutable_targets) @@ -821,6 +856,33 @@ __libint2_engine_inline unsigned int Engine::intrinsic_deriv_order() const { abort(); } +// clang-format off +// accessors to the permutational-symmetry traits of oper_ (see +// operator_traits): RET NAME ARGS const { return +// operator_traits::TRAIT_EXPR; } +#define BOOST_PP_NBODYENGINE_MCR10(r, trait_expr, i, elem) \ + case i: \ + return operator_traits(i)>::trait_expr; +#define LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR(RET, NAME, ARGS, TRAIT_EXPR) \ + __libint2_engine_inline RET Engine::NAME ARGS const { \ + switch (static_cast(oper_)) { \ + BOOST_PP_LIST_FOR_EACH_I(BOOST_PP_NBODYENGINE_MCR10, TRAIT_EXPR, \ + BOOST_PP_NBODY_OPERATOR_LIST) \ + default: \ + break; \ + } \ + assert(false && "missing case in switch"); /* unreachable */ \ + abort(); \ + } +LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR(PairSwap, pair_swap, (), pair_swap) +LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR(bool, braket_swap, (), braket_swap) +LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR(int, pair_swap_phase, (unsigned int c), pair_swap_phase(c)) +LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR(int, braket_swap_phase, (unsigned int c), braket_swap_phase(c)) +LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR(unsigned int, braket_swap_remap, (unsigned int c), braket_swap_remap(c)) +#undef LIBINT2_ENGINE_OPERATOR_TRAIT_ACCESSOR +#undef BOOST_PP_NBODYENGINE_MCR10 +// clang-format on + template <> __libint2_engine_inline any Engine::enforce_params_type( Operator oper, const any& params, bool throw_if_wrong_type) { @@ -1081,7 +1143,7 @@ __libint2_engine_inline void Engine::compute_primdata(Libint_t& primdata, // } if (oper_ == Operator::emultipole1 || oper_ == Operator::emultipole2 || - oper_ == Operator::emultipole3) { + oper_ == Operator::emultipole3 || oper_ == Operator::opemuop) { const auto& O = any_cast< const operator_traits::oper_params_type&>( params_); // same as emultipoleX @@ -1129,7 +1191,8 @@ __libint2_engine_inline void Engine::compute_primdata(Libint_t& primdata, primdata._0_Overlap_0_z[0] = ovlp_ss_z; if (oper_ == Operator::kinetic || (deriv_order_ > 0) || - oper_ == Operator::opVop || oper_ == Operator::op_q_gau_op) { + oper_ == Operator::opVop || oper_ == Operator::op_q_gau_op || + oper_ == Operator::opemuop) { #if LIBINT2_DEFINED(eri, two_alpha0_bra) primdata.two_alpha0_bra[0] = 2.0 * alpha1; #endif @@ -1273,6 +1336,7 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( (tspbra != nullptr && tspket != nullptr)) && "Engine::compute2 -- expects zero or two ShellPair objects"); assert(screening_method_ != ScreeningMethod::Invalid); + if (buildfnptrs_ == nullptr) throw missing_kernel(); // // i.e. bra and ket refer to chemists bra and ket @@ -1293,12 +1357,43 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( #if LIBINT2_SHELLQUARTET_SET == \ LIBINT2_SHELLQUARTET_SET_STANDARD // standard angular momentum ordering - const auto swap_tbra = (tbra1.contr[0].l < tbra2.contr[0].l); - const auto swap_tket = (tket1.contr[0].l < tket2.contr[0].l); - const auto swap_braket = - ((braket_ == BraKet::xx_xx) && (tbra1.contr[0].l + tbra2.contr[0].l > - tket1.contr[0].l + tket2.contr[0].l)) || - braket_ == BraKet::xx_xs; + // canonicalize the shell set according to the permutational symmetry of + // the operator (see operator_traits: pair_swap, braket_swap); the kernels + // are generated for the canonical shell sets only + bool swap_braket; + bool swap_tbra, swap_tket; + { + const auto pair_swap = this->pair_swap(); + const auto bra_total = tbra1.contr[0].l + tbra2.contr[0].l; + const auto ket_total = tket1.contr[0].l + tket2.contr[0].l; + const auto bra_max = std::max(tbra1.contr[0].l, tbra2.contr[0].l); + const auto ket_max = std::max(tket1.contr[0].l, tket2.contr[0].l); + // xx_xs is the alias of xs_xx, hence always swapped; else swap if the bra + // is "heavier" than the ket; with coupled pair swaps the kernels are + // generated for max(la,lb) <= lc when the totals tie + swap_braket = braket_ == BraKet::xx_xs || + (braket_ == BraKet::xx_xx && this->braket_swap() && + (bra_total > ket_total || + (pair_swap == PairSwap::coupled && bra_total == ket_total && + bra_max > ket_max))); + switch (pair_swap) { + case PairSwap::independent: + swap_tbra = (tbra1.contr[0].l < tbra2.contr[0].l); + swap_tket = (tket1.contr[0].l < tket2.contr[0].l); + break; + case PairSwap::coupled: { + // sort the pair that ends up in the ket of the kernel, and swap the + // other pair along with it + const bool swap_pairs = swap_braket + ? (tbra1.contr[0].l < tbra2.contr[0].l) + : (tket1.contr[0].l < tket2.contr[0].l); + swap_tbra = swap_tket = swap_pairs; + } break; + case PairSwap::none: + swap_tbra = swap_tket = false; + break; + } + } #else // orca angular momentum ordering const auto swap_tbra = (tbra1.contr[0].l > tbra2.contr[0].l); const auto swap_tket = (tket1.contr[0].l > tket2.contr[0].l); @@ -1502,17 +1597,25 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( const scalar_type rho = gammap * gammaq * oogammapq; const scalar_type T = PQ2 * rho; auto* gm_ptr = &(primdata.LIBINT_T_SS_EREP_SS(0)[0]); - const auto mmax = l + deriv_order_; + const auto mmax = l + deriv_order_ + intrinsic_deriv_order(); if (!skip_core_ints) { +// Simple core-eval dispatch: just `core_eval_ptr->eval(gm_ptr, T, mmax)`. +// Applies to Coulomb-family operators whose core integral is the bare Fm. +#define LIBINT2_SIMPLE_CORE_EVAL_CASE(OP) \ + case Operator::OP: { \ + const auto& core_eval_ptr = \ + any_cast&>( \ + core_eval_pack_) \ + .first(); \ + core_eval_ptr->eval(gm_ptr, T, mmax); \ + } break switch (oper_) { - case Operator::coulomb: { - const auto& core_eval_ptr = - any_cast&>(core_eval_pack_) - .first(); - core_eval_ptr->eval(gm_ptr, T, mmax); - } break; + LIBINT2_SIMPLE_CORE_EVAL_CASE(coulomb); + LIBINT2_SIMPLE_CORE_EVAL_CASE(coulomb_opop); + LIBINT2_SIMPLE_CORE_EVAL_CASE(coulomb_op); + LIBINT2_SIMPLE_CORE_EVAL_CASE(opop_coulomb_opop); + LIBINT2_SIMPLE_CORE_EVAL_CASE(op_coulomb_op); case Operator::cgtg_x_coulomb: { const auto& core_eval_ptr = any_cast 0 || lmax_bra > 0) { + if (deriv_order_ + intrinsic_deriv_order() > 0 || lmax_bra > 0) { #if LIBINT2_DEFINED(eri, WP_x) primdata.WP_x[0] = Wx - P[0]; #endif @@ -1754,7 +1858,7 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( primdata.WP_z[0] = Wz - P[2]; #endif } - if (deriv_order_ > 0 || lmax_ket > 0) { + if (deriv_order_ + intrinsic_deriv_order() > 0 || lmax_ket > 0) { #if LIBINT2_DEFINED(eri, WQ_x) primdata.WQ_x[0] = Wx - Q[0]; #endif @@ -1838,7 +1942,7 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( #endif // prefactors for derivative ERI relations - if (deriv_order_ > 0) { + if (deriv_order_ + intrinsic_deriv_order() > 0) { #if LIBINT2_DEFINED(eri, alpha1_rho_over_zeta2) primdata.alpha1_rho_over_zeta2[0] = alpha0 * (oogammap * gammaq_o_gammapgammaq); @@ -1921,7 +2025,8 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( } // compute directly (ss|ss) - const auto compute_directly = lmax == 0 && deriv_order_ == 0; + const auto compute_directly = + lmax == 0 && deriv_order_ == 0 && intrinsic_deriv_order() == 0; if (compute_directly) { #ifdef LIBINT2_ENGINE_TIMERS @@ -1948,17 +2053,24 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( timers.start(1); #endif + // the per-center angular momentum limits are checked in all builds: lmax_ + // (checked by initialize()) is the limit of the most permissive center, so + // for a task whose limit differs between the centers (e.g. 3eri, or + // 3coulomb_opop with LIBINT2_RKB_ERI3_MAX_AM != LIBINT2_MAX_AM) a shell can + // be within lmax_ and still beyond the limit of its center, and the index + // into the kernel table computed below would be out of range + auto check_lmax = [this](const Shell& sh, int lmax_plus_1) { + if (sh.contr[0].l >= lmax_plus_1) + throw lmax_exceeded(task_name_, lmax_plus_1 - 1, sh.contr[0].l); + }; + size_t buildfnidx; switch (braket_) { case BraKet::xx_xx: - assert(bra1.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); - assert(bra2.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); - assert(ket1.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); - assert(ket2.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); + check_lmax(bra1, hard_lmax_); + check_lmax(bra2, hard_lmax_); + check_lmax(ket1, hard_lmax_); + check_lmax(ket2, hard_lmax_); buildfnidx = ((bra1.contr[0].l * hard_lmax_ + bra2.contr[0].l) * hard_lmax_ + ket1.contr[0].l) * @@ -1967,63 +2079,61 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( break; case BraKet::xx_xs: - assert(false && "this braket is not supported"); - abort(); - break; + // xx_xs is always canonicalized to xs_xx by swap_braket=true in the + // shell-aliasing logic above; the swapped shells (bra1, ket1, ket2) + // are already in xs_xx layout by this point. Fall through to the + // xs_xx case to compute buildfnidx from those swapped shells. + [[fallthrough]]; case BraKet::xs_xx: { - /// lmax might be center dependent - int ket_lmax = hard_lmax_; - switch (deriv_order_) { - // N.B. notice extra PP_CAT to avoid using - // LIBINT2_CENTER_DEPENDENT_MAX_AM_3eri as a subtoken which gets - // expanded too early ... i.e. PP_CAT is "not associative" -#define BOOST_PP_NBODYENGINE_MCR8(r, data, i, elem) \ - case i: \ - BOOST_PP_IF(BOOST_PP_IS_1(BOOST_PP_CAT( \ - LIBINT2_CENTER_DEPENDENT_MAX_AM_, \ - BOOST_PP_CAT(3eri, BOOST_PP_IIF(BOOST_PP_GREATER(i, 0), i, \ - BOOST_PP_EMPTY())))), \ - ket_lmax = hard_default_lmax_, BOOST_PP_EMPTY()); \ - break; - - BOOST_PP_LIST_FOR_EACH_I(BOOST_PP_NBODYENGINE_MCR8, _, - BOOST_PP_NBODY_DERIV_ORDER_LIST) + // The ket (paired/AO) centers may have a different max AM than the + // bra (fitting) center for *center-dependent* 3-center tasks (e.g. + // 3eri, or 3coulomb_opop with LIBINT2_RKB_ERI3_MAX_AM != + // LIBINT2_MAX_AM), giving a non-cubic build table. _initialize() sets + // hard_default_lmax_ to the paired max (+1) for center-dependent + // tasks, or to INT_MAX for tasks that are not, whose build table is + // [hard_lmax_]^3. + const int ket_lmax = + hard_default_lmax_ != std::numeric_limits::max() + ? hard_default_lmax_ + : hard_lmax_; + check_lmax(bra1, hard_lmax_); + check_lmax(ket1, ket_lmax); + check_lmax(ket2, ket_lmax); - default: - assert(false && "missing case in switch"); - abort(); - } - assert(bra1.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); - assert(ket1.contr[0].l <= ket_lmax && - "the angular momentum limit is exceeded"); - assert(ket2.contr[0].l <= ket_lmax && - "the angular momentum limit is exceeded"); buildfnidx = (bra1.contr[0].l * ket_lmax + ket1.contr[0].l) * ket_lmax + ket2.contr[0].l; -#ifdef LIBINT_ERI3_PURE_SH - if (bra1.contr[0].l > 1) - assert(bra1.contr[0].pure && - "library assumes a solid harmonics shell in bra of a 3-center " - "2-body int, but a cartesian shell given"); -#endif + + // the kernels may have been generated assuming that the fitting + // (bra) shell will be transformed to solid harmonics + // (LIBINT2_ERI3_PURE_SH); this never applies to the RKB 3-center + // kernels, nor to derivative kernels (the generator marks the fitting + // shell pure only for derivative order 0, see build_libint.cc). Such + // kernels produce garbage for a cartesian shell, hence check in all + // builds + if (detail::eri3_pure_sh && deriv_order_ == 0 && + !detail::is_rkb_2body(oper_) && bra1.contr[0].l > 1 && + !bra1.contr[0].pure) + throw std::invalid_argument( + "Engine::compute2 -- this library assumes that the fitting " + "(bra) shell of 3-center 2-body integrals is a solid harmonics " + "shell, but a cartesian shell was given"); } break; case BraKet::xs_xs: - assert(bra1.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); - assert(ket1.contr[0].l <= hard_lmax_ && - "the angular momentum limit is exceeded"); + check_lmax(bra1, hard_lmax_); + check_lmax(ket1, hard_lmax_); buildfnidx = bra1.contr[0].l * hard_lmax_ + ket1.contr[0].l; #ifdef LIBINT_ERI2_PURE_SH - if (bra1.contr[0].l > 1) + // as for 3-center: derivative kernels are generated for cartesian + // shells, only the energy kernels assume solid harmonics + if (deriv_order_ == 0 && bra1.contr[0].l > 1) assert(bra1.contr[0].pure && "library assumes solid harmonics shells in a 2-center " "2-body int, but a cartesian shell given in bra"); - if (ket1.contr[0].l > 1) + if (deriv_order_ == 0 && ket1.contr[0].l > 1) assert(ket1.contr[0].pure && "library assumes solid harmonics shells in a 2-center " - "2-body int, but a cartesian shell given in bra"); + "2-body int, but a cartesian shell given in ket"); #endif break; @@ -2127,6 +2237,14 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( const auto* src_row_ptr = source; auto tgt_ptr = target; + // the phase of this component due to the swaps (see + // operator_traits); N.B. shell sets are ordered as + // deriv index * nopers + component + const auto component = s % this->nopers(); + const value_type component_phase = + (swap_ket ? this->pair_swap_phase(component) : 1) * + (swap_braket ? this->braket_swap_phase(component) : 1); + // if permuting derivatives ints must update their derivative index // Additional BraKet types would require adding support to // DerivMapGenerator::generate_deriv_index_map @@ -2184,9 +2302,9 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( Eigen::Stride( nr2_tgt * ncol_tgt, ncol_tgt)); if (swap_tbra) - tgt_blk_mat = src_blk_mat.transpose(); + tgt_blk_mat = component_phase * src_blk_mat.transpose(); else - tgt_blk_mat = src_blk_mat; + tgt_blk_mat = component_phase * src_blk_mat; } else { // source row {r1,r2} is mapped to target row {r1,r2} if // !swap_tbra, else to {r2,r1} @@ -2194,9 +2312,9 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( !swap_tbra ? r1 * nr2 + r2 : r2 * nr1 + r1; Map tgt_blk_mat(tgt_ptr + tgt_row_idx * ncol, nc1_tgt, nc2_tgt); if (swap_tket) - tgt_blk_mat = src_blk_mat.transpose(); + tgt_blk_mat = component_phase * src_blk_mat.transpose(); else - tgt_blk_mat = src_blk_mat; + tgt_blk_mat = component_phase * src_blk_mat; } } // end of loop } // over rows of source @@ -2220,7 +2338,29 @@ __libint2_engine_inline const Engine::target_ptr_vec& Engine::compute2( // to primdata_[0].targets targets_[s_target] = source; } - } // loop over shellsets + } // loop over shellsets + + // swapping bra and ket may permute the components (see + // operator_traits::braket_swap_remap) + if (permute && swap_braket) { + const auto nopers = this->nopers(); + bool trivial = true; + for (auto c = 0u; c != nopers && trivial; ++c) + trivial = (this->braket_swap_remap(c) == c); + if (!trivial) { + // N.B. targets_ uses ext_stack_allocator (its elements live in + // primdata_[0].targets); do not copy the vector, a copy inherits + // the allocator and may be placed on the same arena + assert(ntargets <= static_cast(max_ntargets)); + std::array tmp; + std::copy(targets_.begin(), targets_.begin() + ntargets, tmp.begin()); + for (auto s = 0; s != ntargets; ++s) { + const auto d = s / nopers; + const auto c = s % nopers; + targets_[d * nopers + this->braket_swap_remap(c)] = tmp[s]; + } + } + } } // if need_scratch => needed to transpose and/or tform else { // did not use scratch? may still need to update targets_ if (set_targets_) { diff --git a/include/libint2/solidharmonics.h b/include/libint2/solidharmonics.h index 7caa7ecea..7a3c341d1 100644 --- a/include/libint2/solidharmonics.h +++ b/include/libint2/solidharmonics.h @@ -39,17 +39,22 @@ #include #include -namespace { -template -signed char parity(Int i) { - return i % 2 ? -1 : 1; -} -} // namespace - namespace libint2 { namespace solidharmonics { +namespace detail { +/// @return \f$ (-1)^i \f$ for any integer @p i , negative included +/// @note always call this qualified (detail::parity): under MSVC-style delayed +/// template parsing (clang-cl's default) an unqualified call from a template +/// body is bound at instantiation and can pick up an unrelated `parity` that +/// an including TU has brought into namespace libint2 (test_eri/eri.h did) +template +constexpr signed char parity(Int i) { + return (i % 2 != 0) ? -1 : 1; +} +} // namespace detail + // to avoid overhead of Eigen::SparseMatrix will roll our own /// Transformation coefficients from unnormalized Cartesian Gaussians (rows) to @@ -129,7 +134,7 @@ class SolidHarmonicsCoefficients { auto comp = (m >= 0) ? 1 : -1; /* if (comp != ((abs_m-lx)%2 ? -1 : 1))*/ auto i = abs_m - lx; - if (comp != parity(abs(i))) return 0.0; + if (comp != detail::parity(std::abs(i))) return 0.0; Real pfac; pfac = sqrt(((fac_real(2 * lx) * fac_real(2 * ly) * @@ -144,16 +149,16 @@ class SolidHarmonicsCoefficients { * sqrt(fac_real(l-abs_m)/(fac_real(l)*fac_real(l)*fac[l+abs_m]));*/ pfac /= (1L << l); if (m < 0) - pfac *= parity((i - 1) / 2); + pfac *= detail::parity((i - 1) / 2); else - pfac *= parity(i / 2); + pfac *= detail::parity(i / 2); auto i_min = j; auto i_max = (l - abs_m) / 2; Real sum = 0; for (auto i = i_min; i <= i_max; i++) { Real pfac1 = bc_real(l, i) * bc_real(i, j); - pfac1 *= (Real(parity(i) * fac_real(2 * (l - i))) / + pfac1 *= (Real(detail::parity(i) * fac_real(2 * (l - i))) / fac_real(l - abs_m - 2 * i)); Real sum1 = 0.0; const int k_min = std::max((lx - abs_m) / 2, 0); @@ -161,7 +166,7 @@ class SolidHarmonicsCoefficients { for (int k = k_min; k <= k_max; k++) { if (lx - 2 * k <= abs_m) sum1 += bc_real(j, k) * bc_real(abs_m, lx - 2 * k) * - parity(k); + detail::parity(k); } sum += pfac1 * sum1; } diff --git a/include/libint2/util/array_adaptor.h b/include/libint2/util/array_adaptor.h index 8c4fea497..0f1f9ba84 100644 --- a/include/libint2/util/array_adaptor.h +++ b/include/libint2/util/array_adaptor.h @@ -69,13 +69,21 @@ class ext_stack_allocator { explicit ext_stack_allocator(U (&array)[N]) noexcept : stack_(const_cast(&array[0])), free_(stack_) {} + /// a container copy-constructed from one using this allocator gets a + /// heap-only allocator: the arena belongs to the original container, and a + /// copy of the allocator state would hand out the same (or overlapping) + /// storage + ext_stack_allocator select_on_container_copy_construction() const noexcept { + return ext_stack_allocator(); + } + template struct rebind { using other = ext_stack_allocator<_Up, N>; }; T* allocate(std::size_t n) { - assert(stack_ != nullptr && "array_view_allocator not initialized"); + if (stack_ == nullptr) return new T[n]; // heap-only allocator if (stack_ + N - free_ >= static_cast(n)) { // have free space on stack const auto result = free_; @@ -86,7 +94,7 @@ class ext_stack_allocator { } } void deallocate(T* p, std::size_t n) noexcept { - if (pointer_on_stack(p)) { + if (stack_ != nullptr && pointer_on_stack(p)) { assert(p + n == free_ && "stack deallocation out of order"); free_ -= n; } else { diff --git a/src/bin/libint/CMakeLists.txt b/src/bin/libint/CMakeLists.txt index ebb7983b7..4e452727f 100644 --- a/src/bin/libint/CMakeLists.txt +++ b/src/bin/libint/CMakeLists.txt @@ -52,7 +52,7 @@ target_compile_options( target_compile_features( libint-libcompiler PUBLIC - "cxx_std_11" + "cxx_std_17" # if constexpr ) set_target_properties( diff --git a/src/bin/libint/build_libint.cc b/src/bin/libint/build_libint.cc index 1f7f87878..2726ea585 100644 --- a/src/bin/libint/build_libint.cc +++ b/src/bin/libint/build_libint.cc @@ -31,7 +31,9 @@ */ #include +#include #include +#include #include #include #include @@ -72,19 +74,52 @@ enum ShellSetType { }; template struct ShellQuartetSetPredicate { - // return true if this set of angular momenta is included - static bool value(int la, int lb, int lc, int ld); + /// @return true if code is to be generated for this set of angular momenta, + /// i.e. if it is canonical with respect to the permutations the engine + /// performs for this operator (see the descriptor traits p1p2_swappable and + /// pair_swap, and operator_traits in the engine) + /// @param p1p2_swappable whether the operator allows swapping particles 1 + /// and 2 (bra and ket in chemists' notation) + /// @param pair_swap which swaps of the two functions within bra and within + /// ket the operator allows + static bool value(int la, int lb, int lc, int ld, bool p1p2_swappable = true, + PairSwap pair_swap = PairSwap::independent); }; template <> struct ShellQuartetSetPredicate { - static bool value(int la, int lb, int lc, int ld) { - return la >= lb && lc >= ld && la + lb <= lc + ld; + static bool value(int la, int lb, int lc, int ld, bool p1p2_swappable = true, + PairSwap pair_swap = PairSwap::independent) { + switch (pair_swap) { + case PairSwap::independent: + return la >= lb && lc >= ld && (!p1p2_swappable || la + lb <= lc + ld); + case PairSwap::coupled: + // the ket is sorted and the bra follows; when the totals tie the + // engine swaps bra and ket such that max(la,lb) <= lc + assert(p1p2_swappable); + return lc >= ld && (la + lb < lc + ld || + (la + lb == lc + ld && std::max(la, lb) <= lc)); + case PairSwap::none: + return !p1p2_swappable || la + lb <= lc + ld; + } + abort(); } }; template <> struct ShellQuartetSetPredicate { - static bool value(int la, int lb, int lc, int ld) { - return la <= lb && lc <= ld && (la < lc || (la == lc && lb <= ld)); + static bool value(int la, int lb, int lc, int ld, bool p1p2_swappable = true, + PairSwap pair_swap = PairSwap::independent) { + switch (pair_swap) { + case PairSwap::independent: + return la <= lb && lc <= ld && + (!p1p2_swappable || (la < lc || (la == lc && lb <= ld))); + case PairSwap::coupled: + assert(p1p2_swappable); + return lc <= ld && (la + lb > lc + ld || + (la + lb == lc + ld && std::min(la, lb) >= lc)); + case PairSwap::none: + return !p1p2_swappable || la + lb >= lc + ld; + } + abort(); } }; template @@ -192,20 +227,28 @@ static void config_to_api(const std::shared_ptr& cparams, #ifdef LIBINT_INCLUDE_ERI #define USE_GENERIC_ERI_BUILD 1 -#if !USE_GENERIC_ERI_BUILD +#endif +#if defined(LIBINT_INCLUDE_ERI) || defined(LIBINT_INCLUDE_RKB_ERI) +#if defined(USE_GENERIC_ERI_BUILD) && !USE_GENERIC_ERI_BUILD +template static void build_TwoPRep_2b_2k( - std::ostream& os, const std::shared_ptr& cparams, + std::ostream& os, std::string label, + const std::shared_ptr& cparams, std::shared_ptr& iface); #else +template static void build_TwoPRep_2b_2k( - std::ostream& os, const std::shared_ptr& cparams, - std::shared_ptr& iface, unsigned int deriv_level); + std::ostream& os, std::string label, + const std::shared_ptr& cparams, + std::shared_ptr& iface, unsigned int deriv_level = 0); #endif #endif -#ifdef LIBINT_INCLUDE_ERI3 +#if defined(LIBINT_INCLUDE_ERI3) || defined(LIBINT_INCLUDE_RKB_ERI3) +template static void build_TwoPRep_1b_2k( - std::ostream& os, const std::shared_ptr& cparams, + std::ostream& os, std::string label, + const std::shared_ptr& cparams, std::shared_ptr& iface, unsigned int deriv_level); #endif @@ -252,6 +295,8 @@ struct AuxQuantaType { typedef EmptySet type; }; +} // namespace + template OperDescrType make_descr(int, int = 0, int = 0) { return OperDescrType(); @@ -277,7 +322,31 @@ template <> σpVσp_Descr make_descr<σpVσp_Descr>(int p, int, int) { return σpVσp_Descr(p); } -} // namespace + +template <> +σpeμσp_Descr make_descr<σpeμσp_Descr>(int p, int, int) { + return σpeμσp_Descr(p); +} + +template <> +Coulombσpσp_Descr make_descr(int p, int, int) { + return Coulombσpσp_Descr(p); +} + +template <> +σpσpCoulombσpσp_Descr make_descr<σpσpCoulombσpσp_Descr>(int p, int, int) { + return σpσpCoulombσpσp_Descr(p); +} + +template <> +σpCoulombσp_Descr make_descr<σpCoulombσp_Descr>(int p, int, int) { + return σpCoulombσp_Descr(p); +} + +template <> +Coulombσp_Descr make_descr(int p, int, int) { + return Coulombσp_Descr(p); +} template void build_onebody_1b_1k(std::ostream& os, std::string label, @@ -406,6 +475,14 @@ void build_onebody_1b_1k(std::ostream& os, std::string label, descrs.emplace_back(make_descr(p)); } } + if (std::is_same<_OperType, σpeμσpOper>::value) { + // reset descriptors array + descrs.resize(0); + // iterate over 12 = 3 dipole directions × 4 Pauli components + for (int p = 0; p != 12; ++p) { + descrs.emplace_back(make_descr(p)); + } + } // derivative index is the outermost (slowest running) // operator component is second slowest @@ -483,8 +560,8 @@ void build_onebody_1b_1k(std::ostream& os, std::string label, eval_label = oss.str(); } - std::cout << "working on " << eval_label << " ... "; - std::cout.flush(); + g_progress.current_task = eval_label; + g_progress.print(); std::string prefix(cparams->source_directory()); std::deque decl_filenames; @@ -524,8 +601,6 @@ void build_onebody_1b_1k(std::ostream& os, std::string label, dg->reset(); memman->reset(); - std::cout << "done" << std::endl; - } // end of b loop } // end of a loop } @@ -547,11 +622,11 @@ void try_main(int argc, char* argv[]) { // overlap, kinetic, elecpot cannot be omitted #define BOOST_PP_ONEBODY_TASK_TUPLE \ (overlap, kinetic, elecpot, 1emultipole, 2emultipole, 3emultipole, \ - sphemultipole, opVop) + sphemultipole, opVop, opemuop) #define BOOST_PP_ONEBODY_TASK_OPER_TUPLE \ (OverlapOper, KineticOper, ElecPotOper, CartesianMultipoleOper<3u>, \ CartesianMultipoleOper<3u>, CartesianMultipoleOper<3u>, \ - SphericalMultipoleOper, σpVσpOper) + SphericalMultipoleOper, σpVσpOper, σpeμσpOper) #define BOOST_PP_ONEBODY_TASK_LIST \ BOOST_PP_TUPLE_TO_LIST(BOOST_PP_ONEBODY_TASK_TUPLE) #define BOOST_PP_ONEBODY_TASK_OPER_LIST \ @@ -570,6 +645,60 @@ void try_main(int argc, char* argv[]) { taskmgr.add(task_label("eri", d)); } #endif + +#if (defined(LIBINT_INCLUDE_RKB_ERI) && LIBINT_INCLUDE_RKB_ERI > 0) || \ + (defined(LIBINT_INCLUDE_RKB_ERI3) && LIBINT_INCLUDE_RKB_ERI3 > 0) +#error "geometric derivatives of RKB integrals are not yet implemented" +#endif +#ifdef LIBINT_INCLUDE_RKB_ERI +#define BOOST_PP_RKB_ERI_TASK_TUPLE \ + (coulomb_opop, opop_coulomb_opop, op_coulomb_op) +#define BOOST_PP_RKB_ERI_TASK_OPER_TUPLE \ + (CoulombσpσpOper, σpσpCoulombσpσpOper, σpCoulombσpOper) +#define BOOST_PP_RKB_ERI_TASK_LIST \ + BOOST_PP_TUPLE_TO_LIST(BOOST_PP_RKB_ERI_TASK_TUPLE) +#define BOOST_PP_RKB_ERI_TASK_OPER_LIST \ + BOOST_PP_TUPLE_TO_LIST(BOOST_PP_RKB_ERI_TASK_OPER_TUPLE) + + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI; ++d) { +#define BOOST_PP_RKB_ERI_MCR1(r, data, elem) \ + taskmgr.add(task_label(BOOST_PP_STRINGIZE(elem), d)); + + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI_MCR1, _, BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR1 + } +#endif + +#ifdef LIBINT_INCLUDE_RKB_ERI3 + // 3-center RKB ERIs. σ·p acts on the AO pair (ket); the fitting center is a + // spectator. Task labels are namespaced with the "3" ncenter prefix so they + // match the BOOST_PP_NBODYENGINE_MCR3_task expansion in engine.impl.h: + // ncenter("3") + oper("coulomb_opop") + deriv("") = "3coulomb_opop". + // The macro tuples mirror BOOST_PP_RKB_ERI_TASK_TUPLE but only the operators + // that make sense in xs_xx geometry are listed (opop_coulomb_opop requires + // σ·p on both bra and ket, which is incompatible with one side being a + // dummy-s fitting shell; it is intentionally omitted). + // - 3coulomb_opop (CoulombσpσpOper): two σ·p on the AO ket pair (4 comps) + // - 3coulomb_op (CoulombσpOper): single σ·p on the 2nd AO ket function + // ν (3 Cartesian comps). No 4-center + // sibling — 3-center only. +#define BOOST_PP_RKB_ERI3_TASK_TUPLE (3coulomb_opop, 3coulomb_op) +#define BOOST_PP_RKB_ERI3_TASK_OPER_TUPLE (CoulombσpσpOper, CoulombσpOper) +#define BOOST_PP_RKB_ERI3_TASK_LIST \ + BOOST_PP_TUPLE_TO_LIST(BOOST_PP_RKB_ERI3_TASK_TUPLE) +#define BOOST_PP_RKB_ERI3_TASK_OPER_LIST \ + BOOST_PP_TUPLE_TO_LIST(BOOST_PP_RKB_ERI3_TASK_OPER_TUPLE) + + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI3; ++d) { +#define BOOST_PP_RKB_ERI3_MCR1(r, data, elem) \ + taskmgr.add(task_label(BOOST_PP_STRINGIZE(elem), d)); + + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR1, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR1 + } +#endif + #ifdef LIBINT_INCLUDE_ERI3 for (unsigned int d = 0; d <= LIBINT_INCLUDE_ERI3; ++d) { taskmgr.add(task_label("3eri", d)); @@ -671,6 +800,115 @@ void try_main(int argc, char* argv[]) { cparams->num_bf(task_label("eri", d), 4); } #endif + +#ifdef LIBINT_INCLUDE_RKB_ERI + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI; ++d) { +#if defined(LIBINT_RKB_ERI_MAX_AM_LIST) +#define BOOST_PP_RKB_ERI_MCR2(r, data, elem) \ + cparams->max_am( \ + task_label(BOOST_PP_STRINGIZE(elem), d), \ + token(LIBINT_RKB_ERI_MAX_AM_LIST, ',', d)); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI_MCR2, _, BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR2 +#elif defined(LIBINT_RKB_ERI_MAX_AM) +#define BOOST_PP_RKB_ERI_MCR3(r, data, elem) \ + cparams->max_am(task_label(BOOST_PP_STRINGIZE(elem), d), \ + LIBINT_RKB_ERI_MAX_AM); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI_MCR3, _, BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR3 +#endif +#if defined(LIBINT_RKB_ERI_OPT_AM_LIST) +#define BOOST_PP_RKB_ERI_MCR4(r, data, elem) \ + cparams->max_am_opt( \ + task_label(BOOST_PP_STRINGIZE(elem), d), \ + token(LIBINT_RKB_ERI_OPT_AM_LIST, ',', d)); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI_MCR4, _, BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR4 +#elif defined(LIBINT_RKB_ERI_OPT_AM) +#define BOOST_PP_RKB_ERI_MCR5(r, data, elem) \ + cparams->max_am_opt(task_label(BOOST_PP_STRINGIZE(elem), d), \ + LIBINT_RKB_ERI_OPT_AM); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI_MCR5, _, BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR5 +#endif + } + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI; ++d) { +#define BOOST_PP_RKB_ERI_MCR6(r, data, elem) \ + cparams->num_bf(task_label(BOOST_PP_STRINGIZE(elem), d), 4); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI_MCR6, _, BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR6 + } +#endif // LIBINT_INCLUDE_RKB_ERI + +#ifdef LIBINT_INCLUDE_RKB_ERI3 + // Per-task CompilationParameters for 3-center RKB ERIs. Parallel structure + // to the LIBINT_INCLUDE_ERI3 block below: + // - center 0 (fitting): LIBINT_RKB_ERI3_MAX_AM[_LIST] + // - centers 1, 2 (AO pair): LIBINT_MAX_AM[_LIST] (paired-center default) + // - num_bf = 3 (3-center dispatch table is 3-dimensional) + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI3; ++d) { +#if defined(LIBINT_RKB_ERI3_MAX_AM_LIST) +#define BOOST_PP_RKB_ERI3_MCR_MAXAM(r, data, elem) \ + cparams->max_am( \ + task_label(BOOST_PP_STRINGIZE(elem), d), \ + token(LIBINT_RKB_ERI3_MAX_AM_LIST, ',', d)); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_MAXAM, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_MAXAM +#elif defined(LIBINT_RKB_ERI3_MAX_AM) +#define BOOST_PP_RKB_ERI3_MCR_MAXAM(r, data, elem) \ + cparams->max_am(task_label(BOOST_PP_STRINGIZE(elem), d), \ + LIBINT_RKB_ERI3_MAX_AM); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_MAXAM, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_MAXAM +#endif +#if defined(LIBINT_RKB_ERI3_OPT_AM_LIST) +#define BOOST_PP_RKB_ERI3_MCR_OPTAM(r, data, elem) \ + cparams->max_am_opt( \ + task_label(BOOST_PP_STRINGIZE(elem), d), \ + token(LIBINT_RKB_ERI3_OPT_AM_LIST, ',', d)); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_OPTAM, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_OPTAM +#elif defined(LIBINT_RKB_ERI3_OPT_AM) +#define BOOST_PP_RKB_ERI3_MCR_OPTAM(r, data, elem) \ + cparams->max_am_opt(task_label(BOOST_PP_STRINGIZE(elem), d), \ + LIBINT_RKB_ERI3_OPT_AM); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_OPTAM, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_OPTAM +#endif + // Paired centers (1, 2) follow the default basis AM. +#if defined(LIBINT_MAX_AM_LIST) +#define BOOST_PP_RKB_ERI3_MCR_PAIRED(r, data, elem) \ + cparams->max_am(task_label(BOOST_PP_STRINGIZE(elem), d), \ + cparams->max_am(task_label("default", d)), 1); \ + cparams->max_am(task_label(BOOST_PP_STRINGIZE(elem), d), \ + cparams->max_am(task_label("default", d)), 2); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_PAIRED, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_PAIRED +#else +#define BOOST_PP_RKB_ERI3_MCR_PAIRED(r, data, elem) \ + cparams->max_am(task_label(BOOST_PP_STRINGIZE(elem), d), \ + cparams->max_am("default"), 1); \ + cparams->max_am(task_label(BOOST_PP_STRINGIZE(elem), d), \ + cparams->max_am("default"), 2); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_PAIRED, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_PAIRED +#endif + } + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI3; ++d) { +#define BOOST_PP_RKB_ERI3_MCR_NBF(r, data, elem) \ + cparams->num_bf(task_label(BOOST_PP_STRINGIZE(elem), d), 3); + BOOST_PP_LIST_FOR_EACH(BOOST_PP_RKB_ERI3_MCR_NBF, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR_NBF + } +#endif // LIBINT_INCLUDE_RKB_ERI3 + #ifdef LIBINT_INCLUDE_ERI3 for (unsigned int d = 0; d <= LIBINT_INCLUDE_ERI3; ++d) { #if defined(LIBINT_ERI3_MAX_AM_LIST) @@ -855,6 +1093,9 @@ void try_main(int argc, char* argv[]) { #ifdef LIBINT_INCLUDE_ERI max_deriv = std::max(LIBINT_INCLUDE_ERI, max_deriv); #endif +#ifdef LIBINT_INCLUDE_RKB_ERI + max_deriv = std::max(LIBINT_INCLUDE_RKB_ERI, max_deriv); +#endif #ifdef LIBINT_INCLUDE_ERI3 max_deriv = std::max(LIBINT_INCLUDE_ERI3, max_deriv); #endif @@ -870,6 +1111,8 @@ void try_main(int argc, char* argv[]) { #endif cparams->print(os); + g_progress.start(); + #ifdef LIBINT_INCLUDE_ONEBODY for (unsigned int d = 0; d <= LIBINT_INCLUDE_ONEBODY; ++d) { #define BOOST_PP_ONEBODY_MCR7(r, data, i, elem) \ @@ -882,16 +1125,39 @@ void try_main(int argc, char* argv[]) { #endif #ifdef LIBINT_INCLUDE_ERI #if !USE_GENERIC_ERI_BUILD - build_TwoPRep_2b_2k(os, cparams, iface); + build_TwoPRep_2b_2k(os, "eri", cparams, iface); #else for (unsigned int d = 0; d <= LIBINT_INCLUDE_ERI; ++d) { - build_TwoPRep_2b_2k(os, cparams, iface, d); + build_TwoPRep_2b_2k(os, "eri", cparams, iface, d); + } +#endif +#endif + +#ifdef LIBINT_INCLUDE_RKB_ERI + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI; ++d) { +#define BOOST_PP_RKB_ERI_MCR7(r, data, i, elem) \ + build_TwoPRep_2b_2k( \ + os, BOOST_PP_STRINGIZE(elem), cparams, iface, d); + BOOST_PP_LIST_FOR_EACH_I(BOOST_PP_RKB_ERI_MCR7, _, + BOOST_PP_RKB_ERI_TASK_LIST) +#undef BOOST_PP_RKB_ERI_MCR7 } #endif + +#ifdef LIBINT_INCLUDE_RKB_ERI3 + for (unsigned int d = 0; d <= LIBINT_INCLUDE_RKB_ERI3; ++d) { +#define BOOST_PP_RKB_ERI3_MCR7(r, data, i, elem) \ + build_TwoPRep_1b_2k( \ + os, BOOST_PP_STRINGIZE(elem), cparams, iface, d); + BOOST_PP_LIST_FOR_EACH_I(BOOST_PP_RKB_ERI3_MCR7, _, + BOOST_PP_RKB_ERI3_TASK_LIST) +#undef BOOST_PP_RKB_ERI3_MCR7 + } #endif + #ifdef LIBINT_INCLUDE_ERI3 for (unsigned int d = 0; d <= LIBINT_INCLUDE_ERI3; ++d) { - build_TwoPRep_1b_2k(os, cparams, iface, d); + build_TwoPRep_1b_2k(os, "3eri", cparams, iface, d); } #if LIBINT_ERI3_PURE_SH iface->to_params(iface->macro_define("ERI3_PURE_SH", 1)); @@ -916,6 +1182,8 @@ void try_main(int argc, char* argv[]) { build_G12DKH_2b_2k(os, cparams, iface); #endif + g_progress.finish(); + // Generate code for the set-level RRs std::deque decl_filenames, def_filenames; generate_rr_code(os, cparams, decl_filenames, def_filenames); @@ -988,15 +1256,31 @@ void print_config(std::ostream& os) { #ifdef LIBINT_INCLUDE_G12DKH os << "Will support G12DKH" << endl; #endif +#ifdef LIBINT_INCLUDE_RKB_ERI + os << "Will support restricted kinetic balance (RKB) 4-center ERIs"; + if (LIBINT_INCLUDE_RKB_ERI > 0) + os << " (deriv order = " << LIBINT_INCLUDE_RKB_ERI << ")"; + os << endl; +#endif +#ifdef LIBINT_INCLUDE_RKB_ERI3 + os << "Will support restricted kinetic balance (RKB) 3-center ERIs"; + if (LIBINT_INCLUDE_RKB_ERI3 > 0) + os << " (deriv order = " << LIBINT_INCLUDE_RKB_ERI3 << ")"; + os << endl; +#endif } -#ifdef LIBINT_INCLUDE_ERI -void build_TwoPRep_2b_2k(std::ostream& os, - const std::shared_ptr& cparams, - std::shared_ptr& iface, - unsigned int deriv_level) { - const std::string task = task_label("eri", deriv_level); - typedef TwoPRep_11_11_sq TwoPRep_sh_11_11; +#if defined(LIBINT_INCLUDE_ERI) || defined(LIBINT_INCLUDE_RKB_ERI) +template +static void build_TwoPRep_2b_2k( + std::ostream& os, std::string label, + const std::shared_ptr& cparams, + std::shared_ptr& iface, unsigned int deriv_level) { + typedef GenIntegralSet_11_11 TwoBody_sh_11_11; + typedef typename OperType::Descriptor OperDescrType; + + const std::string task = task_label(label, deriv_level); + vector shells; unsigned int lmax = cparams->max_am(task); for (unsigned int l = 0; l <= lmax; l++) { @@ -1008,6 +1292,7 @@ void build_TwoPRep_2b_2k(std::ostream& os, taskmgr.current(task); iface->to_params(iface->macro_define(std::string("MAX_AM_") + task, lmax)); + const auto nullaux = typename TwoBody_sh_11_11::AuxIndexType(0u); // // Construct graphs for each desired target integral and // 1) generate source code for the found traversal path @@ -1022,12 +1307,21 @@ void build_TwoPRep_2b_2k(std::ostream& os, std::shared_ptr context(new CppCodeContext(cparams)); std::shared_ptr memman(new WorstFitMemoryManager()); + // the shell sets to generate code for are those that are canonical with + // respect to the permutations the engine performs for this operator + // (see the descriptor's p1p2_swappable and pair_swap) + constexpr bool p1p2_swappable = OperDescrType::p1p2_swappable; + constexpr PairSwap pair_swap = OperDescrType::pair_swap; + + // Note: la, lb, lc, ld generate code for chemist notation (ab|O|cd), where O + // is a two-body operator. for (unsigned int la = 0; la <= lmax; la++) { for (unsigned int lb = 0; lb <= lmax; lb++) { for (unsigned int lc = 0; lc <= lmax; lc++) { for (unsigned int ld = 0; ld <= lmax; ld++) { if (!ShellQuartetSetPredicate( - LIBINT_SHELL_SET)>::value(la, lb, lc, ld)) + LIBINT_SHELL_SET)>::value(la, lb, lc, ld, p1p2_swappable, + pair_swap)) continue; // std::shared_ptr tactic(new ParticleDirectionTactic(la+lb > @@ -1039,13 +1333,24 @@ void build_TwoPRep_2b_2k(std::ostream& os, const int lim = 1; if (!(la == lim && lb == lim && lc == lim && ld == lim)) continue; #endif + // this will hold all target shell sets + std::vector> targets; + + ///////////////////////////////// + // loop over operator components + ///////////////////////////////// + // one target per operator component + std::vector descrs; + for (unsigned int p = 0; p != OperDescrType::max_key; ++p) + descrs.emplace_back(make_descr(p)); // unroll only if max_am <= cparams->max_am_opt(task) using std::max; const unsigned int max_am = max(max(la, lb), max(lc, ld)); const bool need_to_optimize = (max_am <= cparams->max_am_opt(task)); + const auto nopers = descrs.size(); const bool need_to_unroll = - l_to_cgshellsize(la) * l_to_cgshellsize(lb) * + nopers * l_to_cgshellsize(la) * l_to_cgshellsize(lb) * l_to_cgshellsize(lc) * l_to_cgshellsize(ld) <= cparams->unroll_threshold(); const unsigned int unroll_threshold = @@ -1053,10 +1358,14 @@ void build_TwoPRep_2b_2k(std::ostream& os, ? std::numeric_limits::max() : 0; dg_xxxx->registry()->unroll_threshold(unroll_threshold); + // CSE and condense_expr act on the unrolled (CGF-level) expressions + // only; the integral-set DAG is deduplicated by instance regardless. + // Multi-component (RKB) operators rarely unroll (nopers multiplies + // the unroll cost above), but LIBINT__OPT_AM is honored for + // them exactly as for every other class. dg_xxxx->registry()->do_cse(need_to_optimize); dg_xxxx->registry()->condense_expr(condense_expr( cparams->unroll_threshold(), cparams->max_vector_length() > 1)); - // dg_xxxx->registry()->condense_expr(true); // Need to accumulate integrals? dg_xxxx->registry()->accumulate_targets( cparams->accumulate_targets()); @@ -1070,7 +1379,7 @@ void build_TwoPRep_2b_2k(std::ostream& os, //////////// // NB translational invariance is now handled by CR_DerivGauss CartesianDerivIterator<4> diter(deriv_level); - std::vector> targets; + bool last_deriv = false; do { CGShell a(la); @@ -1087,21 +1396,18 @@ void build_TwoPRep_2b_2k(std::ostream& os, } } - std::shared_ptr abcd = - TwoPRep_sh_11_11::Instance(a, b, c, d, mType(0u)); - targets.push_back(abcd); + // operator component loop + for (unsigned int op = 0; op != descrs.size(); ++op) { + OperType oper(descrs[op]); + + std::shared_ptr abcd = + TwoBody_sh_11_11::Instance(a, b, c, d, nullaux, oper); + targets.push_back(abcd); + } + last_deriv = diter.last(); if (!last_deriv) diter.next(); } while (!last_deriv); - // append all derivatives as targets to the graph - for (std::vector>::const_iterator - t = targets.begin(); - t != targets.end(); ++t) { - std::shared_ptr t_ptr = - std::dynamic_pointer_cast(*t); - dg_xxxx->append_target(t_ptr); - } - // make label that characterizes this set of targets // use the label of the nondifferentiated integral as a base std::string abcd_label; @@ -1110,33 +1416,52 @@ void build_TwoPRep_2b_2k(std::ostream& os, CGShell b(lb); CGShell c(lc); CGShell d(ld); - std::shared_ptr abcd = - TwoPRep_sh_11_11::Instance(a, b, c, d, mType(0u)); - abcd_label = abcd->label(); + + if constexpr (std::is_same::value) { + OperType oper; + oper = OperType(descrs[0]); + std::shared_ptr abcd = + TwoBody_sh_11_11::Instance(a, b, c, d, nullaux, oper); + abcd_label = abcd->label(); + } else { + std::ostringstream oss; + oss << "_" << a.label() << "_" << b.label(); + oss << "_" << label; + oss << "_" << c.label() << "_" << d.label(); + abcd_label = oss.str(); + } } // + derivative level (if deriv_level > 0) - std::string label; + std::string eval_label; { - label = ""; + eval_label = ""; if (deriv_level != 0) { std::ostringstream oss; oss << "deriv" << deriv_level; - label += oss.str(); + eval_label += oss.str(); } - label += abcd_label; + eval_label += abcd_label; } - std::cout << "working on " << label << " ... "; - std::cout.flush(); + g_progress.current_task = eval_label; + g_progress.print(); std::string prefix(cparams->source_directory()); std::deque decl_filenames; std::deque def_filenames; - // this will generate code for these targets, and potentially generate - // code for its prerequisites + // append all targets to the graph + for (auto it = targets.begin(); it != targets.end(); ++it) { + std::shared_ptr t_ptr = + std::dynamic_pointer_cast(*it); + dg_xxxx->append_target(t_ptr); + } + + // this will generate code for these targets, and potentially + // generate code for its prerequisites GenerateCode(dg_xxxx, context, cparams, strat, tactic, memman, - decl_filenames, def_filenames, prefix, label, false); + decl_filenames, def_filenames, prefix, eval_label, + false); // update max stack size and # of targets const std::shared_ptr& tparams = @@ -1150,16 +1475,14 @@ void build_TwoPRep_2b_2k(std::ostream& os, ostringstream oss; oss << context->label_to_function_name("libint2_build_" + task) << "[" << la << "][" << lb << "][" << lc << "][" << ld - << "] = " << context->label_to_function_name(label) + << "] = " << context->label_to_function_name(eval_label) << context->end_of_stat() << endl; iface->to_static_init(oss.str()); // need to declare this function internally - for (std::deque::const_iterator i = - decl_filenames.begin(); - i != decl_filenames.end(); ++i) { + for (auto& decl_filename : decl_filenames) { oss.str(""); - oss << "#include <" << *i << ">" << endl; + oss << "#include <" << decl_filename << ">" << endl; iface->to_int_iface(oss.str()); } @@ -1169,24 +1492,30 @@ void build_TwoPRep_2b_2k(std::ostream& os, dg_xxxx->reset(); memman->reset(); - std::cout << "done" << std::endl; - } // end of d loop } // end of c loop } // end of b loop } // end of a loop } -#endif // LIBINT_INCLUDE_ERI +#endif // LIBINT_INCLUDE_ERI || LIBINT_INCLUDE_RKB_ERI -#ifdef LIBINT_INCLUDE_ERI3 +#if defined(LIBINT_INCLUDE_ERI3) || defined(LIBINT_INCLUDE_RKB_ERI3) -void build_TwoPRep_1b_2k(std::ostream& os, - const std::shared_ptr& cparams, - std::shared_ptr& iface, - unsigned int deriv_level) { - const std::string task = task_label("3eri", deriv_level); - typedef TwoPRep_11_11_sq TwoPRep_sh_11_11; +// Templated on OperType so the same dummy-center "3-center via 4-center" +// machinery serves plain TwoPRep (ERI3) and RKB CoulombσpσpOper (RKB_ERI3). +// The composite recurrence (CR_11_Coulombσpσp_11) decomposes σ·p_c σ·p_d into +// derivatives of plain TwoPRep_11_11 children; those children naturally route +// through DerivGaussV2 via the strategy registered for TwoPRep_11_11 — so no +// special wiring is needed here for RKB. +template +static void build_TwoPRep_1b_2k( + std::ostream& os, std::string label, + const std::shared_ptr& cparams, + std::shared_ptr& iface, unsigned int deriv_level) { + const std::string task = task_label(label, deriv_level); + typedef GenIntegralSet_11_11 TwoBody_sh_11_11; + typedef typename OperType::Descriptor OperDescrType; vector shells; const unsigned int lmax = cparams->max_am(task); const unsigned int lmax_default = @@ -1218,13 +1547,20 @@ void build_TwoPRep_1b_2k(std::ostream& os, std::shared_ptr context(new CppCodeContext(cparams)); std::shared_ptr memman(new WorstFitMemoryManager()); + const auto nullaux = typename TwoBody_sh_11_11::AuxIndexType(0u); + for (unsigned int lbra = 0; lbra <= lmax; lbra++) { for (unsigned int lc = 0; lc <= lmax_default; lc++) { for (unsigned int ld = 0; ld <= lmax_default; ld++) { - // eliminate some cases depending on the desired convention - if (!ShellTripletSetPredicate( - LIBINT_SHELL_SET)>::value(lbra, lc, ld)) - continue; + // eliminate some cases depending on the desired convention: the + // Standard/ORCA triangle (lc>=ld / lc<=ld) applies only if the engine + // may swap the two functions of the paired (AO) centers (see the + // descriptor's pair_swap) + if (OperDescrType::pair_swap != PairSwap::none) { + if (!ShellTripletSetPredicate( + LIBINT_SHELL_SET)>::value(lbra, lc, ld)) + continue; + } // I will use 4-center recurrence relations and integrals, and have one // center carry an s function unfortunately, depending on the direction @@ -1243,11 +1579,17 @@ void build_TwoPRep_1b_2k(std::ostream& os, if (!(lbra == lim && lc == lim && ld == lim)) continue; #endif + // one target per operator component + std::vector descrs; + for (unsigned int p = 0; p != OperDescrType::max_key; ++p) + descrs.emplace_back(make_descr(p)); + const auto nopers = descrs.size(); + // unroll only if max_am <= cparams->max_am_opt(task) using std::max; const unsigned int max_am = max(max(lc, ld), lbra); const bool need_to_optimize = (max_am <= cparams->max_am_opt(task)); - const bool need_to_unroll = l_to_cgshellsize(lbra) * + const bool need_to_unroll = nopers * l_to_cgshellsize(lbra) * l_to_cgshellsize(lc) * l_to_cgshellsize(ld) <= cparams->unroll_threshold(); @@ -1256,10 +1598,11 @@ void build_TwoPRep_1b_2k(std::ostream& os, ? std::numeric_limits::max() : 0; dg_xxx->registry()->unroll_threshold(unroll_threshold); + // see build_TwoPRep_2b_2k: LIBINT__OPT_AM is honored for + // multi-component operators as for every other class dg_xxx->registry()->do_cse(need_to_optimize); dg_xxx->registry()->condense_expr(condense_expr( cparams->unroll_threshold(), cparams->max_vector_length() > 1)); - // dg_xxx->registry()->condense_expr(true); // Need to accumulate integrals? dg_xxx->registry()->accumulate_targets(cparams->accumulate_targets()); @@ -1268,7 +1611,7 @@ void build_TwoPRep_1b_2k(std::ostream& os, //////////// // NB translational invariance is now handled by CR_DerivGauss CartesianDerivIterator<3> diter(deriv_level); - std::vector> targets; + std::vector> targets; bool last_deriv = false; do { CGShell a = (dummy_center == 0) ? CGShell::unit() : CGShell(lbra); @@ -1276,8 +1619,19 @@ void build_TwoPRep_1b_2k(std::ostream& os, CGShell c(lc); CGShell d(ld); #if LIBINT_ERI3_PURE_SH - if (dummy_center == 1 && deriv_level == 0) a.pure_sh(true); - if (dummy_center == 0 && deriv_level == 0) b.pure_sh(true); + // pure-SH on the fitting center of plain ERI3 only. The RKB + // operators cannot use it: pure_sh() enables the Ahlrichs + // simplification in the VRR, which is only valid if the shell's + // cartesian components are projected to solid harmonics as is. The + // RKB kernels are built from derivative TwoPRep children, and + // translational invariance routes a derivative onto the fitting + // shell; the derivative of a (pure) shell needs exactly the + // components the simplification drops. The same reason restricts + // pure_sh to deriv_level == 0 for plain ERI3. + if constexpr (std::is_same::value) { + if (dummy_center == 1 && deriv_level == 0) a.pure_sh(true); + if (dummy_center == 0 && deriv_level == 0) b.pure_sh(true); + } #endif unsigned int center = 0; @@ -1292,19 +1646,21 @@ void build_TwoPRep_1b_2k(std::ostream& os, ++center; } - // use 4-center integrals - std::shared_ptr abcd = - TwoPRep_sh_11_11::Instance(a, b, c, d, mType(0u)); - targets.push_back(abcd); + // emit one target per operator component (1 for plain ERI3, 4 for + // RKB CoulombσpσpOper) + for (unsigned int op = 0; op != descrs.size(); ++op) { + OperType oper(descrs[op]); + std::shared_ptr abcd = + TwoBody_sh_11_11::Instance(a, b, c, d, nullaux, oper); + targets.push_back(abcd); + } last_deriv = diter.last(); if (!last_deriv) diter.next(); } while (!last_deriv); // append all derivatives as targets to the graph - for (std::vector>::const_iterator t = - targets.begin(); - t != targets.end(); ++t) { + for (auto t = targets.begin(); t != targets.end(); ++t) { std::shared_ptr t_ptr = - std::dynamic_pointer_cast(*t); + std::dynamic_pointer_cast(*t); dg_xxx->append_target(t_ptr); } @@ -1317,28 +1673,45 @@ void build_TwoPRep_1b_2k(std::ostream& os, CGShell c(lc); CGShell d(ld); #if LIBINT_ERI3_PURE_SH - if (dummy_center == 1 && deriv_level == 0) a.pure_sh(true); - if (dummy_center == 0 && deriv_level == 0) b.pure_sh(true); + if constexpr (std::is_same::value) { + if (dummy_center == 1 && deriv_level == 0) a.pure_sh(true); + if (dummy_center == 0 && deriv_level == 0) b.pure_sh(true); + } #endif - std::shared_ptr abcd = - TwoPRep_sh_11_11::Instance(a, b, c, d, mType(0u)); - abcd_label = abcd->label(); + if constexpr (std::is_same::value) { + OperType oper(descrs[0]); + std::shared_ptr abcd = + TwoBody_sh_11_11::Instance(a, b, c, d, nullaux, oper); + abcd_label = abcd->label(); + } else { + // For RKB operators, build the label by hand (matches the + // build_TwoPRep_2b_2k convention) so that the operator name is + // embedded in the source-file name without depending on the + // TwoPRep label format. + std::ostringstream oss; + oss << "_" << a.label() << "_" << b.label(); + oss << "_" << label; + oss << "_" << c.label() << "_" << d.label(); + abcd_label = oss.str(); + } } // + derivative level (if deriv_level > 0) - std::string label; + std::string eval_label; { - label = ""; + eval_label = ""; if (deriv_level != 0) { std::ostringstream oss; oss << "deriv" << deriv_level; - label += oss.str(); + eval_label += oss.str(); } - label += "eri3"; - label += abcd_label; + if constexpr (std::is_same::value) { + eval_label += "eri3"; + } + eval_label += abcd_label; } - std::cout << "working on " << label << " ... "; - std::cout.flush(); + g_progress.current_task = eval_label; + g_progress.print(); std::string prefix(cparams->source_directory()); std::deque decl_filenames; @@ -1347,7 +1720,7 @@ void build_TwoPRep_1b_2k(std::ostream& os, // this will generate code for this targets, and potentially generate // code for its prerequisites GenerateCode(dg_xxx, context, cparams, strat, tactic, memman, - decl_filenames, def_filenames, prefix, label, false); + decl_filenames, def_filenames, prefix, eval_label, false); // update max stack size and # of targets const std::shared_ptr& tparams = @@ -1361,15 +1734,14 @@ void build_TwoPRep_1b_2k(std::ostream& os, ostringstream oss; oss << context->label_to_function_name("libint2_build_" + task) << "[" << lbra << "][" << lc << "][" << ld - << "] = " << context->label_to_function_name(label) + << "] = " << context->label_to_function_name(eval_label) << context->end_of_stat() << endl; iface->to_static_init(oss.str()); // need to declare this function internally - for (std::deque::const_iterator i = decl_filenames.begin(); - i != decl_filenames.end(); ++i) { + for (auto& decl_filename : decl_filenames) { oss.str(""); - oss << "#include <" << *i << ">" << endl; + oss << "#include <" << decl_filename << ">" << endl; iface->to_int_iface(oss.str()); } @@ -1378,14 +1750,12 @@ void build_TwoPRep_1b_2k(std::ostream& os, #endif dg_xxx->reset(); memman->reset(); - - std::cout << "done" << std::endl; - } // end of d loop } // end of c loop } // end of bra loop } -#endif // LIBINT_INCLUDE_ERI3 + +#endif // LIBINT_INCLUDE_ERI3 || LIBINT_INCLUDE_RKB_ERI3 #ifdef LIBINT_INCLUDE_ERI2 @@ -1537,15 +1907,16 @@ void build_TwoPRep_1b_1k(std::ostream& os, label += abcd_label; } - std::cout << "working on " << label << " ... "; + g_progress.current_task = label; + g_progress.print(); std::cout.flush(); std::string prefix(cparams->source_directory()); std::deque decl_filenames; std::deque def_filenames; - // this will generate code for this targets, and potentially generate code - // for its prerequisites + // this will generate code for this targets, and potentially generate + // code for its prerequisites GenerateCode(dg_xxx, context, cparams, strat, tactic, memman, decl_filenames, def_filenames, prefix, label, false); @@ -1554,7 +1925,8 @@ void build_TwoPRep_1b_1k(std::ostream& os, taskmgr.current().params(); tparams->max_stack_size(max_am, memman->max_memory_used()); tparams->max_ntarget(targets.size()); - // os << " Max memory used = " << memman->max_memory_used() << std::endl; + // os << " Max memory used = " << memman->max_memory_used() << + // std::endl; // set pointer to the top-level evaluator function ostringstream oss; @@ -1565,10 +1937,9 @@ void build_TwoPRep_1b_1k(std::ostream& os, iface->to_static_init(oss.str()); // need to declare this function internally - for (std::deque::const_iterator i = decl_filenames.begin(); - i != decl_filenames.end(); ++i) { + for (auto& decl_filename : decl_filenames) { oss.str(""); - oss << "#include <" << *i << ">" << endl; + oss << "#include <" << decl_filename << ">" << endl; iface->to_int_iface(oss.str()); } @@ -1577,9 +1948,6 @@ void build_TwoPRep_1b_1k(std::ostream& os, #endif dg_xxx->reset(); memman->reset(); - - std::cout << "done" << std::endl; - } // end of ket loop } // end of bra loop } @@ -1748,8 +2116,8 @@ void build_R12kG12_2b_2k(std::ostream& os, std::deque decl_filenames; std::deque def_filenames; - // this will generate code for this targets, and potentially generate - // code for its prerequisites + // this will generate code for this targets, and potentially + // generate code for its prerequisites GenerateCode(dg_xxxx, context, cparams, strat, tactic, memman, decl_filenames, def_filenames, prefix, label, false); @@ -2073,9 +2441,9 @@ void build_G12DKH_2b_2k(std::ostream& os, label = os.str(); } std::string prefix(cparams->source_directory()); - std::string decl_filename(prefix + context->label_to_name(label)); + std::string decl_filename(prefix + context->label_to_filename(label)); decl_filename += ".h"; - std::string src_filename(prefix + context->label_to_name(label)); + std::string src_filename(prefix + context->label_to_filename(label)); src_filename += ".cc"; std::basic_ofstream declfile(decl_filename.c_str()); std::basic_ofstream srcfile(src_filename.c_str()); @@ -2101,11 +2469,11 @@ void build_G12DKH_2b_2k(std::ostream& os, oss << "#include <" << decl_filename << ">" << endl; iface->to_int_iface(oss.str()); - // For the most expensive (i.e. presumably complete) graph extract all - // precomputed quantities -- these will be members of the evaluator - // structure also extract all RRs -- need to keep track of these to - // figure out which external symbols appearing in RR code belong to - // this task also + // For the most expensive (i.e. presumably complete) graph extract + // all precomputed quantities -- these will be members of the + // evaluator structure also extract all RRs -- need to keep track of + // these to figure out which external symbols appearing in RR code + // belong to this task also if (la == lmax && lb == lmax && lc == lmax && ld == lmax) extract_symbols(dg_xxxx); @@ -2143,6 +2511,18 @@ void config_to_api(const std::shared_ptr& cparams, iface->to_params(iface->macro_define("DERIV_ERI_ORDER", LIBINT_INCLUDE_ERI)); max_deriv_order = std::max(max_deriv_order, LIBINT_INCLUDE_ERI); #endif +#ifdef LIBINT_INCLUDE_RKB_ERI + iface->to_params(iface->macro_define("SUPPORT_RKB_ERI", 1)); + iface->to_params( + iface->macro_define("DERIV_RKB_ERI_ORDER", LIBINT_INCLUDE_RKB_ERI)); + max_deriv_order = std::max(max_deriv_order, LIBINT_INCLUDE_RKB_ERI); +#endif +#ifdef LIBINT_INCLUDE_RKB_ERI3 + iface->to_params(iface->macro_define("SUPPORT_RKB_ERI3", 1)); + iface->to_params( + iface->macro_define("DERIV_RKB_ERI3_ORDER", LIBINT_INCLUDE_RKB_ERI3)); + max_deriv_order = std::max(max_deriv_order, LIBINT_INCLUDE_RKB_ERI3); +#endif #ifdef LIBINT_INCLUDE_ERI3 iface->to_params(iface->macro_define("SUPPORT_ERI3", 1)); iface->to_params( @@ -2172,7 +2552,8 @@ void config_to_api(const std::shared_ptr& cparams, // generated tasks declare all tasks in a range of valid tasks as defined or // not LibraryTaskManager& taskmgr = LibraryTaskManager::Instance(); - // the range is defined by max # of centers, max deriv order, and operator set + // the range is defined by max # of centers, max deriv order, and operator + // set const size_t max_ncenter = 4; for (unsigned int ncenter = 0; ncenter <= max_ncenter; ++ncenter) { std::stringstream oss; @@ -2198,8 +2579,9 @@ void config_to_api(const std::shared_ptr& cparams, { // 2-body ints -#define BOOST_PP_TWOBODY_TASKOPER_TUPLE \ - ("eri", "r12kg12", "r12_0_g12", "r12_2_g12", "g12_T1_g12", "g12dkh") +#define BOOST_PP_TWOBODY_TASKOPER_TUPLE \ + ("eri", "coulomb_opop", "coulomb_op", "opop_coulomb_opop", "op_coulomb_op", \ + "r12kg12", "r12_0_g12", "r12_2_g12", "g12_T1_g12", "g12dkh") #define BOOST_PP_TWOBODY_TASKOPER_LIST \ BOOST_PP_TUPLE_TO_LIST(BOOST_PP_TWOBODY_TASKOPER_TUPLE) diff --git a/src/bin/libint/buildtest.cc b/src/bin/libint/buildtest.cc index 604a3869f..1555d5fdc 100644 --- a/src/bin/libint/buildtest.cc +++ b/src/bin/libint/buildtest.cc @@ -73,9 +73,9 @@ void generate_rr_code(std::ostream& os, os << "generating code for " << context->label_to_name(rr->label()) << " target=" << rr->rr_target()->label() << endl; - std::string decl_filename(prefix + context->label_to_name(rr->label())); + std::string decl_filename(prefix + context->label_to_filename(rr->label())); decl_filename += ".h"; - std::string def_filename(prefix + context->label_to_name(rr->label())); + std::string def_filename(prefix + context->label_to_filename(rr->label())); def_filename += ".cc"; std::basic_ofstream declfile(decl_filename.c_str()); std::basic_ofstream deffile(def_filename.c_str()); diff --git a/src/bin/libint/buildtest.h b/src/bin/libint/buildtest.h index a4923c022..1b18cf95c 100644 --- a/src/bin/libint/buildtest.h +++ b/src/bin/libint/buildtest.h @@ -30,12 +30,82 @@ #include #include +#include +#include #include #include +#include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + +/// Progress tracker for code generation. On a terminal shows a single +/// updating line; otherwise (e.g. in CI logs) prints one line per kernel so +/// that a failure can be attributed to the kernel being generated. +struct CodeGenProgress { + unsigned int done = 0; + std::string current_task; + std::chrono::steady_clock::time_point start_time; + bool started = false; + bool interactive = false; + std::string last_printed_task; + + void start() { + start_time = std::chrono::steady_clock::now(); + started = true; + interactive = libint2_stderr_is_a_terminal(); + } + + std::string elapsed() const { + const auto secs = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time) + .count(); + std::ostringstream oss; + oss << std::setfill('0') << std::setw(2) << secs / 60 << ":" << std::setw(2) + << secs % 60; + return oss.str(); + } + + void print() { + if (!started) return; + if (interactive) { + static const char spinner[] = "|/-\\"; + std::cerr << "\r " << spinner[done % 4] << " " << elapsed() << " [" + << done << " functions generated] " << current_task + << " " << std::flush; + } else if (current_task != last_printed_task) { + std::cerr << " " << elapsed() << " [" << done << "] " << current_task + << std::endl; + last_printed_task = current_task; + } + } + + void finish() { + if (!started) return; + std::cerr << (interactive ? "\r" : "") << " done " << elapsed() << " [" + << done << " functions generated]" + << (interactive ? " " : "") + << std::endl; + started = false; + } + + private: + static bool libint2_stderr_is_a_terminal() { +#ifdef _WIN32 + return _isatty(_fileno(stderr)) != 0; +#else + return isatty(fileno(stderr)) != 0; +#endif + } +}; +// the global progress reporter of the code generator, used by GenerateCode() +inline CodeGenProgress g_progress; namespace libint2 { @@ -263,7 +333,6 @@ void GenerateCode(const std::shared_ptr& dg, // if there are missing prerequisites -- make a list of them PrerequisitesExtractor pe; if (dg->missing_prerequisites()) { - // std::cout << "missing some prerequisites!" << std::endl; dg->foreach (pe); } std::deque > prereq_list = pe.vertices; @@ -278,9 +347,9 @@ void GenerateCode(const std::shared_ptr& dg, } #endif - std::string decl_filename(prefix + context->label_to_name(label)); + std::string decl_filename(prefix + context->label_to_filename(label)); decl_filename += ".h"; - std::string def_filename(prefix + context->label_to_name(label)); + std::string def_filename(prefix + context->label_to_filename(label)); def_filename += ".cc"; std::basic_ofstream declfile(decl_filename.c_str()); std::basic_ofstream deffile(def_filename.c_str()); @@ -296,6 +365,11 @@ void GenerateCode(const std::shared_ptr& dg, // extract all external symbols extract_symbols(dg); + // Update progress + ++g_progress.done; + g_progress.current_task = label; + g_progress.print(); + #if PRINT_DAG_GRAPHVIZ { std::basic_ofstream dotfile(dg->label() + ".symb.dot"); diff --git "a/src/bin/libint/comp_11_Coulomb\317\203p_11.h" "b/src/bin/libint/comp_11_Coulomb\317\203p_11.h" new file mode 100644 index 000000000..fd85b01f4 --- /dev/null +++ "b/src/bin/libint/comp_11_Coulomb\317\203p_11.h" @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_COMP_11_COULOMBΣP_11_H +#define LIBINT_COMP_11_COULOMBΣP_11_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace libint2 { + +/** + * Computes the 3-center single-σ·p integral (a b | 1/r_{12} | c σ·p d), with + * the fitting function as a (bra1) and the unit function as b (ket1). Each + * Cartesian component k is the TwoPRep integral over d differentiated with + * respect to its center D in direction k (libint's derivative convention), + * i.e. exactly one first-derivative ERI child, so it routes naturally through + * the TwoPRep DerivGaussV2 strategy; unlike Coulombσpσp there is no σ·σ + * folding. Since p_k = -i ∂/∂r_k = +i ∂/∂D_k, the user multiplies component k + * by +i and contracts with σ_k (see the rkb_operators page in engine.h). + * @tparam F basis function type. valid choices are CGShell or CGF + */ +template +class CR_11_Coulombσp_11 : public GenericRecurrenceRelation< + CR_11_Coulombσp_11, F, + GenIntegralSet_11_11> { + public: + typedef CR_11_Coulombσp_11 ThisType; + typedef F BasisFunctionType; + typedef CoulombσpOper OperType; + typedef GenIntegralSet_11_11 TargetType; + typedef GenericRecurrenceRelation + ParentType; + friend class GenericRecurrenceRelation; + static const unsigned int max_nchildren = 1; + + using ParentType::Instance; + + static bool directional() { return false; } + + private: + using ParentType::is_simple; + using ParentType::target_; + using ParentType::RecurrenceRelation::expr_; + using ParentType::RecurrenceRelation::nflops_; + + /// Constructor is private, used by Instance that maintains + /// registry of these objects + CR_11_Coulombσp_11(const std::shared_ptr&, unsigned int = 0); + + static std::string descr() { return "CR"; } + + /// The single term of Cartesian component @p k: the first derivative of d + /// (ket2) with respect to its center, direction k. The constructor (the + /// children) and generate_code() (the element-wise expression) are both + /// driven by this table, so they cannot disagree. + static RKBFoldTerms fold_terms(int k) { + if (k < 0 || k > 2) + throw std::runtime_error("CR_11_Coulombσp_11: invalid Cartesian index " + + std::to_string(k)); + return {RKBFoldTerm{1, {{-1, -1, -1, k}}}}; + } + + // --- Code sharing overrides --- + // All shell sets with the same Cartesian component share one function + // (the element-wise copy is spectator-dimension-independent). + + std::string generate_label() const override { + return "CR_Coulombop_" + + std::to_string(target_->oper()->descr().cartesian_index()); + } + + std::string spfunction_call( + const std::shared_ptr& context, + const std::shared_ptr& dims) const override { + return rkb_fold_spfunction_call(*this, target_, context); + } + + std::shared_ptr adapt_dims_( + const std::shared_ptr& dims) const override { + return rkb_fold_adapt_dims(dims); + } + + /// emits the element-wise copy target = src0 (0 flops) + void generate_code(const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::string& funcname, std::ostream& decl, + std::ostream& def) override { + rkb_fold_generate_code_from_terms( + *this, fold_terms(target_->oper()->descr().cartesian_index()), + "CR_11_Coulombσp_11", context, dims, funcname, decl, def); + } +}; + +template +CR_11_Coulombσp_11::CR_11_Coulombσp_11( + const std::shared_ptr& Tint, unsigned int) + : ParentType(Tint, 0) { + assert(Tint->num_func_bra(/* particle */ 0) == 1); + assert(Tint->num_func_bra(/* particle */ 1) == 1); + assert(Tint->num_func_ket(/* particle */ 0) == 1); + assert(Tint->num_func_ket(/* particle */ 1) == 1); + + F a(Tint->bra(0, 0)); + F b(Tint->ket(0, 0)); + F c(Tint->bra(1, 0)); + F d(Tint->ket(1, 0)); + + if (a.contracted() || b.contracted() || c.contracted() || d.contracted()) + return; + + ChildFactory> + factory(this); + rkb_fold_make_children(factory, a, b, c, d, + fold_terms(Tint->oper()->descr().cartesian_index()), + is_simple(), expr_, nflops_); + +} // CR_11_Coulombσp_11::CR_11_Coulombσp_11 + +} // namespace libint2 + +#endif // LIBINT_COMP_11_COULOMBΣP_11_H diff --git "a/src/bin/libint/comp_11_Coulomb\317\203p\317\203p_11.h" "b/src/bin/libint/comp_11_Coulomb\317\203p\317\203p_11.h" new file mode 100644 index 000000000..a5126cef6 --- /dev/null +++ "b/src/bin/libint/comp_11_Coulomb\317\203p\317\203p_11.h" @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_COMP_11_COULOMBΣPΣP_11_H +#define LIBINT_COMP_11_COULOMBΣPΣP_11_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace libint2 { + +/** + * Computes the integral (a b | 1/r_{12} | (σ·p c)† (σ·p d)) over CGShell/CGF, + * with σ·p acting on both functions of electron 2 (c = bra2, d = ket2), as a + * linear combination of first-derivative TwoPRep integrals. The Dirac identity + * σ_i σ_j = δ_ij + i ε_ijk σ_k folds the two Pauli matrices into 4 quaternion + * components: 0: ∇_c·∇_d, 1-3: (∇_c × ∇_d)_{x,y,z}. The derivatives are with + * respect to the centers of c and d; since (p_i c)† (p_j d) = + * (-i ∂_{C,i} c)(+i ∂_{D,j} d) = ∂_{C,i} c ∂_{D,j} d, the user assembles the + * 2×2 spin block as q₀ + i (q₁ σ_x + q₂ σ_y + q₃ σ_z) with no further + * prefactor (see the rkb_operators page in engine.h). + * @tparam F basis function type. valid choices are CGShell or CGF + */ +template +class CR_11_Coulombσpσp_11 + : public GenericRecurrenceRelation< + CR_11_Coulombσpσp_11, F, + GenIntegralSet_11_11> { + public: + typedef CR_11_Coulombσpσp_11 ThisType; + typedef F BasisFunctionType; + typedef CoulombσpσpOper OperType; + typedef GenIntegralSet_11_11 TargetType; + typedef GenericRecurrenceRelation + ParentType; + friend class GenericRecurrenceRelation; + // children_.reserve() hint only (see generic_rr.h): the scalar component + // sums 3 deriv-ERI children, the cross-product components 2 + static const unsigned int max_nchildren = 3; + + using ParentType::Instance; + + static bool directional() { return false; } + + private: + using ParentType::is_simple; + using ParentType::target_; + using ParentType::RecurrenceRelation::expr_; + using ParentType::RecurrenceRelation::nflops_; + + /// Constructor is private, used by Instance that maintains + /// registry of these objects + CR_11_Coulombσpσp_11(const std::shared_ptr&, unsigned int = 0); + + static std::string descr() { return "CR"; } + + /// The terms of quaternion component @p q of (σ·∇_c)(σ·∇_d) (see + /// rkb_fold_pair_terms): derivative direction i on c (bra2), j on d (ket2). + /// The constructor (the children) and generate_code() (the element-wise + /// expression) are both driven by this table, so they cannot disagree. + static RKBFoldTerms fold_terms(int q) { + RKBFoldTerms terms; + for (const auto& p : rkb_fold_pair_terms(q)) + terms.push_back(RKBFoldTerm{p.coef, {{-1, -1, p.i, p.j}}}); + return terms; + } + + // --- Code sharing overrides --- + // All shell sets with the same quaternion component share one function. + + std::string generate_label() const override { + return "CR_Coulombopop_" + + std::to_string(target_->oper()->descr().quaternion_index()); + } + + std::string spfunction_call( + const std::shared_ptr& context, + const std::shared_ptr& dims) const override { + return rkb_fold_spfunction_call(*this, target_, context); + } + + std::shared_ptr adapt_dims_( + const std::shared_ptr& dims) const override { + return rkb_fold_adapt_dims(dims); + } + + /// emits the element-wise fold of the children, e.g. + /// target = src0 + src1 + src2 for the scalar component + void generate_code(const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::string& funcname, std::ostream& decl, + std::ostream& def) override { + rkb_fold_generate_code_from_terms( + *this, fold_terms(target_->oper()->descr().quaternion_index()), + "CR_11_Coulombσpσp_11", context, dims, funcname, decl, def); + } +}; + +template +CR_11_Coulombσpσp_11::CR_11_Coulombσpσp_11( + const std::shared_ptr& Tint, unsigned int) + : ParentType(Tint, 0) { + assert(Tint->num_func_bra(/* particle */ 0) == 1); + assert(Tint->num_func_bra(/* particle */ 1) == 1); + assert(Tint->num_func_ket(/* particle */ 0) == 1); + assert(Tint->num_func_ket(/* particle */ 1) == 1); + + F a(Tint->bra(0, 0)); + F b(Tint->ket(0, 0)); + F c(Tint->bra(1, 0)); + F d(Tint->ket(1, 0)); + + if (a.contracted() || b.contracted() || c.contracted() || d.contracted()) + return; + + ChildFactory> + factory(this); + rkb_fold_make_children(factory, a, b, c, d, + fold_terms(Tint->oper()->descr().quaternion_index()), + is_simple(), expr_, nflops_); + +} // CR_11_Coulombσpσp_11::CR_11_Coulombσpσp_11 + +} // namespace libint2 + +#endif // LIBINT_COMP_11_COULOMBΣPΣP_11_H diff --git "a/src/bin/libint/comp_11_\317\203pCoulomb\317\203p_11.h" "b/src/bin/libint/comp_11_\317\203pCoulomb\317\203p_11.h" new file mode 100644 index 000000000..9189ea408 --- /dev/null +++ "b/src/bin/libint/comp_11_\317\203pCoulomb\317\203p_11.h" @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_COMP_11_ΣPCOULOMBΣP_11_H +#define LIBINT_COMP_11_ΣPCOULOMBΣP_11_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace libint2 { + +/** + * Computes the "Gaunt LS bilinear" integral (a σ·p b | 1/r_{12} | c σ·p d), + * with one σ·p on the second function of each electron (b = ket1 in the spin + * space of electron 1, d = ket2 in that of electron 2), in the SO(3) + * irreducible decomposition of the rank-2 tensor + * T_ij = (a ∂_{B,i} b | 1/r_{12} | c ∂_{D,j} d): 1 scalar trace + 3 + * antisymmetric (curl-curl) + 5 symmetric-traceless = 9 components, each a + * small linear combination of first-derivative TwoPRep integrals. The + * derivatives are with respect to the centers B and D; since + * (p_i b)(p_j d) = (+i ∂_{B,i} b)(+i ∂_{D,j} d) = -∂_{B,i} b ∂_{D,j} d, the + * user multiplies the tensor by -1 before contracting with σ_{1,i} σ_{2,j} + * (see the rkb_operators page in engine.h). + * + * @tparam F basis function type. valid choices are CGShell or CGF + */ +template +class CR_11_σpCoulombσp_11 + : public GenericRecurrenceRelation< + CR_11_σpCoulombσp_11, F, + GenIntegralSet_11_11> { + public: + typedef CR_11_σpCoulombσp_11 ThisType; + typedef F BasisFunctionType; + typedef σpCoulombσpOper OperType; + typedef GenIntegralSet_11_11 TargetType; + typedef GenericRecurrenceRelation + ParentType; + friend class GenericRecurrenceRelation; + static const unsigned int max_nchildren = 3; + + using ParentType::Instance; + + static bool directional() { return false; } + + private: + using ParentType::is_simple; + using ParentType::target_; + using ParentType::RecurrenceRelation::expr_; + using ParentType::RecurrenceRelation::nflops_; + + /// Constructor is private, used by Instance that maintains + /// registry of these objects + CR_11_σpCoulombσp_11(const std::shared_ptr&, unsigned int = 0); + + static std::string descr() { return "CR"; } + + /// The terms of SO(3) irreducible component @p comp of T_ij: coefficient, + /// derivative direction i on b (ket1) and j on d (ket2). The constructor + /// (the children) and generate_code() (the element-wise expression) are both + /// driven by this table, so they cannot disagree. + static RKBFoldTerms fold_terms(int comp) { + constexpr int x = 0; + constexpr int y = 1; + constexpr int z = 2; + auto T = [](int coef, int i, int j) { + return RKBFoldTerm{coef, {{-1, i, -1, j}}}; + }; + switch (comp) { + case σpCoulombσp_Descr::Scalar: // T_xx + T_yy + T_zz + return {T(1, x, x), T(1, y, y), T(1, z, z)}; + case σpCoulombσp_Descr::AntisymX: // T_yz - T_zy + return {T(1, y, z), T(-1, z, y)}; + case σpCoulombσp_Descr::AntisymY: // T_zx - T_xz + return {T(1, z, x), T(-1, x, z)}; + case σpCoulombσp_Descr::AntisymZ: // T_xy - T_yx + return {T(1, x, y), T(-1, y, x)}; + case σpCoulombσp_Descr::SymTLDiagA: // T_xx - T_yy + return {T(1, x, x), T(-1, y, y)}; + case σpCoulombσp_Descr::SymTLDiagB: // 2 T_zz - T_xx - T_yy + return {T(2, z, z), T(-1, x, x), T(-1, y, y)}; + case σpCoulombσp_Descr::SymTLOffXY: // T_xy + T_yx + return {T(1, x, y), T(1, y, x)}; + case σpCoulombσp_Descr::SymTLOffXZ: // T_xz + T_zx + return {T(1, x, z), T(1, z, x)}; + case σpCoulombσp_Descr::SymTLOffYZ: // T_yz + T_zy + return {T(1, y, z), T(1, z, y)}; + default: + throw std::runtime_error( + "CR_11_σpCoulombσp_11: invalid component index " + + std::to_string(comp)); + } + } + + // --- Code sharing overrides (mirror Coulombσpσp pattern) --- + // All shell sets with the same component share one function. + + std::string generate_label() const override { + return "CR_opCoulombop_" + + std::to_string(target_->oper()->descr().component_index()); + } + + std::string spfunction_call( + const std::shared_ptr& context, + const std::shared_ptr& dims) const override { + return rkb_fold_spfunction_call(*this, target_, context); + } + + std::shared_ptr adapt_dims_( + const std::shared_ptr& dims) const override { + return rkb_fold_adapt_dims(dims); + } + + /// emits the element-wise irrep combination of the children, e.g. + /// target = 2.0*src0 - src1 - src2 for SymTLDiagB + void generate_code(const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::string& funcname, std::ostream& decl, + std::ostream& def) override { + rkb_fold_generate_code_from_terms( + *this, fold_terms(target_->oper()->descr().component_index()), + "CR_11_σpCoulombσp_11", context, dims, funcname, decl, def); + } +}; + +template +CR_11_σpCoulombσp_11::CR_11_σpCoulombσp_11( + const std::shared_ptr& Tint, unsigned int) + : ParentType(Tint, 0) { + assert(Tint->num_func_bra(/* particle */ 0) == 1); + assert(Tint->num_func_bra(/* particle */ 1) == 1); + assert(Tint->num_func_ket(/* particle */ 0) == 1); + assert(Tint->num_func_ket(/* particle */ 1) == 1); + + F a(Tint->bra(0, 0)); + F b(Tint->ket(0, 0)); + F c(Tint->bra(1, 0)); + F d(Tint->ket(1, 0)); + + if (a.contracted() || b.contracted() || c.contracted() || d.contracted()) + return; + + ChildFactory> + factory(this); + rkb_fold_make_children(factory, a, b, c, d, + fold_terms(Tint->oper()->descr().component_index()), + is_simple(), expr_, nflops_); + +} // CR_11_σpCoulombσp_11::CR_11_σpCoulombσp_11 + +} // namespace libint2 + +#endif // LIBINT_COMP_11_ΣPCOULOMBΣP_11_H diff --git "a/src/bin/libint/comp_11_\317\203p\317\203pCoulomb\317\203p\317\203p_11.h" "b/src/bin/libint/comp_11_\317\203p\317\203pCoulomb\317\203p\317\203p_11.h" new file mode 100644 index 000000000..0404da395 --- /dev/null +++ "b/src/bin/libint/comp_11_\317\203p\317\203pCoulomb\317\203p\317\203p_11.h" @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_COMP_11_ΣPΣPCOULOMBΣPΣP_11_H +#define LIBINT_COMP_11_ΣPΣPCOULOMBΣPΣP_11_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace libint2 { + +/** + * Computes the integral + * \f$ ((\sigma_1\cdot\hat{p}) a)^\dagger ((\sigma_1\cdot\hat{p}) b) + * \frac{1}{r_{12}} + * ((\sigma_2\cdot\hat{p}) c)^\dagger ((\sigma_2\cdot\hat{p}) d) \f$ + * over CGShell/CGF by rewriting it as a linear combination of integrals over + * first derivatives of all four functions (with respect to their centers). + * + * The two σ operators act on independent spin spaces (electron 1 on the bra + * pair a, b; electron 2 on the ket pair c, d). Using the Dirac identity (see + * e.g. Eq. 1.27 of I. P. Grant, "Relativistic Quantum Theory of Atoms and + * Molecules", Springer, 2007): + * \f$ (\sigma \cdot u)(\sigma \cdot v) = (u \cdot v)I + * + i\sigma \cdot (u \times v) \f$ + * applied independently to each electron's spin space gives a tensor product + * of two quaternions with \f$ 4 \times 4 = 16 \f$ components: + * + * index = 4 * q₁ + q₂ + * + * where q₁ (bra pair) and q₂ (ket pair) are: 0=S (scalar, ∇·∇), 1=X, 2=Y, + * 3=Z (the components of the cross product ∇×∇). The components are built by + * fold_terms() as the product of the Pauli components of the two pairs (see + * rkb_fold_pair_terms): + * SS (index 0): (∇_a·∇_b)(∇_c·∇_d) 9 terms + * SX,SY,SZ (1-3), XS,YS,ZS (4,8,12): dot × cross 6 terms + * XX..ZZ (5-7, 9-11, 13-15): -(∇_a×∇_b)_i (∇_c×∇_d)_j 4 terms + * + * Sign convention: the cross × cross block includes the minus sign from + * \f$ i^2 = -1 \f$, the product of the two \f$ i \f$ factors of the Dirac + * identity: + * \f$ [i\sigma_1 \cdot (u \times v)] \otimes [i\sigma_2 \cdot (w \times x)] + * = -\sigma_{1,i} \otimes \sigma_{2,j}\; (u \times v)_i\, (w \times x)_j + * \f$ + * so the user multiplies component 4q₁+q₂ by 1 (q₁=q₂=0), i σ_{2,q₂} (q₁=0), + * i σ_{1,q₁} (q₂=0), or σ_{1,q₁} σ_{2,q₂} (both ≥ 1); the center-derivative + * convention contributes no further prefactor since + * (p_i a)† (p_j b) = ∂_{A,i} a ∂_{B,j} b (see the rkb_operators page in + * engine.h). + * + * @tparam F basis function type. valid choices are CGShell or CGF + */ +template +class CR_11_σpσpCoulombσpσp_11 + : public GenericRecurrenceRelation< + CR_11_σpσpCoulombσpσp_11, F, + GenIntegralSet_11_11> { + public: + typedef CR_11_σpσpCoulombσpσp_11 ThisType; + typedef F BasisFunctionType; + typedef σpσpCoulombσpσpOper OperType; + typedef GenIntegralSet_11_11 TargetType; + typedef GenericRecurrenceRelation + ParentType; + friend class GenericRecurrenceRelation; + // children_.reserve() hint only (see generic_rr.h): the SS component sums + // 9 deriv-ERI children, all others 6 or 4 + static const unsigned int max_nchildren = 9; + + using ParentType::Instance; + + static bool directional() { return false; } + + private: + using ParentType::is_simple; + using ParentType::target_; + using ParentType::RecurrenceRelation::expr_; + using ParentType::RecurrenceRelation::nflops_; + + /// Constructor is private, used by Instance that maintains + /// registry of these objects + CR_11_σpσpCoulombσpσp_11(const std::shared_ptr&, + unsigned int = 0); + + static std::string descr() { return "CR"; } + + /// The terms of component @p comp = 4 q₁ + q₂: the product of the terms of + /// Pauli component q₁ of (σ₁·∇_a)(σ₁·∇_b) (derivative directions on a = bra1 + /// and b = ket1) and of q₂ of (σ₂·∇_c)(σ₂·∇_d) (on c = bra2 and d = ket2), + /// see rkb_fold_pair_terms; the i's of two cross products multiply to -1. + /// The constructor (the children) and generate_code() (the element-wise + /// expression) are both driven by this table, so they cannot disagree. + static RKBFoldTerms fold_terms(int comp) { + if (comp < 0 || comp > 15) + throw std::runtime_error( + "CR_11_σpσpCoulombσpσp_11: invalid component index " + + std::to_string(comp) + " (expected 0-15)"); + const int q1 = comp / 4; + const int q2 = comp % 4; + const int sign = (q1 != 0 && q2 != 0) ? -1 : 1; + RKBFoldTerms terms; + for (const auto& p1 : rkb_fold_pair_terms(q1)) + for (const auto& p2 : rkb_fold_pair_terms(q2)) + terms.push_back( + RKBFoldTerm{sign * p1.coef * p2.coef, {{p1.i, p1.j, p2.i, p2.j}}}); + return terms; + } + + // --- Code sharing overrides --- + // All shell sets with the same component share one function. + + std::string generate_label() const override { + return "CR_opopCoulombopop_" + + std::to_string(target_->oper()->descr().component_index()); + } + + std::string spfunction_call( + const std::shared_ptr& context, + const std::shared_ptr& dims) const override { + return rkb_fold_spfunction_call(*this, target_, context); + } + + std::shared_ptr adapt_dims_( + const std::shared_ptr& dims) const override { + return rkb_fold_adapt_dims(dims); + } + + /// emits the element-wise fold of the children, e.g. + /// target = src0 + src1 + ... + src8 for the SS component + void generate_code(const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::string& funcname, std::ostream& decl, + std::ostream& def) override { + rkb_fold_generate_code_from_terms( + *this, fold_terms(target_->oper()->descr().component_index()), + "CR_11_σpσpCoulombσpσp_11", context, dims, funcname, decl, def); + } +}; + +template +CR_11_σpσpCoulombσpσp_11::CR_11_σpσpCoulombσpσp_11( + const std::shared_ptr& Tint, unsigned int) + : ParentType(Tint, 0) { + assert(Tint->num_func_bra(/* particle */ 0) == 1); + assert(Tint->num_func_bra(/* particle */ 1) == 1); + assert(Tint->num_func_ket(/* particle */ 0) == 1); + assert(Tint->num_func_ket(/* particle */ 1) == 1); + + F a(Tint->bra(0, 0)); + F b(Tint->ket(0, 0)); + F c(Tint->bra(1, 0)); + F d(Tint->ket(1, 0)); + + if (a.contracted() || b.contracted() || c.contracted() || d.contracted()) + return; + + ChildFactory> + factory(this); + rkb_fold_make_children(factory, a, b, c, d, + fold_terms(Tint->oper()->descr().component_index()), + is_simple(), expr_, nflops_); + +} // CR_11_σpσpCoulombσpσp_11::CR_11_σpσpCoulombσpσp_11 + +} // namespace libint2 + +#endif // LIBINT_COMP_11_ΣPΣPCOULOMBΣPΣP_11_H diff --git "a/src/bin/libint/comp_1_\317\203pV\317\203p_1.h" "b/src/bin/libint/comp_1_\317\203pV\317\203p_1.h" index 5ecfc796b..238918f36 100644 --- "a/src/bin/libint/comp_1_\317\203pV\317\203p_1.h" +++ "b/src/bin/libint/comp_1_\317\203pV\317\203p_1.h" @@ -22,6 +22,7 @@ #define LIBINT_COMP_1_ΣPVΣP_1_H #include +#include namespace libint2 { @@ -45,7 +46,8 @@ class CR_1_σpVσp_1 ParentType; friend class GenericRecurrenceRelation; - static const unsigned int max_nchildren = 100; // TODO figure out + // trace component folds 3 derivative children; the antisym components fold 2 + static const unsigned int max_nchildren = 3; using ParentType::Instance; @@ -78,76 +80,19 @@ CR_1_σpVσp_1::CR_1_σpVσp_1(const std::shared_ptr &Tint, // primitive Gaussians only if (a.contracted() || b.contracted()) return; - using namespace libint2::algebra; - using namespace libint2::prefactor; - using libint2::algebra::operator*; - const mType zero_m(0u); ChildFactory> factory(this); - constexpr auto x = 0; - constexpr auto y = 1; - constexpr auto z = 2; - - F Dx_a{a}; - Dx_a.deriv().inc(x); - F Dx_b{b}; - Dx_b.deriv().inc(x); - F Dy_a{a}; - Dy_a.deriv().inc(y); - F Dy_b{b}; - Dy_b.deriv().inc(y); - F Dz_a{a}; - Dz_a.deriv().inc(z); - F Dz_b{b}; - Dz_b.deriv().inc(z); - - // (a|W0|b) = (d a/dAx | V | d b/dBx) + (d a/dAy | V | d b/dBy) + (d a/dAz | V - // | d b/dBz) - switch (oper->descr().quaternionic_index()) { - case 0: { - auto Dx_a_V_Dx_b = factory.make_child(Dx_a, Dx_b, zero_m); - auto Dy_a_V_Dy_b = factory.make_child(Dy_a, Dy_b, zero_m); - auto Dz_a_V_Dz_b = factory.make_child(Dz_a, Dz_b, zero_m); - if (is_simple()) { - expr_ = Dx_a_V_Dx_b + Dy_a_V_Dy_b + Dz_a_V_Dz_b; - nflops_ += 2; - } - } break; - // (a|Wx|b) = (d a/dAy | V | d b/dBz) - (d a/dAz | V | d b/dBy) - case 1: { - auto Dy_a_V_Dz_b = factory.make_child(Dy_a, Dz_b, zero_m); - auto Dz_a_V_Dy_b = factory.make_child(Dz_a, Dy_b, zero_m); - if (is_simple()) { - expr_ = Dy_a_V_Dz_b - Dz_a_V_Dy_b; - nflops_ += 1; - } - } break; - // (a|Wy|b) = (d a/dAz | V | d b/dBx) - (d a/dAx | V | d b/dBz) - case 2: { - auto Dz_a_V_Dx_b = factory.make_child(Dz_a, Dx_b, zero_m); - auto Dx_a_V_Dz_b = factory.make_child(Dx_a, Dz_b, zero_m); - if (is_simple()) { - expr_ = Dz_a_V_Dx_b - Dx_a_V_Dz_b; - nflops_ += 1; - } - } break; - // (a|Wz|b) = (d a/dAx | V | d b/dBy) - (d a/dAy | V | d - // b/dBx) - case 3: { - auto Dx_a_V_Dy_b = factory.make_child(Dx_a, Dy_b, zero_m); - auto Dy_a_V_Dx_b = factory.make_child(Dy_a, Dx_b, zero_m); - if (is_simple()) { - expr_ = Dx_a_V_Dy_b - Dy_a_V_Dx_b; - nflops_ += 1; - } - } break; - default: - throw std::runtime_error("CR_1_σpVσp_1: invalid Pauli index"); - } + // σ·p V σ·p fold: inner child is the electrostatic-potential integral + // (Da | V | Db). Shared structure lives in sigma_p_1body_fold(). + sigma_p_1body_fold( + a, b, oper->descr().quaternion_index(), is_simple(), expr_, nflops_, + [&](const F &Da, const F &Db) { + return factory.make_child(Da, Db, zero_m); + }); } // CR_1_σpVσp_1::CR_1_σpVσp_1 diff --git "a/src/bin/libint/comp_1_\317\203pe\316\274\317\203p_1.h" "b/src/bin/libint/comp_1_\317\203pe\316\274\317\203p_1.h" new file mode 100644 index 000000000..ef5bf52b5 --- /dev/null +++ "b/src/bin/libint/comp_1_\317\203pe\316\274\317\203p_1.h" @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_COMP_1_ΣPEΜΣP_1_H +#define LIBINT_COMP_1_ΣPEΜΣP_1_H + +#include +#include + +namespace libint2 { + +/** + * Computes the integral of \f$ \sigma \cdot \hat{p}\, r_k\, \sigma \cdot + * \hat{p} \f$ over CGShell/CGF by folding the 9 raw \f$ \sigma_a \partial_a r_k + * \sigma_b \partial_b \f$ dyadics per dipole direction \f$ k \f$ to 4 + * Pauli-quaternion components via \f$ \sigma_a \sigma_b = \delta_{ab} + + * i\epsilon_{abc}\sigma_c \f$. 12 outputs total = 3 dipole directions × 4 + * Pauli components, mirroring the σpVσp fold but with the central operator + * being a Cartesian dipole instead of the electrostatic potential V. + * + * @tparam F basis function type. valid choices are CGShell or CGF + */ +template +class CR_1_σpeμσp_1 + : public GenericRecurrenceRelation< + CR_1_σpeμσp_1, F, GenIntegralSet_1_1> { + public: + typedef CR_1_σpeμσp_1 ThisType; + typedef F BasisFunctionType; + typedef σpeμσpOper OperType; + typedef GenIntegralSet_1_1 TargetType; + typedef GenericRecurrenceRelation + ParentType; + friend class GenericRecurrenceRelation; + // trace component folds 3 derivative children; the antisym components fold 2 + static const unsigned int max_nchildren = 3; + + using ParentType::Instance; + + static bool directional() { return false; } + + private: + using ParentType::is_simple; + using ParentType::target_; + using ParentType::RecurrenceRelation::expr_; + using ParentType::RecurrenceRelation::nflops_; + + CR_1_σpeμσp_1(const std::shared_ptr &, unsigned int = 0); + + static std::string descr() { return "CR"; } +}; + +template +CR_1_σpeμσp_1::CR_1_σpeμσp_1(const std::shared_ptr &Tint, + unsigned int) + : ParentType(Tint, 0) { + assert(Tint->num_func_bra(/* particle */ 0) == 1); + assert(Tint->num_func_ket(/* particle */ 0) == 1); + const auto &a = Tint->bra(0, 0); + const auto &b = Tint->ket(0, 0); + const auto &oper = Tint->oper(); + + // express σ·p r_k σ·p in terms of derivative integrals of the dipole + // operator r_k for primitive Gaussians only + if (a.contracted() || b.contracted()) return; + + ChildFactory, + EmptySet>> + factory(this); + + // Build the dipole multipole descriptor for direction k. + const auto k = oper->descr().dipole_dir(); + CartesianMultipole_Descr<3u> mu_k; + mu_k.inc(k, 1); // r_k = (kx,ky,kz) with k_k = 1, others 0 + + // σ·p r_k σ·p fold: inner child is the Cartesian-dipole integral + // (Da | r_k | Db). Shared structure lives in sigma_p_1body_fold(). + sigma_p_1body_fold(a, b, oper->descr().quaternion_index(), is_simple(), expr_, + nflops_, [&](const F &Da, const F &Db) { + return factory.make_child(Da, Db, EmptySet(), mu_k); + }); + +} // CR_1_σpeμσp_1::CR_1_σpeμσp_1 + +}; // namespace libint2 + +#endif // LIBINT_COMP_1_ΣPEΜΣP_1_H diff --git a/src/bin/libint/comp_deriv_gauss_v2.h b/src/bin/libint/comp_deriv_gauss_v2.h new file mode 100644 index 000000000..172540677 --- /dev/null +++ b/src/bin/libint/comp_deriv_gauss_v2.h @@ -0,0 +1,646 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef _libint2_src_bin_libint_compderivgaussv2_h_ +#define _libint2_src_bin_libint_compderivgaussv2_h_ + +#include +#include // CR_DerivGauss_GenericInstantiator +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace libint2 { + +/** Optimized compute relation for (geometric) derivative Gaussian integrals. + * + * Like CR_DerivGauss, this expands derivative Gaussians via + * d/dA G(a) = 2*alpha * G(a+1) - a_i * G(a-1), + * the derivative with respect to the center A of the Gaussian (= -d/dr). + * + * Unlike CR_DerivGauss, this uses the HRR-like code-sharing optimization: + * since differentiation of a Gaussian at position (part, where) depends only + * on that shell's quanta (not spectator shells), we generate code once per + * unique differentiated shell and pass spectator dimensions at the call site. + * + * @tparam IntType integral type + * @tparam part particle index of the function to be differentiated + * @tparam where position of the function to be differentiated (InBra/InKet) + * @tparam trans_inv_part if non-negative, specifies the particle index for + * translational invariance + * @tparam trans_inv_where position for translational invariance + */ +template +class DerivGaussV2 : public RecurrenceRelation { + private: + static constexpr auto trans_inv_oper = + not IntType::OperType::Properties::odep; + static constexpr auto using_trans_inv = + trans_inv_oper && (part == trans_inv_part) && (where == trans_inv_where); + + public: + typedef RecurrenceRelation ParentType; + typedef typename IntType::BasisFunctionType BasisFunctionType; + typedef DerivGaussV2 ThisType; + typedef IntType TargetType; + typedef IntType ChildType; + typedef RecurrenceRelation::ExprType ExprType; + + static const unsigned int max_nchildren_ = + using_trans_inv ? (IntType::num_bf - 1) : 2u; + + static std::shared_ptr Instance( + const std::shared_ptr& Tint, unsigned int dir = 0); + virtual ~DerivGaussV2() {} + + /// always directional + static bool directional() { return true; } + + unsigned int num_children() const override { return nchildren_; } + std::shared_ptr rr_target() const override { + return std::static_pointer_cast(target_); + } + std::shared_ptr rr_child(unsigned int i) const override { + return children_.at(i); + } + bool is_simple() const override { + return TrivialBFSet::result; + } + + std::string spfunction_call( + const std::shared_ptr& context, + const std::shared_ptr& dims) const override; + + private: + DerivGaussV2(const std::shared_ptr& Tint, unsigned int dir); + + unsigned int dir_; + std::shared_ptr target_; + std::vector> children_; + unsigned int nchildren_; + + std::string generate_label() const override; + std::shared_ptr adapt_dims_( + const std::shared_ptr& dims) const override; + bool register_with_rrstack() const; + bool expl_high_dim() const; + bool expl_low_dim() const; + +#if LIBINT_ENABLE_GENERIC_CODE + /// Implementation of RecurrenceRelation::has_generic(): the generic + /// (runtime-dimensioned) GenericGaussDeriv kernel replaces the explicit code + /// of the code-shared function when the differentiated shell is beyond the + /// optimized AM range (the spectator dimensions are runtime arguments of the + /// shared function anyway, so only the differentiated shell matters here) + bool has_generic( + const std::shared_ptr& cparams) const override; + /// Implementation of RecurrenceRelation::generic_header() + std::string generic_header() const override; + /// Implementation of RecurrenceRelation::generic_instance() + std::string generic_instance( + const std::shared_ptr& context, + const std::shared_ptr& args) const override; +#endif + + /// add child, deduplicating + const std::shared_ptr& add_child( + const std::shared_ptr& child) { + for (auto& c : children_) { + if (c == child) return c; + } + children_.push_back(child); + ++nchildren_; + return children_.back(); + } +}; + +// +// Implementation +// + +template +std::shared_ptr< + DerivGaussV2> +DerivGaussV2::Instance( + const std::shared_ptr& Tint, unsigned int dir) { + std::shared_ptr this_ptr(new ThisType(Tint, dir)); + if (this_ptr->num_children() != 0) { + this_ptr->register_with_rrstack(); + return this_ptr; + } + return std::shared_ptr(); +} + +template +DerivGaussV2:: + DerivGaussV2(const std::shared_ptr& Tint, unsigned int dir) + : dir_(dir), target_(Tint), nchildren_(0) { + using namespace libint2::algebra; + using namespace libint2::prefactor; + using namespace libint2::braket; + typedef BasisFunctionType F; + const F& _1 = unit(is_simple() ? dir : 0); + + const typename IntType::AuxQuantaType& aux = Tint->aux(); + const typename IntType::OperType& oper = Tint->oper(); + + children_.reserve(max_nchildren_); + + // the Gaussian must be differentiated in direction dir + { + if (where == InBra && Tint->bra(part, 0).deriv().d(dir) == 0) return; + if (where == InKet && Tint->ket(part, 0).deriv().d(dir) == 0) return; + } + + // if not using translational invariance, can only expand primitives + if (not using_trans_inv) { + if (where == InBra && Tint->bra(part, 0).contracted()) return; + if (where == InKet && Tint->ket(part, 0).contracted()) return; + } + + typedef typename IntType::BraType IBraType; + typedef typename IntType::KetType IKetType; + IBraType* bra = new IBraType(Tint->bra()); + IKetType* ket = new IKetType(Tint->ket()); + + if (not using_trans_inv) { // differentiate + + if (where == InBra) { + F a(bra->member(part, 0)); + + // add a+1 + F ap1(bra->member(part, 0) + _1); + ap1.deriv().dec(dir); + bra->set_member(ap1, part, 0); + auto int_ap1 = add_child(IntType::Instance(*bra, *ket, aux, oper)); + bra->set_member(a, part, 0); + if (is_simple()) { + std::ostringstream oss; + oss << "two_alpha" << part << "_bra"; + expr_ = Scalar(oss.str()) * int_ap1; + nflops_ += 1; + } + + // See if a-1 exists + F am1(bra->member(part, 0) - _1); + if (exists(am1)) { + am1.deriv().dec(dir); + bra->set_member(am1, part, 0); + auto int_am1 = add_child(IntType::Instance(*bra, *ket, aux, oper)); + bra->set_member(a, part, 0); + if (is_simple()) { + expr_ -= Scalar(a[dir]) * int_am1; + nflops_ += 2; + } + } + delete bra; + delete ket; + return; + } + + if (where == InKet) { + F a(ket->member(part, 0)); + + // add a+1 + F ap1(ket->member(part, 0) + _1); + ap1.deriv().dec(dir); + ket->set_member(ap1, part, 0); + auto int_ap1 = add_child(IntType::Instance(*bra, *ket, aux, oper)); + ket->set_member(a, part, 0); + if (is_simple()) { + std::ostringstream oss; + oss << "two_alpha" << part << "_ket"; + expr_ = Scalar(oss.str()) * int_ap1; + nflops_ += 1; + } + + // See if a-1 exists + F am1(ket->member(part, 0) - _1); + if (exists(am1)) { + am1.deriv().dec(dir); + ket->set_member(am1, part, 0); + auto int_am1 = add_child(IntType::Instance(*bra, *ket, aux, oper)); + ket->set_member(a, part, 0); + if (is_simple()) { + expr_ -= Scalar(a[dir]) * int_am1; + nflops_ += 2; + } + } + delete bra; + delete ket; + return; + } + + } else { // use translational invariance + + // remove one deriv quantum from the target function + if (where == InBra) bra->member(part, 0).deriv().dec(dir); + if (where == InKet) ket->member(part, 0).deriv().dec(dir); + + int term_count = 0; + for (int p = 0; p != IntType::num_particles; ++p) { + typedef BasisFunctionType F; + if (p != trans_inv_part || trans_inv_where != InBra) { + F a(bra->member(p, 0)); + if (not a.is_unit()) { + F da(a); + da.deriv().inc(dir); + bra->set_member(da, p, 0); + auto int_da = add_child(IntType::Instance(*bra, *ket, aux, oper)); + bra->set_member(a, p, 0); + if (is_simple()) { + if (term_count == 0) + expr_ = Scalar(-1) * int_da; + else + expr_ -= int_da; + ++term_count; + nflops_ += 1; + } + } + } + if (p != trans_inv_part || trans_inv_where != InKet) { + F a(ket->member(p, 0)); + if (not a.is_unit()) { + F da(a); + da.deriv().inc(dir); + ket->set_member(da, p, 0); + auto int_da = add_child(IntType::Instance(*bra, *ket, aux, oper)); + ket->set_member(a, p, 0); + if (is_simple()) { + if (term_count == 0) + expr_ = Scalar(-1) * int_da; + else + expr_ -= int_da; + ++term_count; + nflops_ += 1; + } + } + } + } + } + + delete bra; + delete ket; +} + +template +bool DerivGaussV2::register_with_rrstack() const { + using std::swap; + + // only register RRs for shell sets (not individual integrals) + if (TrivialBFSet::result) return false; + + // translational invariance path not optimized yet — register as-is + if (using_trans_inv) { + std::shared_ptr rrstack = RRStack::Instance(); + std::shared_ptr this_ptr = + std::const_pointer_cast( + std::static_pointer_cast( + std::enable_shared_from_this::shared_from_this())); + rrstack->find(this_ptr); + return true; + } + + typedef typename IntType::BraType IBraType; + typedef typename IntType::KetType IKetType; + const IBraType& bra = target_->bra(); + const IKetType& ket = target_->ket(); + + // check if all spectator shells already have zero quanta + bool nonzero_quanta = false; + unsigned const int npart = IntType::OperatorType::Properties::np; + for (unsigned int p = 0; p < npart; p++) { + int nfbra = bra.num_members(p); + for (int f = 0; f < nfbra; f++) { + // skip the differentiated position + if (static_cast(p) == part && where == InBra) continue; + if (!bra.member(p, f).zero() || !bra.member(p, f).deriv().zero()) + nonzero_quanta = true; + } + int nfket = ket.num_members(p); + for (int f = 0; f < nfket; f++) { + if (static_cast(p) == part && where == InKet) continue; + if (!ket.member(p, f).zero() || !ket.member(p, f).deriv().zero()) + nonzero_quanta = true; + } + } + + // if all spectators are zero, register this instance directly + if (!nonzero_quanta) { + std::shared_ptr rrstack = RRStack::Instance(); + std::shared_ptr this_ptr = + std::const_pointer_cast( + std::static_pointer_cast( + std::enable_shared_from_this::shared_from_this())); + rrstack->find(this_ptr); + return true; + } + + // Otherwise, zero out all spectator shells and register a dummy + IBraType bra_zero(bra); + IKetType ket_zero(ket); + for (unsigned int p = 0; p < npart; p++) { + int nfbra = bra_zero.num_members(p); + for (int f = 0; f < nfbra; f++) { + if (static_cast(p) == part && where == InBra) continue; + typedef typename IBraType::bfs_type bfs_type; + typedef typename IBraType::bfs_ref bfs_ref; + bfs_ref bfs = bra_zero.member(p, f); + if (!bfs.zero() || !bfs.deriv().zero()) { + bfs_type null_bfs; + swap(bfs, null_bfs); + } + } + int nfket = ket_zero.num_members(p); + for (int f = 0; f < nfket; f++) { + if (static_cast(p) == part && where == InKet) continue; + typedef typename IKetType::bfs_type bfs_type; + typedef typename IKetType::bfs_ref bfs_ref; + bfs_ref bfs = ket_zero.member(p, f); + if (!bfs.zero() || !bfs.deriv().zero()) { + bfs_type null_bfs; + swap(bfs, null_bfs); + } + } + } + + // create a generic integral with a dummy operator + typedef GenOper> + DummyOper; + typedef EmptySet DummyQuanta; + typedef GenIntegralSet + DummyIntegral; + DummyOper dummy_oper; + DummyQuanta dummy_quanta(std::vector(0, 0)); + std::shared_ptr dummy_integral = + DummyIntegral::Instance(bra_zero, ket_zero, dummy_quanta, dummy_oper); + + // construct a DerivGaussV2 over the dummy integral and register it + typedef DerivGaussV2 DummyDerivGaussV2; + std::shared_ptr dummy_rr = + DummyDerivGaussV2::Instance(dummy_integral, dir_); + std::shared_ptr rrstack = RRStack::Instance(); + rrstack->find(dummy_rr); + return true; +} + +template +std::string DerivGaussV2::generate_label() const { + std::ostringstream os; + + // For translational invariance, children depend on ALL shells, so + // the label must include full integral info (no code sharing). + // For direct differentiation, only the differentiated shell matters. + // Either way the direction is part of the identity: a shell carrying + // derivative quanta in two directions is a valid target for two instances + // whose children and coefficients (a[dir]) differ, and the RRStack + // deduplicates generated functions by this label. + if constexpr (using_trans_inv) { + typedef typename TargetType::AuxIndexType mType; + static std::shared_ptr aux0(new mType(0u)); + // N.B. not "CR_DerivGauss...": CR_DerivGauss labels its own instances + // that way, and the RRStack deduplicates by label + os << "DerivGaussV2TI" + << "P" << part << to_string(where) << "D" << dir_ + << genintegralset_label(target_->bra(), target_->ket(), aux0, + target_->oper()); + return os.str(); + } + + os << "DerivGaussV2 P" << part << " " << to_string(where) << " D" << dir_ + << " "; + + // Only encode the differentiated shell — not spectators + if (where == InBra) { + BasisFunctionType sh(target_->bra(part, 0)); + sh.uncontract(); + os << sh.label(); + } else { + BasisFunctionType sh(target_->ket(part, 0)); + sh.uncontract(); + os << sh.label(); + } + + return os.str(); +} + +template +std::string +DerivGaussV2:: + spfunction_call(const std::shared_ptr& context, + const std::shared_ptr& dims) const { + std::ostringstream os; + os << context->label_to_function_name(label()) << "(inteval, " + << context->value_to_pointer(rr_target()->symbol()); + + const unsigned int nc = num_children(); + for (unsigned int c = 0; c < nc; c++) { + os << ", " << context->value_to_pointer(rr_child(c)->symbol()); + } + + // compute hsr and lsr — dimensions of spectator shells + // canonical order: for each particle p, bra then ket + // hsr = product of dims before (part, where) + // lsr = product of dims after (part, where) + unsigned int hsr = 1; + unsigned int lsr = 1; + const unsigned int np = IntType::OperType::Properties::np; + for (int p = 0; p < static_cast(np); p++) { + unsigned int nbra = target_->bra().num_members(p); + assert(nbra == 1); + for (unsigned int i = 0; i < nbra; i++) { + SubIterator* iter = target_->bra().member_subiter(p, i); + if (p < part || (p == part && where == InKet)) hsr *= iter->num_iter(); + // skip p == part && where == InBra (the differentiated shell) + if (p > part) lsr *= iter->num_iter(); + delete iter; + } + unsigned int nket = target_->ket().num_members(p); + assert(nket == 1); + for (unsigned int i = 0; i < nket; i++) { + SubIterator* iter = target_->ket().member_subiter(p, i); + if (p < part) hsr *= iter->num_iter(); + // skip p == part && where == InKet (the differentiated shell) + if (p > part || (p == part && where == InBra)) lsr *= iter->num_iter(); + delete iter; + } + } + + if (expl_high_dim()) os << "," << hsr; + if (expl_low_dim()) os << "," << lsr; + os << ")" << context->end_of_stat() << std::endl; + return os.str(); +} + +template +bool DerivGaussV2::expl_high_dim() const { + // translational invariance: no code sharing, no explicit dims + if (using_trans_inv) return false; + // need explicit high dim unless this is the first position + if (part == 0 && where == InBra) return false; + return true; +} + +template +bool DerivGaussV2::expl_low_dim() const { + // translational invariance: no code sharing, no explicit dims + if (using_trans_inv) return false; + unsigned int np = IntType::OperType::Properties::np; + // need explicit low dim unless this is the last position + if (static_cast(np) - 1 == part && where == InKet) return false; + return true; +} + +template +std::shared_ptr +DerivGaussV2:: + adapt_dims_(const std::shared_ptr& dims) const { + bool high_rank = expl_high_dim(); + bool low_rank = expl_low_dim(); + + std::shared_ptr high_dim, low_dim; + if (high_rank) { + high_dim = + std::shared_ptr(new RTimeEntity("highdim")); + } else { + high_dim = dims->high(); + } + if (low_rank) { + low_dim = + std::shared_ptr(new RTimeEntity("lowdim")); + } else { + low_dim = dims->low(); + } + + std::shared_ptr localdims( + new ImplicitDimensions(high_dim, low_dim, dims->vecdim())); + return localdims; +} + +#if LIBINT_ENABLE_GENERIC_CODE +template +bool DerivGaussV2:: + has_generic(const std::shared_ptr& cparams) const { + // no generic relation for translational invariance (as in CR_DerivGauss) + if (using_trans_inv) return false; + // generic code applies to shell sets only + if (TrivialBFSet::result) return false; + // the function is shared by all shell sets with the same differentiated + // shell, hence base the decision on that shell alone + const BasisFunctionType& sh = + where == InBra ? target_->bra(part, 0) : target_->ket(part, 0); + // the OPT_AM of the current task (the per-class value, e.g. + // LIBINT2_ERI_OPT_AM), not that of the default task + return sh.norm() > + cparams->max_am_opt(LibraryTaskManager::Instance().current().label()); +} + +template +std::string DerivGaussV2::generic_header() const { + return std::string("GenericGaussDeriv.h"); +} + +template +std::string +DerivGaussV2:: + generic_instance(const std::shared_ptr& context, + const std::shared_ptr& args) const { + std::ostringstream oss; + + oss << "using namespace libint2;" << std::endl; + + BasisFunctionType sh(where == InBra ? target_->bra(part, 0) + : target_->ket(part, 0)); + + const unsigned int L = sh.norm(); + const bool vectorize = context->cparams()->max_vector_length() != 1; + oss << "libint2::GenericGaussDeriv<" << L << "," + << (vectorize ? "true" : "false") << ">::compute(inteval"; + + oss << "," << args->symbol(0); // target + const unsigned int nargs = args->n(); + for (unsigned int a = 1; a < nargs; a++) { + oss << "," << args->symbol(a); + } + // L == 0 => second argument not needed + if (nargs == 2) oss << ",0"; + + // the spectator dimensions: runtime arguments of the (shared) function + // where explicit (see adapt_dims_), else 1 by construction (the + // differentiated shell is the first/last function) + const auto localdims = adapt_dims_(ImplicitDimensions::default_dims()); + if (expl_high_dim()) + oss << "," << localdims->high()->id(); + else + oss << ",1"; + if (expl_low_dim()) + oss << "," << localdims->low()->id(); + else + oss << ",1"; + + // direction + oss << "," << dir_; + + // two_alpha prefactor + oss << ",inteval->two_alpha" << part << "_" + << (where == InBra ? "bra" : "ket"); + + oss << ");"; + + CR_DerivGauss_GenericInstantiator::instance().add(L, vectorize); + + return oss.str(); +} +#endif // LIBINT_ENABLE_GENERIC_CODE + +}; // namespace libint2 + +#endif diff --git a/src/bin/libint/context.cc b/src/bin/libint/context.cc index d8eeea32e..58adbe42b 100644 --- a/src/bin/libint/context.cc +++ b/src/bin/libint/context.cc @@ -22,8 +22,12 @@ #include #include +#include #include +#include #include +#include +#include using namespace std; using namespace libint2; @@ -103,6 +107,22 @@ std::string CodeContext::label_to_function_name( return cparams()->api_prefix() + label_to_name(label_to_funcname(label)); } +std::string CodeContext::label_to_filename(const std::string& label) const { + const std::string name = label_to_name(label); + std::string folded_name = name; + std::transform(folded_name.begin(), folded_name.end(), folded_name.begin(), + [](unsigned char c) { return std::tolower(c); }); + // folded name -> distinct names seen so far; index in vector = suffix + static std::map> registry; + auto& names = registry[folded_name]; + auto it = std::find(names.begin(), names.end(), name); + const auto idx = std::distance(names.begin(), it); + if (it == names.end()) names.push_back(name); + // '-' cannot appear in label_to_name() output, so suffixed names cannot + // collide with unsuffixed ones + return idx == 0 ? name : name + "-" + std::to_string(idx); +} + std::string CodeContext::replace_chars(const std::string& S, const std::string& From, const std::string& To) { diff --git a/src/bin/libint/context.h b/src/bin/libint/context.h index db5d22c70..621839d8b 100644 --- a/src/bin/libint/context.h +++ b/src/bin/libint/context.h @@ -63,6 +63,13 @@ class CodeContext { /// label_to_function_name(label) converts label to a valid function name /// with the API prefix prepended std::string label_to_function_name(const std::string& label) const; + /// label_to_filename(label) converts label to a base name (no extension) for + /// the generated source/header files. Same as label_to_name(label) unless + /// that name differs only in letter case from a name already handed out + /// (e.g. contracted vs primitive shells, \c P vs \c p), in which case a + /// suffix is appended so that the files do not clash on case-insensitive + /// filesystems (macOS, Windows). The mapping is process-wide and stable. + std::string label_to_filename(const std::string& label) const; /** declare returns a statement which declares variable named 'name' of type 'type' */ diff --git a/src/bin/libint/dg.cc b/src/bin/libint/dg.cc index 69b5465d4..0955cbb05 100644 --- a/src/bin/libint/dg.cc +++ b/src/bin/libint/dg.cc @@ -501,10 +501,11 @@ void DirectedGraph::apply_to(const std::shared_ptr& vertex, // Optimize out simple recurrence relations void DirectedGraph::optimize_rr_out( - const std::shared_ptr& context) { + const std::shared_ptr& context, + const std::shared_ptr& dims) { replace_rr_with_expr(); remove_trivial_arithmetics(); - handle_trivial_nodes(context); + handle_trivial_nodes(context, dims); remove_disconnected_vertices(); find_subtrees(); } @@ -799,7 +800,8 @@ inline std::string to_vector_symbol(const std::shared_ptr& v) { // refer to another node so that no code is generated for it. // void DirectedGraph::handle_trivial_nodes( - const std::shared_ptr& context) { + const std::shared_ptr& context, + const std::shared_ptr& dims) { typedef vertices::iterator iter; for (iter v = stack_.begin(); v != stack_.end(); ++v) { const ver_ptr& vptr = vertex_ptr(*v); @@ -823,8 +825,6 @@ void DirectedGraph::handle_trivial_nodes( // if (child->symbol_set() == false) { const std::string stack_name("stack"); - const std::shared_ptr& dims = - ImplicitDimensions::default_dims(); std::string low_rank = dims->low_label(); std::string veclen = dims->vecdim_label(); @@ -1107,7 +1107,7 @@ void DirectedGraph::generate_code( for (FuncNameContainer::const_iterator fn = func_names_.begin(); fn != func_names_.end(); fn++) { string function_name = (*fn).first; - def << "#include <" << context->label_to_name(function_name) << ".h>" + def << "#include <" << context->label_to_filename(function_name) << ".h>" << endl; } def << endl; diff --git a/src/bin/libint/dg.h b/src/bin/libint/dg.h index 3bb8fa90e..a6e504b99 100644 --- a/src/bin/libint/dg.h +++ b/src/bin/libint/dg.h @@ -22,6 +22,7 @@ #define _libint2_src_bin_libint_dg_h_ #include +#include #include #include #include @@ -253,7 +254,9 @@ class DirectedGraph : public std::enable_shared_from_this { optimized away. optimize_rr_out() will replace all simple recurrence relations with code representing them. */ - void optimize_rr_out(const std::shared_ptr& context); + void optimize_rr_out(const std::shared_ptr& context, + const std::shared_ptr& dims = + ImplicitDimensions::default_dims()); /** after all apply's have been called, traverse() construct a heuristic order of traversal for the graph. @@ -444,7 +447,9 @@ class DirectedGraph : public std::enable_shared_from_this { to their equivalents (such as (ss|ss) shell quartet can only be connected to (ss|ss) integral) */ - void handle_trivial_nodes(const std::shared_ptr& context); + void handle_trivial_nodes(const std::shared_ptr& context, + const std::shared_ptr& dims = + ImplicitDimensions::default_dims()); /// This functions removes vertices not connected to other vertices void remove_disconnected_vertices(); /** Finds (binary) subtrees. The subtrees correspond to a single-line code (no diff --git a/src/bin/libint/master_ints_list.h b/src/bin/libint/master_ints_list.h index 1aa8c3e64..8ed891e8f 100644 --- a/src/bin/libint/master_ints_list.h +++ b/src/bin/libint/master_ints_list.h @@ -21,12 +21,15 @@ #ifndef _libint2_src_bin_libint_masterintslist_h_ #define _libint2_src_bin_libint_masterintslist_h_ -// need extra-long mpl list +// need extra-long mpl list — split across two sub-lists and joined via +// boost::mpl::joint_view, since boost only ships pre-generated list headers up +// to size 50. #define BOOST_MPL_CFG_NO_PREPROCESSED_HEADERS #define BOOST_MPL_LIMIT_LIST_SIZE 50 #include #include +#include #include #if LIBINT_SUPPORT_ONEBODYINTS #include @@ -47,6 +50,8 @@ typedef GenIntegralSet_1_1 ElecPot_1_1_sh; typedef GenIntegralSet_1_1 ElecPot_1_1_int; typedef GenIntegralSet_1_1 σpVσp_1_1_sh; typedef GenIntegralSet_1_1 σpVσp_1_1_int; +typedef GenIntegralSet_1_1 σpeμσp_1_1_sh; +typedef GenIntegralSet_1_1 σpeμσp_1_1_int; typedef GenIntegralSet_1_1, EmptySet> CMultipole_1_1_sh; typedef GenIntegralSet_1_1, EmptySet> @@ -106,6 +111,18 @@ typedef GenIntegralSet_1_1, CartesianMultipoleOper<1u>, ////////////////////////// typedef GenIntegralSet_11_11 TwoPRep_11_11_sq; typedef GenIntegralSet_11_11 TwoPRep_11_11_int; +typedef GenIntegralSet_11_11 + Coulombσpσp_11_11_sq; +typedef GenIntegralSet_11_11 Coulombσpσp_11_11_int; +typedef GenIntegralSet_11_11 + σpσpCoulombσpσp_11_11_sq; +typedef GenIntegralSet_11_11 + σpσpCoulombσpσp_11_11_int; +typedef GenIntegralSet_11_11 + σpCoulombσp_11_11_sq; +typedef GenIntegralSet_11_11 σpCoulombσp_11_11_int; +typedef GenIntegralSet_11_11 Coulombσp_11_11_sq; +typedef GenIntegralSet_11_11 Coulombσp_11_11_int; typedef GenIntegralSet_11_11 R12kG12_11_11_sq; typedef GenIntegralSet_11_11 R12kG12_11_11_int; typedef GenIntegralSet_11_11 @@ -138,17 +155,25 @@ typedef boost::mpl::list< Overlap_1_1_sh_y, Overlap_1_1_int_y, Overlap_1_1_sh_z, Overlap_1_1_int_z, Kinetic_1_1_sh, Kinetic_1_1_int, Kinetic_1_1_sh_x, Kinetic_1_1_int_x, Kinetic_1_1_sh_y, Kinetic_1_1_int_y, Kinetic_1_1_sh_z, Kinetic_1_1_int_z, - ElecPot_1_1_sh, ElecPot_1_1_int, σpVσp_1_1_sh, σpVσp_1_1_int, - CMultipole_1_1_sh, CMultipole_1_1_int, CMultipole_1_1_sh_x, + ElecPot_1_1_sh, ElecPot_1_1_int, σpVσp_1_1_sh, σpVσp_1_1_int, σpeμσp_1_1_sh, + σpeμσp_1_1_int, CMultipole_1_1_sh, CMultipole_1_1_int, CMultipole_1_1_sh_x, CMultipole_1_1_sh_y, CMultipole_1_1_sh_z, CMultipole_1_1_int_x, CMultipole_1_1_int_y, CMultipole_1_1_int_z, SMultipole_1_1_sh, - SMultipole_1_1_int, #endif - TwoPRep_11_11_sq, TwoPRep_11_11_int, R12kG12_11_11_sq, R12kG12_11_11_int, - R12kR12lG12_11_11_sq, R12kR12lG12_11_11_int, TiG12_11_11_sq, - TiG12_11_11_int, G12TiG12_11_11_sq, G12TiG12_11_11_int, + SMultipole_1_1_int> + MasterIntegralTypeList_1body_part; +typedef boost::mpl::list< + TwoPRep_11_11_sq, TwoPRep_11_11_int, Coulombσpσp_11_11_sq, + Coulombσpσp_11_11_int, σpσpCoulombσpσp_11_11_sq, σpσpCoulombσpσp_11_11_int, + σpCoulombσp_11_11_sq, σpCoulombσp_11_11_int, Coulombσp_11_11_sq, + Coulombσp_11_11_int, R12kG12_11_11_sq, + R12kG12_11_11_int, R12kR12lG12_11_11_sq, R12kR12lG12_11_11_int, + TiG12_11_11_sq, TiG12_11_11_int, G12TiG12_11_11_sq, G12TiG12_11_11_int, DivG12prime_xTx_11_11_sq, DivG12prime_xTx_11_11_int, DummySymmIntegral_11_11_sq, DummySymmIntegral_11_11_int> + MasterIntegralTypeList_2body_part; +typedef boost::mpl::joint_view MasterIntegralTypeList; }; // namespace libint2 diff --git a/src/bin/libint/master_rrs_list.h b/src/bin/libint/master_rrs_list.h index f3ec4e2d2..be345f285 100644 --- a/src/bin/libint/master_rrs_list.h +++ b/src/bin/libint/master_rrs_list.h @@ -21,12 +21,18 @@ #ifndef _libint2_src_bin_libint_masterrrslist_h_ #define _libint2_src_bin_libint_masterrrslist_h_ +#include +#include #include #include #include #include +#include +#include #include +#include #include +#include #include #include #include @@ -178,6 +184,9 @@ typedef VRR_1_ElecPot_1 VRR_b_1_ElecPot_1_int; typedef CR_1_σpVσp_1 CR_1_σpVσp_1_sh; typedef CR_1_σpVσp_1 CR_1_σpVσp_1_int; +typedef CR_1_σpeμσp_1 CR_1_σpeμσp_1_sh; +typedef CR_1_σpeμσp_1 CR_1_σpeμσp_1_int; + // TODO investigate whether need to stay away from HRR for now to be sure that // multipoles are computed as precisely as possible typedef HRR @@ -241,31 +250,62 @@ typedef ITR_11_TwoPRep_11 typedef ITR_11_TwoPRep_11 ITR_d_11_TwoPRep_11_int; -typedef CR_DerivGauss - Deriv_a_11_TwoPRep_11_sh; -typedef CR_DerivGauss - Deriv_b_11_TwoPRep_11_sh; -typedef CR_DerivGauss - Deriv_c_11_TwoPRep_11_sh; -typedef CR_DerivGauss - Deriv_d_11_TwoPRep_11_sh; -typedef CR_DerivGauss - Deriv_a_11_TwoPRep_11_int; -typedef CR_DerivGauss - Deriv_b_11_TwoPRep_11_int; -typedef CR_DerivGauss - Deriv_c_11_TwoPRep_11_int; -typedef CR_DerivGauss - Deriv_d_11_TwoPRep_11_int; +// DerivGaussV2 for TwoPRep (shell sets) +typedef DerivGaussV2 + DerivV2_a_11_TwoPRep_11_sh; +typedef DerivGaussV2 + DerivV2_b_11_TwoPRep_11_sh; +typedef DerivGaussV2 + DerivV2_c_11_TwoPRep_11_sh; +typedef DerivGaussV2 + DerivV2_d_11_TwoPRep_11_sh; +// DerivGaussV2 for TwoPRep (individual integrals) +typedef DerivGaussV2 + DerivV2_a_11_TwoPRep_11_int; +typedef DerivGaussV2 + DerivV2_b_11_TwoPRep_11_int; +typedef DerivGaussV2 + DerivV2_c_11_TwoPRep_11_int; +typedef DerivGaussV2 + DerivV2_d_11_TwoPRep_11_int; + +// DerivGaussV2 for DummySymmIntegral (used by register_with_rrstack) +typedef DerivGaussV2 + DerivV2_a_11_Dummy_11_sh; +typedef DerivGaussV2 + DerivV2_b_11_Dummy_11_sh; +typedef DerivGaussV2 + DerivV2_c_11_Dummy_11_sh; +typedef DerivGaussV2 + DerivV2_d_11_Dummy_11_sh; +typedef DerivGaussV2 + DerivV2_a_11_Dummy_11_int; +typedef DerivGaussV2 + DerivV2_b_11_Dummy_11_int; +typedef DerivGaussV2 + DerivV2_c_11_Dummy_11_int; +typedef DerivGaussV2 + DerivV2_d_11_Dummy_11_int; + +typedef CR_11_Coulombσpσp_11 CR_11_Coulombσpσp_11_sh; +typedef CR_11_Coulombσpσp_11 CR_11_Coulombσpσp_11_int; + +typedef CR_11_σpσpCoulombσpσp_11 CR_11_σpσpCoulombσpσp_11_sh; +typedef CR_11_σpσpCoulombσpσp_11 CR_11_σpσpCoulombσpσp_11_int; + +typedef CR_11_σpCoulombσp_11 CR_11_σpCoulombσp_11_sh; +typedef CR_11_σpCoulombσp_11 CR_11_σpCoulombσp_11_int; +typedef CR_11_Coulombσp_11 CR_11_Coulombσp_11_sh; +typedef CR_11_Coulombσp_11 CR_11_Coulombσp_11_int; }; // namespace libint2 #endif // header guard diff --git a/src/bin/libint/oper.h b/src/bin/libint/oper.h index 6afbff587..aec9a8867 100644 --- a/src/bin/libint/oper.h +++ b/src/bin/libint/oper.h @@ -238,6 +238,19 @@ typedef OperatorProperties<2, false, PermutationalSymmetry::symm> typedef OperatorProperties<2, false, PermutationalSymmetry::nonsymm> NonmultiplicativeNonsymm2Body_Props; +/// which swaps of the two functions within the bra and within the ket of a +/// 2-body integral the engine may perform to canonicalize a shell set; the +/// generated kernels cover exactly the shell sets that are canonical with +/// respect to these swaps. Must match PairSwap in the engine (engine.h). +enum class PairSwap { + /// bra and ket are sorted independently of each other + independent, + /// bra and ket are swapped together, or not at all (the ket is sorted) + coupled, + /// neither bra nor ket may be swapped + none +}; + /** GenMultSymmOper is a generic multiplicative symmetric N-body operator */ template @@ -289,23 +302,22 @@ BOOST_PP_LIST_FOR_EACH(BOOST_PP_DECLARE_HERMITIAN_ONEBODY_DESCRIPTOR, struct σpVσp_Descr : public Contractable<σpVσp_Descr> { typedef MultiplicativeODep1Body_Props Properties; - σpVσp_Descr() : quaternionic_index_(0) {} - σpVσp_Descr(int quaternionic_index) - : quaternionic_index_(quaternionic_index) { - assert(quaternionic_index <= 3); + σpVσp_Descr() : quaternion_index_(0) {} + σpVσp_Descr(int quaternion_index) : quaternion_index_(quaternion_index) { + assert(quaternion_index <= 3); } static const unsigned int max_key = 4; - unsigned int key() const { return quaternionic_index(); } + unsigned int key() const { return quaternion_index(); } std::string description() const { std::string descr("opVop["); - if (quaternionic_index() == 0) + if (quaternion_index() == 0) descr += "0"; - else if (quaternionic_index() == 1) + else if (quaternion_index() == 1) descr += "X"; - else if (quaternionic_index() == 2) + else if (quaternion_index() == 2) descr += "Y"; - else if (quaternionic_index() == 3) + else if (quaternion_index() == 3) descr += "Z"; else abort(); @@ -315,13 +327,52 @@ struct σpVσp_Descr : public Contractable<σpVσp_Descr> { int psymm(int i, int j) const { abort(); } int hermitian(int i) const { return +1; } - int quaternionic_index() const { return quaternionic_index_; } + int quaternion_index() const { return quaternion_index_; } private: - const int quaternionic_index_ = -1; + const int quaternion_index_ = -1; }; typedef GenOper<σpVσp_Descr> σpVσpOper; +/** opemuop: (μ σ·p | r | σ·p ν), one-body σ·p-on-both-sides analog of dipole. + * σ_a σ_b = δ_ab + iε_abc σ_c folds the 9 raw σ_a∂_a r_k σ_b∂_b dyadics per + * dipole direction k down to 4 Pauli-quaternion components (trace + 3 + * antisym), mirroring σpVσp's fold of σ·p V σ·p. 12 outputs total = 3 dipole + * directions × 4 Pauli components, indexed composite_index = 4·k + q. + */ +struct σpeμσp_Descr : public Contractable<σpeμσp_Descr> { + typedef MultiplicativeODep1Body_Props Properties; + + σpeμσp_Descr() : composite_index_(0) {} + σpeμσp_Descr(int composite_index) : composite_index_(composite_index) { + assert(composite_index >= 0 && composite_index < 12); + } + + static const unsigned int max_key = 12; + unsigned int key() const { return composite_index(); } + std::string description() const { + static const char* dipole_lbl[] = {"x", "y", "z"}; + static const char* pauli_lbl[] = {"0", "X", "Y", "Z"}; + const auto ci = composite_index(); + if (ci < 0 || ci >= 12) abort(); + return std::string("opemuop[") + dipole_lbl[ci / 4] + "," + + pauli_lbl[ci % 4] + "]"; + } + std::string label() const { return description(); } + int psymm(int i, int j) const { abort(); } + int hermitian(int i) const { return +1; } + + int composite_index() const { return composite_index_; } + /// dipole direction ∈ {0=x, 1=y, 2=z} + int dipole_dir() const { return composite_index_ / 4; } + /// Pauli quaternion ∈ {0=trace, 1=σ_x, 2=σ_y, 3=σ_z} + int quaternion_index() const { return composite_index_ % 4; } + + private: + const int composite_index_ = -1; +}; +typedef GenOper<σpeμσp_Descr> σpeμσpOper; + /// cartesian multipole operator in \c NDIM dimensions /// \f$ \hat{O}(\vec{k}) \equiv \vec{r}^{\cdot \vec{k}} = r_1^{k_1} r_2^{k_2} /// \cdots \f$ \internal OriginDerivative is used to store cartesian @@ -392,6 +443,11 @@ using SphericalMultipoleOper = GenOper; struct TwoPRep_Descr : public Contractable { typedef MultiplicativeSymm2Body_Props Properties; static const unsigned int max_key = 1; + /// whether the engine may swap particles 1 and 2 (bra and ket in chemists' + /// notation) to canonicalize a shell set + static constexpr bool p1p2_swappable = true; + /// which swaps within bra and within ket the engine may perform + static constexpr PairSwap pair_swap = PairSwap::independent; unsigned int key() const { return 0; } std::string description() const { return "1/r_{12}"; } std::string label() const { return "TwoPRep"; } @@ -400,6 +456,233 @@ struct TwoPRep_Descr : public Contractable { }; typedef GenOper TwoPRep; +/** Coulombσpσp: (a b | 1/r_{12} | σ·p c σ·p d), the Coulomb operator with + * σ·p acting on both functions of particle 2. The Dirac identity + * σ_i σ_j = δ_ij + i ε_ijk σ_k folds the Pauli matrices into 4 quaternion + * components: 0: ∇_c·∇_d, 1-3: (∇_c × ∇_d)_{x,y,z}. + */ +struct Coulombσpσp_Descr : public Contractable { + // σ·p acts on particle 2 only, hence no particle 1<->2 symmetry + typedef MultiplicativeNonsymm2Body_Props Properties; + static constexpr bool p1p2_swappable = false; + /// swapping c and d changes the sign of components 1-3 (handled by the + /// engine) + static constexpr PairSwap pair_swap = PairSwap::independent; + + Coulombσpσp_Descr() : quaternion_index_(0) {} + Coulombσpσp_Descr(int quaternion_index) + : quaternion_index_(quaternion_index) { + assert(quaternion_index <= 3); + } + + static const unsigned int max_key = 4; + unsigned int key() const { return quaternion_index(); } + std::string description() const { + std::string descr("coulomb_opop["); + if (quaternion_index() == 0) + descr += "0"; + else if (quaternion_index() == 1) + descr += "X"; + else if (quaternion_index() == 2) + descr += "Y"; + else if (quaternion_index() == 3) + descr += "Z"; + else + abort(); + return descr + "]"; + } + std::string label() const { return description(); } + int psymm(int i, int j) const { abort(); } + /// N.B. the Multiplicative*2Body_Props above make Oper::hermitian() return + /// +1, i.e. HRR would treat this operator as commuting with the transfer of + /// quanta between the centers, which it does not (σ·p acts on specific + /// functions). Harmless as long as only the composite relation is in this + /// operator's strategy: do not add HRR to it. + int hermitian(int i) const { return +1; } + + int quaternion_index() const { return quaternion_index_; } + + private: + const int quaternion_index_ = -1; +}; +typedef GenOper CoulombσpσpOper; + +/** Coulombσp is the 3-center single-σ·p operator: + * (P | 1/r_{12} | μ, σ·p ν), with the fitting function P a spectator and a + * single σ·p acting on the 2nd ket AO function ν. Unlike Coulombσpσp (two σ·p + * on the ket pair, folded via the Dirac identity into 4 quaternion + * components), a single σ·p produces no σ·σ folding: the result is the bare + * Cartesian gradient of ν, i.e. the SO(3) vector irrep with 3 components + * (x=0, y=1, z=2), each exactly one first-derivative ERI child: the + * derivative of ν with respect to its center D. Since + * p_k = -i ∂/∂r_k = +i ∂/∂D_k, the caller multiplies component k by +i and + * contracts with σ_k (see the rkb_operators page in engine.h). + */ +struct Coulombσp_Descr : public Contractable { + // σ·p acts on particle 2 only, hence no particle 1<->2 symmetry + typedef MultiplicativeNonsymm2Body_Props Properties; + static constexpr bool p1p2_swappable = false; + /// σ·p acts on d only, hence c and d cannot be swapped: kernels are + /// generated for every ordering of the paired centers + static constexpr PairSwap pair_swap = PairSwap::none; + + Coulombσp_Descr() : cartesian_index_(0) {} + Coulombσp_Descr(int cartesian_index) : cartesian_index_(cartesian_index) { + assert(cartesian_index >= 0 && cartesian_index <= 2); + } + + static const unsigned int max_key = 3; + unsigned int key() const { return cartesian_index(); } + std::string description() const { + std::string descr("coulomb_op["); + if (cartesian_index() == 0) + descr += "X"; + else if (cartesian_index() == 1) + descr += "Y"; + else if (cartesian_index() == 2) + descr += "Z"; + else + abort(); + return descr + "]"; + } + std::string label() const { return description(); } + int psymm(int i, int j) const { abort(); } + /// N.B. the Multiplicative*2Body_Props above make Oper::hermitian() return + /// +1, i.e. HRR would treat this operator as commuting with the transfer of + /// quanta between the centers, which it does not (σ·p acts on specific + /// functions). Harmless as long as only the composite relation is in this + /// operator's strategy: do not add HRR to it. + int hermitian(int i) const { return +1; } + + int cartesian_index() const { return cartesian_index_; } + + private: + const int cartesian_index_ = -1; +}; +typedef GenOper CoulombσpOper; + +struct σpσpCoulombσpσp_Descr : public Contractable<σpσpCoulombσpσp_Descr> { + // particle 1<->2 symmetric up to a remap of the components + typedef MultiplicativeSymm2Body_Props Properties; + static constexpr bool p1p2_swappable = true; + /// (ab|cd) = (ba|dc) up to the sign of some components, but + /// (ab|cd) != (ba|cd): the pairs can only be swapped together + static constexpr PairSwap pair_swap = PairSwap::coupled; + + σpσpCoulombσpσp_Descr() : component_index_(0) {} + σpσpCoulombσpσp_Descr(int component_index) + : component_index_(component_index) { + assert(component_index >= 0 && component_index <= 15); + } + + /// 16 components from tensor product of two independent spin spaces: + /// index = 4 * bra_spin_index + ket_spin_index + /// where spin indices are: 0=S (scalar), 1=X, 2=Y, 3=Z (cross product) + static const unsigned int max_key = 16; + unsigned int key() const { return component_index(); } + std::string description() const { + // clang-format off + // Option A (tensor product order): index = 4 * bra_spin + ket_spin + static const char* labels[] = { + "SS", "SX", "SY", "SZ", + "XS", "XX", "XY", "XZ", + "YS", "YX", "YY", "YZ", + "ZS", "ZX", "ZY", "ZZ" + }; + // clang-format on + const auto qi = component_index(); + if (qi > 15) abort(); + return std::string("opop_coulomb_opop[") + labels[qi] + "]"; + } + std::string label() const { return description(); } + int psymm(int i, int j) const { abort(); } + /// N.B. the Multiplicative*2Body_Props above make Oper::hermitian() return + /// +1, i.e. HRR would treat this operator as commuting with the transfer of + /// quanta between the centers, which it does not (σ·p acts on specific + /// functions). Harmless as long as only the composite relation is in this + /// operator's strategy: do not add HRR to it. + int hermitian(int i) const { return +1; } + + int component_index() const { return component_index_; } + + private: + const int component_index_ = -1; +}; +typedef GenOper<σpσpCoulombσpσp_Descr> σpσpCoulombσpσpOper; + +/** σpCoulombσp: (μ σ·p ν | 1/r_{12} | κ σ·p λ). + * Gaunt LS "bilinear" operator with one σ·p on each side. + * Outputs the SO(3) irreducible decomposition of the 3×3 gradient-gradient + * tensor T_{ab} = ∂_a ∂_b (μν|κλ): 1 scalar trace + 3 antisymmetric + * (curl-curl) + 5 symmetric-traceless = 9 components. Same storage as the + * raw dyadic, but indexed by physics-meaningful irreps so consumers do not + * need to hand-build trace/antisym/sym-TL combinations at every contraction + * site. + */ +struct σpCoulombσp_Descr : public Contractable<σpCoulombσp_Descr> { + // particle 1<->2 symmetric up to the sign of the antisymmetric components + typedef MultiplicativeSymm2Body_Props Properties; + static constexpr bool p1p2_swappable = true; + /// σ·p acts on one specific function of each pair, hence no swaps within + /// bra or ket + static constexpr PairSwap pair_swap = PairSwap::none; + + /// SO(3) irreducible components of the rank-2 Cartesian tensor T_{ab}. + enum Component : int { + Scalar = 0, ///< T_xx + T_yy + T_zz (trace, ∇·∇) + AntisymX = 1, ///< T_yz − T_zy = (∇×∇)_x + AntisymY = 2, ///< T_zx − T_xz = (∇×∇)_y + AntisymZ = 3, ///< T_xy − T_yx = (∇×∇)_z + SymTLDiagA = 4, ///< T_xx − T_yy + SymTLDiagB = 5, ///< 2·T_zz − T_xx − T_yy + SymTLOffXY = 6, ///< T_xy + T_yx + SymTLOffXZ = 7, ///< T_xz + T_zx + SymTLOffYZ = 8, ///< T_yz + T_zy + }; + + σpCoulombσp_Descr() : component_index_(0) {} + σpCoulombσp_Descr(int component_index) : component_index_(component_index) { + assert(component_index >= 0 && component_index <= 8); + } + + static const unsigned int max_key = 9; + unsigned int key() const { return component_index(); } + std::string description() const { + static const char* labels[] = { + "scalar", "antisym_x", "antisym_y", + "antisym_z", "symtl_diag_a", "symtl_diag_b", + "symtl_off_xy", "symtl_off_xz", "symtl_off_yz", + }; + const auto ci = component_index(); + if (ci > 8) abort(); + return std::string("op_coulomb_op[") + labels[ci] + "]"; + } + std::string label() const { return description(); } + int psymm(int i, int j) const { abort(); } + /// N.B. the Multiplicative*2Body_Props above make Oper::hermitian() return + /// +1, i.e. HRR would treat this operator as commuting with the transfer of + /// quanta between the centers, which it does not (σ·p acts on specific + /// functions). Harmless as long as only the composite relation is in this + /// operator's strategy: do not add HRR to it. + int hermitian(int i) const { return +1; } + + int component_index() const { return component_index_; } + + bool is_scalar() const { return component_index_ == Scalar; } + bool is_antisym() const { + return component_index_ >= AntisymX && component_index_ <= AntisymZ; + } + bool is_sym_tl() const { + return component_index_ >= SymTLDiagA && component_index_ <= SymTLOffYZ; + } + /// for antisym components: 0=x, 1=y, 2=z (only valid if is_antisym()) + int antisym_cart() const { return component_index_ - AntisymX; } + + private: + const int component_index_ = -1; +}; +typedef GenOper<σpCoulombσp_Descr> σpCoulombσpOper; + /** GTG_1d is the two-body 1-dimensional Gaussian geminal */ struct GTG_1d_Descr : public Contractable { diff --git a/src/bin/libint/rkb_fold_1body.h b/src/bin/libint/rkb_fold_1body.h new file mode 100644 index 000000000..85ab05e02 --- /dev/null +++ b/src/bin/libint/rkb_fold_1body.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_RKB_FOLD_1BODY_H +#define LIBINT_RKB_FOLD_1BODY_H + +#include + +#include + +namespace libint2 { + +/** + * Shared one-body \f$ \sigma\cdot\hat{p}\, \hat{O}\, \sigma\cdot\hat{p} \f$ + * Pauli-quaternion fold for primitive Gaussians. Both \c CR_1_σpVσp_1 (inner + * operator = electrostatic potential V) and \c CR_1_σpeμσp_1 (inner operator = + * Cartesian dipole \f$ r_k \f$) reduce to exactly this combination of the 6 + * first-derivative basis functions; only the inner child integral differs, so + * the caller supplies it via @p make_child. + * + * Using \f$ \sigma_a \sigma_b = \delta_{ab} + i\epsilon_{abc}\sigma_c \f$, the + * quaternion index @p q selects: + * - q=0 (trace δ_ab): Σ_a (∂_a a | O | ∂_a b) + * - q=1 (σ_x antisym): (∂_y a | O | ∂_z b) − (∂_z a | O | ∂_y b) + * - q=2 (σ_y antisym): (∂_z a | O | ∂_x b) − (∂_x a | O | ∂_z b) + * - q=3 (σ_z antisym): (∂_x a | O | ∂_y b) − (∂_y a | O | ∂_x b) + * + * Children are always created (so they register on the graph); @p expr is set + * only when @p is_simple. + * + * @tparam F basis function type (CGShell or CGF) + * @tparam ExprPtr type of the RR's expr_ member (shared_ptr) + * @tparam MakeChild callable (const F& Da, const F& Db) -> child vertex, + * wrapping the operator-specific ChildFactory::make_child + * @param a,b bra/ket basis functions of the target + * @param q Pauli quaternion index (0..3) + * @param is_simple value of the RR's is_simple() + * @param expr the RR's expr_ (assigned when is_simple) + * @param nflops the RR's nflops_ (incremented by the fold's adds) + * @param make_child builds the inner child integral (Da | O | Db) + */ +template +void sigma_p_1body_fold(const F &a, const F &b, unsigned int q, bool is_simple, + ExprPtr &expr, unsigned int &nflops, + MakeChild &&make_child) { + using namespace libint2::algebra; + using libint2::algebra::operator*; + + constexpr int x = 0; + constexpr int y = 1; + constexpr int z = 2; + + F Dx_a{a}; + Dx_a.deriv().inc(x); + F Dx_b{b}; + Dx_b.deriv().inc(x); + F Dy_a{a}; + Dy_a.deriv().inc(y); + F Dy_b{b}; + Dy_b.deriv().inc(y); + F Dz_a{a}; + Dz_a.deriv().inc(z); + F Dz_b{b}; + Dz_b.deriv().inc(z); + + switch (q) { + case 0: { + auto Dx_a_O_Dx_b = make_child(Dx_a, Dx_b); + auto Dy_a_O_Dy_b = make_child(Dy_a, Dy_b); + auto Dz_a_O_Dz_b = make_child(Dz_a, Dz_b); + if (is_simple) { + expr = Dx_a_O_Dx_b + Dy_a_O_Dy_b + Dz_a_O_Dz_b; + nflops += 2; + } + } break; + case 1: { + auto Dy_a_O_Dz_b = make_child(Dy_a, Dz_b); + auto Dz_a_O_Dy_b = make_child(Dz_a, Dy_b); + if (is_simple) { + expr = Dy_a_O_Dz_b - Dz_a_O_Dy_b; + nflops += 1; + } + } break; + case 2: { + auto Dz_a_O_Dx_b = make_child(Dz_a, Dx_b); + auto Dx_a_O_Dz_b = make_child(Dx_a, Dz_b); + if (is_simple) { + expr = Dz_a_O_Dx_b - Dx_a_O_Dz_b; + nflops += 1; + } + } break; + case 3: { + auto Dx_a_O_Dy_b = make_child(Dx_a, Dy_b); + auto Dy_a_O_Dx_b = make_child(Dy_a, Dx_b); + if (is_simple) { + expr = Dx_a_O_Dy_b - Dy_a_O_Dx_b; + nflops += 1; + } + } break; + default: + throw std::runtime_error("sigma_p_1body_fold: invalid quaternionic index"); + } +} + +} // namespace libint2 + +#endif // LIBINT_RKB_FOLD_1BODY_H diff --git a/src/bin/libint/rkb_fold_codegen.h b/src/bin/libint/rkb_fold_codegen.h new file mode 100644 index 000000000..43360a91a --- /dev/null +++ b/src/bin/libint/rkb_fold_codegen.h @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2004-2026 Edward F. Valeev + * + * This file is part of Libint compiler. + * + * Libint compiler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Libint compiler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Libint compiler. If not, see . + * + */ + +#ifndef LIBINT_RKB_FOLD_CODEGEN_H +#define LIBINT_RKB_FOLD_CODEGEN_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace libint2 { + +// Shared machinery of the σ·p "element-wise fold" recurrence relations: +// CR_11_Coulombσp_11, CR_11_Coulombσpσp_11, CR_11_σpCoulombσp_11 and +// CR_11_σpσpCoulombσpσp_11. Each decomposes its target into a fixed linear +// combination of first-derivative TwoPRep children, evaluated element-wise over +// the (spectator) shell dimensions. Every such relation is defined by one table +// of terms per component (RKBFoldTerms): the constructor creates the children +// from it (rkb_fold_make_children) and generate_code() emits the element-wise +// expression from it (rkb_fold_generate_code_from_terms), so the order of the +// children and the signs in the generated code cannot disagree. The call-site +// emission, dimension adaptation and per-function scaffold are identical across +// the four relations and live here as well. + +/// One term of an element-wise σ·p fold: @c coef times the TwoPRep integral +/// over copies of the functions (a, b, c, d) = (bra1, ket1, bra2, ket2) of the +/// target that carry one extra derivative quantum in Cartesian direction +/// @c deriv[0..3] (-1: no derivative on that function). The derivatives are +/// with respect to the centers of the functions (libint's convention), see +/// the rkb_operators page in engine.h for the resulting prefactors. +struct RKBFoldTerm { + int coef; + std::array deriv; +}; +typedef std::vector RKBFoldTerms; + +/// One term of a Pauli component of the product (σ·∇)(σ·∇') of two gradients: +/// @c coef times ∂_i ∂'_j +struct RKBFoldPairTerm { + int coef; + int i; + int j; +}; + +/// @return the terms of Pauli component @p q of +/// (σ·∇)(σ·∇') = ∇·∇' + i σ·(∇ × ∇'): q = 0 is the dot product (δ_ij), +/// q = 1..3 the x, y, z component of the cross product (the Levi-Civita symbol +/// ε_{qij}). The imaginary unit and the Pauli matrix of the cross-product +/// components are left to the user of the library (see the rkb_operators page +/// in engine.h). +inline const std::vector& rkb_fold_pair_terms(int q) { + static const std::vector terms[4] = { + {{1, 0, 0}, {1, 1, 1}, {1, 2, 2}}, // ∇·∇' = ∂_x∂'_x + ∂_y∂'_y + ∂_z∂'_z + {{1, 1, 2}, {-1, 2, 1}}, // (∇×∇')_x = ∂_y∂'_z - ∂_z∂'_y + {{1, 2, 0}, {-1, 0, 2}}, // (∇×∇')_y = ∂_z∂'_x - ∂_x∂'_z + {{1, 0, 1}, {-1, 1, 0}} // (∇×∇')_z = ∂_x∂'_y - ∂_y∂'_x + }; + if (q < 0 || q > 3) + throw std::invalid_argument( + "rkb_fold_pair_terms: invalid Pauli component index " + + std::to_string(q)); + return terms[q]; +} + +/// Constructor body of a fold relation: creates one TwoPRep child per term of +/// @p terms, in the order of @p terms (the order generate_code() relies on), +/// and, if @p simple, the symbolic expression Σ_t coef_t · child_t in @p expr +/// and its flop count in @p nflops. The children are created even when not +/// @p simple so that they register on the graph. +/// @tparam Factory ChildFactory> +/// @tparam ExprPtr type of RecurrenceRelation::expr_ +/// @param a,b,c,d the functions of the target: bra1, ket1, bra2, ket2 +template +void rkb_fold_make_children(Factory& factory, const F& a, const F& b, + const F& c, const F& d, const RKBFoldTerms& terms, + bool simple, ExprPtr& expr, unsigned int& nflops) { + using namespace libint2::algebra; + using libint2::algebra::operator*; + using libint2::prefactor::Scalar; + + const typename Factory::AuxIndexType zero_m(0u); + std::shared_ptr first_child; + for (std::size_t t = 0; t != terms.size(); ++t) { + const RKBFoldTerm& term = terms[t]; + F fa{a}; + F fb{b}; + F fc{c}; + F fd{d}; + if (term.deriv[0] >= 0) fa.deriv().inc(term.deriv[0]); + if (term.deriv[1] >= 0) fb.deriv().inc(term.deriv[1]); + if (term.deriv[2] >= 0) fc.deriv().inc(term.deriv[2]); + if (term.deriv[3] >= 0) fd.deriv().inc(term.deriv[3]); + const std::shared_ptr child = + factory.make_child(fa, fb, fc, fd, zero_m); + if (!simple) continue; + + if (t == 0) { + first_child = child; + // a bare child is not an expression: a leading +child is combined with + // the second term below, any other leading term is Scalar(coef) * child + if (term.coef != 1 || terms.size() == 1) expr = Scalar(term.coef) * child; + } else { + const std::shared_ptr lhs = + (t == 1 && terms[0].coef == 1) ? first_child + : std::shared_ptr(expr); + if (term.coef == 1) + expr = lhs + child; + else if (term.coef == -1) + expr = lhs - child; + else + expr = lhs + Scalar(term.coef) * child; + nflops += 1; + } + if (std::abs(term.coef) != 1) nflops += 1; + } +} + +/// @return the right-hand side of the element-wise fold of @p terms with child +/// t of the relation named src, e.g. "src0[hsi] - src1[hsi]" or +/// "2.0*src0[hsi] - src1[hsi] - src2[hsi]"; @p nflops receives its flop count +inline std::string rkb_fold_rhs(const RKBFoldTerms& terms, + unsigned int& nflops) { + std::ostringstream os; + nflops = 0; + for (std::size_t t = 0; t != terms.size(); ++t) { + const int coef = terms[t].coef; + if (t == 0) { + if (coef < 0) os << "-"; + } else { + os << (coef < 0 ? " - " : " + "); + ++nflops; + } + if (std::abs(coef) != 1) { + os << std::abs(coef) << ".0*"; + ++nflops; + } + os << "src" << t << "[hsi]"; + } + return os.str(); +} + +/// adapt_dims_ body: replace the high dimension with the runtime "highdim" +/// symbol (the fold loops over the high/spectator dimension only). +inline std::shared_ptr rkb_fold_adapt_dims( + const std::shared_ptr& dims) { + auto high_dim = std::make_shared>("highdim"); + return std::make_shared(high_dim, dims->low(), + dims->vecdim()); +} + +/// spfunction_call body: emit the call to the shared element-wise function, +/// passing target, the children, and total_dim (= product of all four spectator +/// shell dimensions). @p self supplies the public RR interface; @p tgt is the +/// typed target (for bra()/ket() member iteration). +template +std::string rkb_fold_spfunction_call( + const RR& self, const TypedTarget& tgt, + const std::shared_ptr& context) { + std::ostringstream os; + os << context->label_to_function_name(self.label()) << "(inteval, " + << context->value_to_pointer(self.rr_target()->symbol()); + const unsigned int nc = self.num_children(); + for (unsigned int c = 0; c < nc; c++) { + os << ", " << context->value_to_pointer(self.rr_child(c)->symbol()); + } + // total_dim = product of all shell dims (all 4 shells are spectators) + unsigned int total_dim = 1; + for (unsigned int p = 0; p < 2; p++) { + SubIterator* si = tgt->bra().member_subiter(p, 0); + total_dim *= si->num_iter(); + delete si; + si = tgt->ket().member_subiter(p, 0); + total_dim *= si->num_iter(); + delete si; + } + os << "," << total_dim; + os << ")" << context->end_of_stat() << std::endl; + return os.str(); +} + +/// Emits the full element-wise function: the caller supplies the per-target +/// right-hand-side expression @p rhs (e.g. "src0[hsi] + src1[hsi]") and its +/// flop count @p nflops; everything else (symbol assignment, function +/// declaration, and the element loop scaffold) is identical across the four +/// operators and emitted here. Prefer rkb_fold_generate_code_from_terms(). +template +void rkb_fold_generate_code(const RR& self, + const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::string& funcname, std::ostream& decl, + std::ostream& def, const std::string& rhs, + unsigned int nflops) { + // declare_function lives in dg.cc + extern std::string declare_function( + const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::shared_ptr& args, const std::string& tlabel, + const std::string& function_descr, std::ostream& decl); + + std::shared_ptr localdims = rkb_fold_adapt_dims(dims); + // inline assign_symbols_: set symbol names on target/children and populate + // CodeSymbols + std::shared_ptr symbols(new CodeSymbols); + self.rr_target()->set_symbol("target"); + symbols->append_symbol("target"); + for (unsigned int c = 0; c < self.num_children(); c++) { + std::string symb = "src" + std::to_string(c); + self.rr_child(c)->set_symbol(symb); + symbols->append_symbol(symb); + } + LibraryTaskManager& taskmgr = LibraryTaskManager::Instance(); + const std::string tlabel = taskmgr.current().label(); + const std::string func_decl = + declare_function(context, localdims, symbols, tlabel, funcname, decl); + def << context->std_header(); + def << "#include <" << context->label_to_filename(funcname) << ".h>\n\n"; + def << context->code_prefix(); + def << func_decl << context->open_block() << std::endl; + def << context->std_function_header(); + def << "#ifdef __INTEL_COMPILER\n#pragma ivdep\n#endif\n"; + // the fold is element-wise, hence loop over all elements, including the + // low and vector dimensions + std::string nelem = localdims->high_label(); + if (!localdims->low_is_static() || localdims->low_label() != "1") + nelem += "*" + localdims->low_label(); + if (!localdims->vecdim_is_static()) + nelem += "*veclen"; + else if (localdims->vecdim_label() != "1") + nelem += "*" + localdims->vecdim_label(); + def << "for(int hsi = 0; hsi<" << nelem << "; hsi++) {\n"; + def << "target[hsi] = " << rhs << ";\n}\n"; + def << "/** Number of flops = " << nflops << " */\n"; + def << context->close_block() << std::endl; + def << context->code_postfix(); +} + +/// generate_code body of a fold relation whose children were created by +/// rkb_fold_make_children() from the same @p terms: emits +/// target = Σ_t coef_t · src. @p rr_name is used in the error message only. +template +void rkb_fold_generate_code_from_terms( + const RR& self, const RKBFoldTerms& terms, const std::string& rr_name, + const std::shared_ptr& context, + const std::shared_ptr& dims, + const std::string& funcname, std::ostream& decl, std::ostream& def) { + // add_child() deduplicates children by instance (see generic_rr.h), so a + // collision among the children would silently shift the src slots; fail the + // export loudly instead of emitting a wrong expression + if (self.num_children() != terms.size()) + throw std::logic_error(rr_name + "::generate_code: expected " + + std::to_string(terms.size()) + " children, got " + + std::to_string(self.num_children())); + unsigned int nflops = 0; + const std::string rhs = rkb_fold_rhs(terms, nflops); + rkb_fold_generate_code(self, context, dims, funcname, decl, def, rhs, nflops); +} + +} // namespace libint2 + +#endif // LIBINT_RKB_FOLD_CODEGEN_H diff --git a/src/bin/libint/rr.cc b/src/bin/libint/rr.cc index 32bbb9df9..a653bf4b2 100644 --- a/src/bin/libint/rr.cc +++ b/src/bin/libint/rr.cc @@ -127,8 +127,11 @@ void RecurrenceRelation::generate_code( // Assign symbols for the target and source integral sets std::shared_ptr symbols(new CodeSymbols); assign_symbols_(symbols); + // Compute local dimensions before optimize_rr_out so that + // handle_trivial_nodes uses the correct dims (e.g., "lowdim" instead of "1") + std::shared_ptr localdims = adapt_dims_(dims); // Traverse the graph - dg->optimize_rr_out(context); + dg->optimize_rr_out(context, localdims); dg->traverse(); #if PRINT_DAG_GRAPHVIZ { @@ -138,7 +141,6 @@ void RecurrenceRelation::generate_code( #endif // Generate code std::shared_ptr memman(new WorstFitMemoryManager()); - std::shared_ptr localdims = adapt_dims_(dims); dg->generate_code(context, memman, localdims, symbols, funcname, decl, def); // extract all external symbols -- these will be members of the evaluator diff --git a/src/bin/libint/strategy.cc b/src/bin/libint/strategy.cc index bbfc3fe21..d09b4811c 100644 --- a/src/bin/libint/strategy.cc +++ b/src/bin/libint/strategy.cc @@ -70,51 +70,92 @@ struct MasterStrategy; #if LIBINT_SHELLQUARTET_STRATEGY == LIBINT_SHELLQUARTET_STRATEGY_A0C0 template <> struct MasterStrategy { - typedef boost::mpl::list + VRR_a_11_TwoPRep_11_sh, VRR_c_11_TwoPRep_11_sh> value; }; template <> struct MasterStrategy { - typedef boost::mpl::list + VRR_a_11_TwoPRep_11_int, VRR_c_11_TwoPRep_11_int> value; }; #else // 0B0D strategy template <> struct MasterStrategy { - typedef boost::mpl::list + VRR_b_11_TwoPRep_11_sh, VRR_d_11_TwoPRep_11_sh> value; }; template <> struct MasterStrategy { - typedef boost::mpl::list + VRR_b_11_TwoPRep_11_int, VRR_d_11_TwoPRep_11_int> value; }; #endif +// TODO: each RKB operator adds a fixed set of MasterStrategy specializations +// that only name its composite recurrence (CR_11_*). A generic specialization +// keyed on the operator's composite-RR trait would remove this per-operator +// boilerplate and scale to new RKB operators without edits here. +template <> +struct MasterStrategy { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy<σpσpCoulombσpσp_11_11_sq> { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy<σpσpCoulombσpσp_11_11_int> { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy<σpCoulombσp_11_11_sq> { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy<σpCoulombσp_11_11_int> { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy { + typedef boost::mpl::list value; +}; + #if LIBINT_SHELLQUARTET_STRATEGY == LIBINT_SHELLQUARTET_STRATEGY_A0C0 template <> struct MasterStrategy { @@ -197,21 +238,31 @@ struct MasterStrategy { #if LIBINT_SHELLQUARTET_STRATEGY == LIBINT_SHELLQUARTET_STRATEGY_A0C0 template <> struct MasterStrategy { - typedef boost::mpl::list value; + typedef boost::mpl::list + value; }; template <> struct MasterStrategy { - typedef boost::mpl::list + typedef boost::mpl::list value; }; #else // 0B0D strategy template <> struct MasterStrategy { - typedef boost::mpl::list value; + typedef boost::mpl::list + value; }; template <> struct MasterStrategy { - typedef boost::mpl::list + typedef boost::mpl::list value; }; #endif @@ -276,6 +327,14 @@ struct MasterStrategy<σpVσp_1_1_int> { typedef boost::mpl::list value; }; template <> +struct MasterStrategy<σpeμσp_1_1_sh> { + typedef boost::mpl::list value; +}; +template <> +struct MasterStrategy<σpeμσp_1_1_int> { + typedef boost::mpl::list value; +}; +template <> struct MasterStrategy { typedef boost::mpl::list> value; }; diff --git a/src/bin/test_eri/eri.h b/src/bin/test_eri/eri.h index 079db0f02..deecbda33 100644 --- a/src/bin/test_eri/eri.h +++ b/src/bin/test_eri/eri.h @@ -101,10 +101,10 @@ struct ExpensiveMath { namespace libint2 { inline int min(int t1, unsigned int t2) { return t1 > (int)t2 ? (int)t2 : t1; } +/// @return \f$ (-1)^a \f$ for any integer @p a , negative included template int parity(Int a) { - if (a % 2 == 1) return -1; - return 1; + return (a % 2 != 0) ? -1 : 1; } }; // namespace libint2