Skip to content

Relativistic 2e 3- and 4-center integrals - #399

Merged
evaleev merged 72 commits into
masterfrom
kshitij/feature/2e_rkb_ints
Oct 26, 2026
Merged

evaleev merged 72 commits into
masterfrom
kshitij/feature/2e_rkb_ints

Conversation

@kshitij-05

@kshitij-05 kshitij-05 commented Feb 9, 2026 •

Copy link
Copy Markdown
Collaborator
  • Implement 2-electron 4-center relativistic integrals with restricted kinetic balance condition (RKB).

    • (LL|SS)
    • (SS|SS)
    • (LS|LS) [gaunt]
  • Implement 2e 3-center relativistic integrals with RKB.

    • (X|SS)
    • (X|LS) [gaunt]

    Also adds σpeμσp operator for small component dipole moment integrals

@loriab loriab left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed this and saw some tweaks to propose. You might want to add the new class to INSTALL.md, too.

Comment thread include/libint2/config.h.cmake.in Outdated
Comment thread include/libint2/config.h.cmake.in Outdated
Comment thread include/libint2/engine.impl.h Outdated
Comment thread include/libint2/engine.impl.h Outdated
Comment thread include/libint2/engine.impl.h Outdated
Comment thread include/libint2/engine.impl.h Outdated
Comment thread cmake/modules/int_am.cmake Outdated
… unique am shell sets and phase change for this operator
…+ progress bar + sign fix

- ShellQuartetSetPredicate: add braket-swap tiebreaker for bra_ket_coswappable
  operators (σpσpCoulombσpσp). When la+lb == lc+ld, use max(la,lb) <= lc to
  pick one canonical representative, reducing duplicate quartet generation.

- Engine (engine.impl.h): update swap_braket logic for opop_coulomb_opop to
  match the new predicate tiebreaker. Add coupled-swap sign correction in the
  swap_braket branch (was missing — exposed by d-shell testing).

- build_libint.cc: disable CSE (do_cse/condense_expr) for multi-component
  operators since their 16 components share no intermediates at the expression
  level. This eliminates the superlinear optimize_rr_out bottleneck (e.g.,
  8.8s → 71ms for (ss|ds) prerequisite DAG).

- build_libint.cc: fix compilation when only LIBINT_INCLUDE_RKB_ERI is defined
  (without LIBINT_INCLUDE_ERI): extend #ifdef guards for build_TwoPRep_2b_2k,
  add forward declaration, move make_descr to detail namespace, use if constexpr
  for component descriptor construction.

- buildtest.h: add CodeGenProgress spinner showing elapsed time, function count,
  and current task name on stderr during code generation.

- int_am.cmake: fix typo in OPT_AM variable reference.
@kshitij-05
kshitij-05 force-pushed the kshitij/feature/2e_rkb_ints branch from 25265a6 to 41f1dad Compare March 22, 2026 18:19
@kshitij-05
kshitij-05 force-pushed the kshitij/feature/2e_rkb_ints branch from 025cdfb to 3194cad Compare April 1, 2026 16:26
Apply the same optimization as HRR: since differentiation of a Gaussian
depends only on that shell's quanta (not spectators), generate code once
per unique differentiated shell and pass spectator dims at the call site.

Verified: 295,944 assertions pass (Coulomb + ERI derivs + RKB integrals).
handle_trivial_nodes() used default_dims() (hardcoded "1") before
adapt_dims_() provided correct runtime dims ("lowdim"/"highdim").
Pass localdims through optimize_rr_out → handle_trivial_nodes.
@kshitij-05
kshitij-05 force-pushed the kshitij/feature/2e_rkb_ints branch from 3194cad to 9b50b0b Compare April 10, 2026 18:02
…p files

Add code-sharing overrides (generate_label, spfunction_call, adapt_dims_,
generate_code) to CR_11_Coulombσpσp_11 and CR_11_σpσpCoulombσpσp_11.
Shell quartets with the same quaternion component share a single function
parameterized by highdim. Hand-emits element-wise loops to avoid TwoPRep
particle-swap child deduplication issues in DAG-based codegen.
@evaleev evaleev added this to the 2.14 milestone Apr 17, 2026
(μ ∂_a ν | 1/r12 | κ ∂_b λ) for a, b ∈ {x,y,z}. Needed for Gaunt LS
Fock where coulomb_opop's 4 folded outputs are rank-deficient. Only
2-fold bra↔ket symmetry (p1_p2_swappable, no within-side swap); uses
dedicated predicate la+lb <= lc+ld. Also: LIBINT2_SIMPLE_CORE_EVAL_CASE
macro to compactify Coulomb-family dispatch in engine.impl.h.
Scalar trace + 3 antisym + 5 sym-TL replaces raw 3*a+b dyadic.
Bra<->ket swap: per-component sign flip on antisym (was: index
transpose). p1_p2_swappable=true. Tests pass: 2,112,533 assertions.
12 components = 3 dipole directions × 4 Pauli quaternion (trace + 3
antisym), mirroring σpVσp's fold. Engine origin via set_params.
Master integral list split via mpl::joint_view (boost list50 limit).
…rkb_ints

# Conflicts:
#	export/tests/unit/test-1body.cc
#	include/libint2/engine.h
#	include/libint2/engine.impl.h
#	src/bin/libint/comp_1_σpVσp_1.h
#	src/bin/libint/oper.h
kshitij-05 and others added 8 commits October 23, 2026 17:39
generate_label() returned "CR_σpCoulombσp_<n>", so the Unicode σ leaked
into generated function names and source filenames (CR_σpCoulombσp_0.cc
etc.), unlike the sibling fold CRs (CR_Coulombopop_, CR_opopCoulombopop_,
CR_Coulombop_). Use "CR_opCoulombop_".

generate_code() already selected the right-hand side from the component
index but did not check num_children(); add the same guard as the other
two fold CRs so a child collision in add_child() fails the export instead
of producing a call with fewer src arguments than the expression uses.
…ument the RKB component conventions

Engine: operator_traits<Operator> gains pair_swap (independent/coupled/none),
braket_swap, pair_swap_phase(c), braket_swap_phase(c) and
braket_swap_remap(c). Engine::compute2 canonicalizes shell sets and applies
per-component phases and remaps from these traits; the per-operator branches
for coulomb_opop, coulomb_op, opop_coulomb_opop and op_coulomb_op are gone.
Engine::_initialize no longer leaves stale target/scratch buffers when the
library lacks the kernels.

Compiler: the 2-body operator descriptors gain the matching p1p2_swappable and
pair_swap traits, ShellQuartetSetPredicate takes PairSwap instead of a bool,
and build_TwoPRep_{2b_2k,1b_2k} derive the shell sets and the per-component
targets (make_descr over max_key) from the descriptor instead of
std::is_same branches. Adds the missing make_descr specializations for
σpCoulombσp and Coulombσp. Generated shell-set coverage is unchanged
(verified per task against the predicates).

engine.h gains a doc page (\page rkb_operators) with one table of the
component layouts, spin folding and naming (engine vs compiler vs task label)
of the four RKB 2-body operators and opemuop.

Also: the progress counter no longer counts each 2-body kernel twice, and
print_config reports RKB 3-center ERIs (and spells "kinetic balance").
- Libint compiler requires C++17 (it uses if constexpr); was compiled as a
  C++14 extension on clang and relied on the gcc default elsewhere
- code generation progress: on a terminal keep the single updating line; in
  logs (CI) print one line per kernel so a failure can be attributed
- the Hartree-Fock examples are only built/tested if the library has 4-center
  Coulomb integrals, so the check target passes for an RKB-only library
- config.h: fix copy-pasted comments of the RKB options
- CHANGES: document the new std::invalid_argument for cartesian fitting
  shells with LIBINT2_ERI3_PURE_SH and the compiler's C++17 requirement
…ject xx_xs derivatives early; defined AM-limit state

- The std::invalid_argument for a cartesian fitting shell with
  LIBINT2_ERI3_PURE_SH (a287bd7) now applies to derivative order 0 only:
  the generator marks the fitting shell pure_sh only for deriv_level == 0
  (build_libint.cc), so derivative kernels are cartesian-complete and were
  wrongly rejected. Same condition on the ERI2 asserts (xs_xs), whose ket
  message also said "in bra".
- BraKet::xx_xs with deriv_order > 0 is rejected at initialize(); before it
  reached a std::logic_error from DerivMapGenerator::instance inside
  compute2.
- _initialize() clears buildfnptrs_ before throwing lmax_exceeded, so an
  Engine that outlives the exception throws missing_kernel instead of
  dispatching a build table that does not match primdata_. hard_lmax_ and
  hard_default_lmax_ are initialized; they were indeterminate for a
  kernel-less or default-constructed Engine and copied as such.
…asses in the derivative glob

Upstream e22dfdd renamed set(${class}_MAX_AM ...) to LIBINT_${class}_MAX_AM
but left the reads in the scalar branch of both process_integrals_class*
macros bare, so with a scalar LIBINT2_<class>_MAX_AM the
_candidate_<class>_d<d> values were empty: Libint2_ERI_COMPONENTS
under-reported the generated kernels (find_package(Libint2 COMPONENTS ...)),
the "Enabling integrals class X to max AM" message printed a blank, and the
AM-range validation was dead. The restored validation rejects negative
values (the list branch's rule) rather than zero.

Also: LIBINT2_ENABLE_RKB_ERI/RKB_ERI3 take part in _glob_classes_derivs, and
_rkb_eri3_impure_sh is renamed _rkb_eri3_components now that
LIBINT2_RKB_ERI3_PURE_SH is gone.
…RI3_ORDER

Mirrors the RKB_ERI feature macros so a consumer can test for 3-center RKB
support without probing LIBINT2_TASK_EXISTS_3coulomb_opop.
…function label

directional() is always true, so a shell carrying derivative quanta in two
directions (second derivatives) is a valid target for two instances whose
children and a[dir] coefficients differ. The RRStack deduplicates generated
functions by label, which omitted dir, so only the first instance's function
was emitted and correctness relied on the tactic tie-break selecting that
same instance. Encode dir in both the code-shared and the
translational-invariance labels (the latter inherited the omission from
CR_DerivGauss).

Also drop a dead `if (np == 1) return true;` in expl_low_dim().
@kshitij-05

kshitij-05 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Audit of the branch before pushing: two review passes (branch vs master, then the four commits that landed today), each finding verified against the code. What was fixed, and what the new commits had already covered.

Fixed in this push

  • edf8161 engine.h / engine.impl.h
    • The new std::invalid_argument for Cartesian fitting shells with LIBINT2_ERI3_PURE_SH (a287bd7) now applies to derivative order 0 only. The generator marks the fitting shell pure_sh only for deriv_level == 0 (build_libint.cc 1671/1716), so derivative kernels are Cartesian-complete and were being rejected. Same condition on the Debug-only ERI2 asserts (plus "in bra" → "in ket" in the ket message).
    • BraKet::xx_xs with deriv_order > 0 is rejected at initialize(); before, it got as far as a std::logic_error from DerivMapGenerator::instance inside compute2.
    • _initialize() clears buildfnptrs_ before throwing lmax_exceeded, so an Engine that survives the exception throws missing_kernel instead of dispatching a table that does not match primdata_. hard_lmax_ / hard_default_lmax_ are initialized; they were indeterminate for a kernel-less or default-constructed Engine (and copied as such).
  • 00042b6 cmake/modules/int_am.cmake
    • The scalar LIBINT2_<class>_MAX_AM branch of both process_integrals_class* macros still read the bare ${class}_MAX_AM after e22dfdd renamed it to LIBINT_${class}_MAX_AM. Effects: _candidate_<class>_d<d> empty, so Libint2_ERI_COMPONENTS under-reported (find_package(Libint2 COMPONENTS ...)); the "Enabling integrals class X to max AM" line printed a blank; the AM-range validation was dead. Pre-existing on master, hit by every lane that passes a scalar max AM.
    • RKB classes take part in _glob_classes_derivs; _rkb_eri3_impure_sh renamed after the PURE_SH removal.
  • 2a5178c build_libint.cc: LIBINT2_SUPPORT_RKB_ERI3 / LIBINT2_DERIV_RKB_ERI3_ORDER feature macros, mirroring RKB_ERI.
  • af3d628 comp_deriv_gauss_v2.h: the generated-function label includes the differentiation direction. directional() is always true, so a shell with derivative quanta in two directions (2nd derivatives) is a valid target for two instances whose children and a[dir] coefficients differ, yet the RRStack deduplicated them by label and kept the first one; correctness relied on the tactic tie-break selecting the same instance. Inherited from CR_DerivGauss. Also removed a dead if (np == 1) return true;.
  • 54afe4d / 9c02f59 (earlier today): fold CRs select the sign pattern from the component index and check the child count; CR_σpCoulombσp_ → CR_opCoulombop_, since the Unicode label had leaked into generated symbols and filenames.

Already covered by f3d21c1 / 884e8d7: per-operator swap branches (now traits), progress double count and the stderr spinner, print_config, the stale canonical-form comment, and both RKB_ERI3_PURE_SH findings.

Verified: light build, all max AM 2, ONEBODY/RKB_ERI/RKB_ERI3 at derivative 0 and ERI/ERI3 at derivative 1: compiler tests pass, export passes, the configure log now prints the max AM values; the library build and check-libint2 on this exact tree were still running when this was posted and CI runs the full set. Earlier today, before the engine/cmake fixes, the same pipeline at derivative 0 passed 12/13 ctest entries (the known hf++ validate.py SyntaxError) with the unit suite at 3,084,450 assertions in 41 cases, including the new solid-harmonics case.

Update (build finished): on the pushed tree (af3d628f), same configuration, check-libint2 passes 13/13; unit suite 21,019,631 assertions in 41 cases; "eri geometric derivatives vs finite differences" passes on the new DerivGaussV2 labels. libint2/hf++/run passes as well: its long-standing SyntaxError in validate.py came from the empty LIBINT_ERI_MAX_AM that the int_am.cmake fix (00042b6) restores.

…g components

`targets_` is a std::vector with ext_stack_allocator, its elements live in
primdata_[0].targets. The component remap after a bra-ket swap (f3d21c1)
copied that vector (`const auto tmp = targets_;`); the copy inherits the
allocator, and whether the copy's storage overlaps the original's depends on
how the standard library maintains the bump pointer, so on libc++ (mingw
CLANG64 CI lane) the remap read entries it had already overwritten and the
opop_coulomb_opop components came out scrambled for every quartet that needs
a bra-ket swap ((s d|s p) in "RKB Coulomb integrals"; the canonical kernel
(p s|d s) was bit-identical to Linux). Copy the pointers into a local
std::array, as the pre-f3d21c16 code did.

Also make ext_stack_allocator safe to copy through a container:
select_on_container_copy_construction() returns a heap-only allocator, and
allocate()/deallocate() handle the heap-only state.
@kshitij-05

Copy link
Copy Markdown
Collaborator Author

Windows CI, two separate failures, both bisected by job history to commits of today (neither from the audit fixes):

  1. Export • windows-mingw-CLANG64, "RKB Coulomb integrals", since f3d21c1. Root cause found and fixed in 7d8b6ad: the component remap after a bra-ket swap copied targets_ (const auto tmp = targets_;), a std::vector on ext_stack_allocator whose elements live in primdata_[0].targets. The copy inherits the allocator state; on libc++ the copy's storage overlapped the original's, so the remap read entries it had already overwritten and every opop_coulomb_opop quartet that needs a bra-ket swap came out scrambled. Evidence from a diagnostic branch run: on mingw the canonical kernel (p s|d s) is bit-identical to Linux, while the engine-permuted (s d|s p) disagrees with its transpose by 0.042. The fix copies the pointers into a local std::array (as before f3d21c1) and makes ext_stack_allocator::select_on_container_copy_construction() return a heap-only allocator so a container copy can never alias again.

  2. Export • windows-clang-cl, five 1-body absolute-reference tests (electrostatic potential, erf, W, σpeμσp, python correctness), since a287bd7. The wrong values are exactly "correct pure value + 1.0 × the Cartesian xx element", i.e. the d-shell solid-harmonics m=0 row behaves as if its xx coefficient were +0.5 instead of −0.5. The tarball consumed by that lane is byte-identical in generated code to the last passing run; ASan/UBSan are clean on the path; and on the diagnostic branch, which only appends a TEST_CASE to test-1body.cc, all 42 cases pass on clang-cl with every printed value correct (parity(-1) = -1, m=0 row {−0.5, −0.5, 1}). So this one is layout-dependent (heap or code layout), not a logic error visible in the source; the first test of the run is the one failing, so test-order contamination is excluded. I could not identify the mechanism from macOS. If it comes back after 7d8b6ad (which changes the library layout), it needs a Windows debugger or a MSan-like build.

…i parity for negative arguments

SolidHarmonicsCoefficients::coeff() called an anonymous-namespace `parity`
unqualified. Under MSVC-style delayed template parsing (clang-cl's default)
an unqualified name in a template body is bound at instantiation, so in a TU
that had brought another `parity` into namespace libint2 the call resolved
to that one instead. test_eri/eri.h declares such a libint2::parity, and it
returns +1 for negative odd arguments (`a % 2 == 1` is false for -1). Since
a287bd7 test-2body.cc (which includes test_eri/eri.h) instantiates the
solid harmonics transforms, its copy of coeff<double> won the COMDAT for the
whole unit-test executable, and the one coefficient that depends on
parity(-1) for l <= 2, the xx entry of the d-shell m=0 row, came out +0.5
instead of -0.5. Every 1-body absolute-reference test with pure shells then
failed on the windows-clang-cl CI lane while all other lanes, which use
two-phase lookup, passed. Reproduced with clang++ -fdelayed-template-parsing.

- solidharmonics.h: move parity into libint2::solidharmonics::detail, make
  it constexpr and correct for negative arguments, call it qualified
- test_eri/eri.h: libint2::parity returns (-1)^a for negative a too
- test-2body.cc: check coeff(2,0,2,0,0) == -0.5 from the TU that includes
  test_eri/eri.h
@kshitij-05

Copy link
Copy Markdown
Collaborator Author

clang-cl root cause found and fixed in 307d3dd. SolidHarmonicsCoefficients::coeff() called an anonymous-namespace parity unqualified. clang-cl defaults to MSVC-style delayed template parsing, which binds unqualified names in template bodies at instantiation. test-2body.cc includes test_eri/eri.h, which declares libint2::parity with if (a % 2 == 1) return -1; (so parity(-1) == +1), and since a287bd7 that TU instantiates the solid-harmonics transforms; its copy of coeff<double> wins the COMDAT for the whole test executable. The only l ≤ 2 coefficient that depends on parity(-1) is the xx entry of the d-shell m=0 row, hence exactly "+0.5 instead of −0.5" and exactly the pure-shell 1-body tests. Reproduced locally with clang++ -fdelayed-template-parsing (gives +0.5; without the flag −0.5; with the fix −0.5 in both modes). Fix: parity moved to libint2::solidharmonics::detail, called qualified, correct for negative arguments; libint2::parity in test_eri/eri.h fixed for negative odd arguments; a one-line guard in test-2body.cc. The diagnostic branch is deleted.

…ment xx_xs, port generic code to DerivGaussV2, extend tests

- build_libint: LIBINT_RKB_ERI[3]_OPT_AM were no-ops because CSE/condense_expr were
  forced off for multi-component operators; both builders now treat them as every
  other class (CSE only affects unrolled expressions, the DAG dedups by instance)
- engine.h: document that with BraKet::xx_xs sigma.p acts on the user's bra for
  coulomb_opop/coulomb_op; restore the rkb_operators table (clang-format had
  reflowed it) and guard it with clang-format off/on
- DerivGaussV2: implement has_generic/generic_header/generic_instance so
  LIBINT2_ENABLE_GENERIC_CODE keeps working for derivative ERIs; the generic call
  takes the shared function's runtime highdim/lowdim
- CHANGES: list the library-wide changes (xx_xs energies, DerivGaussV2 for all ERI
  derivative kernels, missing_kernel instead of asserts, int_am.cmake validation)
- CI: the macOS Release lane also builds 2nd ERI derivatives
- tests: opemuop checked vs finite differences for all s/p/d pairs (contracted,
  pure, displaced origin); new test that a failed re-initialization leaves the
  Engine throwing missing_kernel
Resolves conflicts in CHANGES and include/libint2/config.h.cmake.in. The RKB_ERI
and RKB_ERI3 scalar MAX_AM/OPT_AM macros now use master's #define/#undef pattern
(#445/#446), so that a value of 0 is preserved, and are mapped to the -1 sentinel
in CMakeLists.txt like the other classes.
Resolves the conflict in CHANGES.
…le in buildtest.h

GenerateCode() in buildtest.h references g_progress, which was defined only in
build_libint.cc and test.cc, so libint-compiler-eri-test-generate failed to link.
…et; fix the RKB xx_xs alias test

- set() changes oper_/braket_ before calling initialize(), so when initialize()
  rejected RKB or xx_xs derivatives the Engine kept the kernel table of the
  previous operator and compute() dispatched it (e.g. eri1 kernels for
  coulomb_opop). Clear buildfnptrs_ before throwing, as for lmax_exceeded, so
  compute() throws missing_kernel; add a test.
- the xx_xs alias test constructed its Engines with the default (4-center)
  braket, which throws lmax_exceeded when LIBINT2_RKB_ERI_MAX_AM is below the
  3-center AM; pass the braket to the constructor.
@evaleev

evaleev commented Oct 25, 2026

Copy link
Copy Markdown
Owner

@kshitij-05 I merged master twice (#445/#446, then #449) and ran another adversarial review of the branch. Nothing blocks the merge. Three bugs are fixed in the commits I just pushed. Three warnings are left for you, because they depend on conventions you chose.

The two items from the previous round are fixed:

  • the sign patterns in the composite RRs now switch on the component index;
  • the comment about canonicalization is gone.

Merges (b0802c7, cfbf6e7)

Fixed

  • 5a3755c: set() changes oper_/braket_ and then calls initialize(). When initialize() rejected the change (RKB with deriv > 0, or xx_xs with deriv > 0), the Engine kept the previous kernel table, and compute() dispatched it.
    • Example: Engine(coulomb, n, L, 1), then set(coulomb_opop) throws, and a later compute() runs the eri1 kernels as coulomb_opop, which also overruns the Boys table.
    • buildfnptrs_ is now cleared before the throw, as for lmax_exceeded, so compute() throws missing_kernel instead.
    • There is a new test, and it fails without the fix.
  • 5a3755c: the "Coulombσpσp 3-center xx_xs alias" test built its Engines with the default 4-center braket. That throws lmax_exceeded whenever LIBINT2_RKB_ERI_MAX_AM is less than the 3-center AM. The test now passes the braket to the constructor.
  • d20e092: libint-compiler-eri-test-generate failed to link with an undefined g_progress. It is now defined once, as an inline variable in buildtest.h.

Warnings: please take a look

  1. Sign convention of coulomb_op.
    • CR_11_Coulombσp_11 takes the derivative with respect to the center of d (d_k.deriv().inc(k)), i.e. ∂/∂D = −∂/∂r.
    • Its comment says "the -i factor ... applied by the caller". But σ·p ν = −i σ·∇_r ν = +i σ·∂_D ν, so a caller who follows the comment gets every coulomb_op integral with the wrong sign.
    • This is the only RKB operator with an odd number of derivatives; for the other three the sign cancels.
    • The unit test uses the same center-derivative convention, so it cannot catch this.
    • Suggested fixes:
      • add a prefactor column to the \page rkb_operators table in engine.h (the other operators also carry an implicit −1 and/or i);
      • fix the comment;
      • change "d/dr" to "d/dA" in the comment of comp_deriv_gauss_v2.h.
  2. Each expansion is written twice. In comp_11_Coulombσpσp_11.h, comp_11_σpσpCoulombσpσp_11.h and comp_11_σpCoulombσp_11.h, the order of the make_child calls and the sign strings in generate_code() are maintained separately.
    • The child-count guard catches deduplication, but not a reordering. Swapping two mc(...) lines leaves the count, and expr_, unchanged, while generate_code() emits the wrong sign for every shell set that shares the function.
    • Driving both from one (coefficient, derivative tuple) table per component would make that impossible.
  3. AM limits per center are checked only by assert (the xs_xx buildfnidx block in engine.impl.h).
    • _initialize checks lmax_ only against max(hard_lmax_, hard_default_lmax_).
    • Example: build with MAX_AM=2 and RKB_ERI3_MAX_AM=3. In Release, an f AO shell indexes into the wrong kernel (e.g. it returns the (p|ss) kernel's result).
    • The asserts also use <= where < is needed.
    • This is the same on master, but the new RKB_ERI3_MAX_AM knob makes a mismatched configuration much more likely. Suggest throwing lmax_exceeded per center in all builds.

Notes

  • set(BraKet) and set_max_l do not rebuild core_eval_pack_. So Engine(..., BraKet::xs_xx) followed by set(BraKet::xx_xx) can need a higher Boys order than the evaluator was built for. This is pre-existing, but the intrinsic derivatives of the RKB operators make it more likely.
  • The choice between explicit DerivGaussV2 code and generic code (comp_deriv_gauss_v2.h, max_am_opt()) uses the default task's OPT_AM, not the per-class value. This affects performance only.
  • The "lmax_exceeded leaves no kernels" test compiles to nothing unless MAX_AM_3eri > MAX_AM_eri. Using 3coulomb_opop vs coulomb_opop would exercise it in more configurations.
  • The xx_xs alias test and the coulomb_op test pick AO shells using the fitting-center limit (LIBINT2_MAX_AM_3coulomb_opop). check_3center correctly uses min(LIBINT2_MAX_AM_default, ...).
  • The RKB descriptors use Multiplicative*_Props, so hermitian() returns +1 and HRR would consider them movable. This is harmless today because only the composite RR is in their strategies, but it is a trap if HRR is ever enabled for them.
  • Stale or misleading text:
    • "CR_DerivGauss" label prefix and comments in the DerivGaussV2 translational-invariance path;
    • "quaternion_index" for the 16- and 9-component indices;
    • "(a b | op c op d)" in comp_11_σpCoulombσp_11.h;
    • "the operator acts on b and/or d" in engine.h, which contradicts the table;
    • the \page block now separates the /// doc of rank() from the function.
  • opemuop renumbers every 2-body Operator. CHANGES says so, and the next release should also bump the SONAME.

This review was agent-generated and human-reviewed.

- "lmax_exceeded leaves no kernels" aborted in Debug builds: set(BraKet::xx_xx)
  changes the braket before initialize() throws, so the Engine is left as a
  kernel-less 4-center Engine and the 3-shell compute(fit, s, s) that followed
  tripped the argument-count assert of compute() (Release builds fell through
  to missing_kernel by accident). Check the braket instead, and add the
  coulomb_opop/3coulomb_opop variant of the test, which is live whenever
  LIBINT2_RKB_ERI3_MAX_AM > LIBINT2_RKB_ERI_MAX_AM.
- The Release lane with LIBINT2_MAX_AM="2;2;1" and ERI derivatives up to 2
  (introduced in aceb113) segfaulted in the Fortran ERI test and example and
  failed hartree-fock++: all three computed 2nd-derivative integrals over a d
  shell although the 2nd-derivative kernels stop at p. The Fortran test and
  example clamp the shells of each derivative order to LIBINT2_MAX_AM_eri<d>;
  hartree-fock++ computes the 2-body forces/hessian (and the derivative
  2-center checks) only if the kernels support the basis and says so otherwise,
  which the validator accepts.
- The xx_xs alias and coulomb_op 3-center tests limit the AO shells by
  LIBINT2_MAX_AM, not by the fitting-center limit.
…e evaluator in set(BraKet)/set_max_l; document the σ·p prefactors

compute2() checked the angular momentum of each shell against the limit of its
center only by assert. initialize() checks lmax_ against the most permissive
center, so with a center-dependent table (3eri, or 3coulomb_opop with
LIBINT2_RKB_ERI3_MAX_AM != LIBINT2_MAX_AM) an AO shell could pass that check
and, in Release, index the kernel table out of range and run another shell
set's kernel. Throw lmax_exceeded instead, in all builds; the asserts also used
<= where < was needed (hard_lmax_ is the limit + 1). The task label is kept for
the message.

set(BraKet) and set_max_l() did not rebuild core_eval_pack_, whose order is
braket_rank() * lmax_ + deriv_order_, so e.g. Engine(..., BraKet::xs_xx)
followed by set(BraKet::xx_xx) could need a higher Boys order than the
evaluator was built for.

The rkb_operators page gets a prefactor column: all kernels differentiate with
respect to the centers of the functions (∂_X = -∂_r), so σ·p contributes +i
(on b, d) or -i (on the conjugated a, c). coulomb_opop and opop_coulomb_opop
need no prefactor beyond the folded spin block q₀ + i σ·q, coulomb_op needs
+i, op_coulomb_op needs -1. The old comment on coulomb_op said -i, which is
wrong. The page is moved so that it no longer separates the doc of rank() from
the function, and the sentence "acts on b and/or d" (wrong for coulomb_opop and
opop_coulomb_opop) is replaced by a column.
… children and the generated code

Each of the four σ·p fold relations (CR_11_Coulombσp_11, CR_11_Coulombσpσp_11,
CR_11_σpCoulombσp_11, CR_11_σpσpCoulombσpσp_11) kept its expansion twice: the
order of the make_child() calls in the constructor and the sign string in
generate_code(). The child-count guard caught a deduplicated child but not a
reordering. Now each relation defines fold_terms(component): a list of
(coefficient, derivative direction on a, b, c, d) terms, from which
rkb_fold_make_children() creates the children (and the symbolic expression) and
rkb_fold_generate_code_from_terms() emits "target = Σ coef_t src_t", so the two
cannot disagree. The Pauli components of (σ·∇)(σ·∇') live in one table
(rkb_fold_pair_terms); the 16-component relation is its product (with the -1
of i² on the cross × cross block), the 9 irreps of op_coulomb_op are listed
explicitly. The generated kernels are unchanged up to the order of the terms
in the dot × cross components and the spacing of a leading sign.

Also:
- σpσpCoulombσpσp_Descr::quaternion_index() is renamed component_index(): the
  16 components are not a quaternion
- the RKB descriptors note that their Multiplicative*_Props make hermitian()
  return +1, so HRR must stay out of their strategies
- Coulombσp_Descr and CR_11_Coulombσp_11 state the correct prefactor (+i, not
  -i) and that the derivative is with respect to the center of the function
- DerivGaussV2: the expansion formula is d/dA, not d/dr; the translational
  invariance instances no longer share the "CR_DerivGauss" label prefix with
  CR_DerivGauss's own instances (the RRStack deduplicates by label); the
  explicit-vs-generic decision uses the OPT_AM of the current task, not of the
  default task
@kshitij-05

Copy link
Copy Markdown
Collaborator Author

Follow-up to the review of 2026-10-25, in 40e9389 (tests), 9fba2b0 (engine) and 046a313 (compiler).

CI of 5a3755c: two failures, both in tests

  • Debug lanes: "lmax_exceeded leaves no kernels" aborted on the argument-count assert of compute(). set(BraKet::xx_xx) changes the braket before initialize() throws, so the Engine is left as a kernel-less 4-center Engine, and the 3-shell compute(fit, s, s) that followed is an argument-count error (Release reached missing_kernel by accident). The test now checks the braket instead, and has a coulomb_opop/3coulomb_opop section (live when LIBINT2_RKB_ERI3_MAX_AM > LIBINT2_RKB_ERI_MAX_AM), as suggested.
  • macOS Release shared=ON, the lane that aceb113 reconfigured to LIBINT2_MAX_AM="2;2;1" with ERI derivatives up to 2: the Fortran ERI test and example and hf++ evaluated 2nd-derivative integrals over a d shell while the deriv-2 kernels stop at p (segfault through the C/Fortran kernel table; lmax_exceeded from hf++'s Hessian engine). The Fortran test and example clamp each derivative order to LIBINT2_MAX_AM_eri<d>; hf++ computes the 2-body forces/Hessian (and the derivative 2-center checks) only when the kernels support the basis and prints a "skipped" line otherwise, which the validator accepts.

Warnings

  1. Sign convention of coulomb_op: the comment was wrong, the kernels are unchanged. The \page rkb_operators now states the derivative convention (all kernels differentiate with respect to the centers, so σ·p contributes +i on b, d and −i on the conjugated a, c) and has a prefactor column: coulomb_opop and opop_coulomb_opop need nothing beyond the folded spin block q₀ + i σ·q, coulomb_op needs +i, op_coulomb_op needs −1. The "acts on b and/or d" sentence became a column; comp_deriv_gauss_v2.h says d/dA.
  2. Each expansion written twice: every fold relation now has one fold_terms(component) table, (coefficient, derivative direction on a, b, c, d) per term; rkb_fold_make_children() creates the children and the symbolic expression from it and rkb_fold_generate_code_from_terms() emits the element-wise expression from it. The Pauli components of (σ·∇)(σ·∇') live in one table (rkb_fold_pair_terms), the 16-component relation is its product with the −1 of i² on the cross × cross block, the 9 irreps of op_coulomb_op are listed. Diff of the 32 generated fold kernels before/after: 17 byte-identical (all of coulomb_op, coulomb_opop, op_coulomb_op, and SS of opop_coulomb_opop); the other 15 differ only in the spacing of a leading minus and, for the dot × cross components, in the order of the six terms (bra pair outer, ket pair inner).
  3. Per-center AM limits: compute2() now throws lmax_exceeded (with the task label) in all builds when a shell exceeds the limit of its center, comparing with <.

Notes

  • set(BraKet) and set_max_l() rebuild core_eval_pack_.
  • DerivGaussV2's explicit-vs-generic choice uses the OPT_AM of the current task.
  • The 16-component descriptor's quaternion_index() is now component_index(); the RKB descriptors carry a note on the hermitian()/HRR trap; the translational-invariance instances of DerivGaussV2 are labeled DerivGaussV2TI... instead of CR_DerivGauss... (CR_DerivGauss labels its own instances that way, and the RRStack deduplicates by label); (a σ·p b | c σ·p d) fixed; the page no longer splits the doc of rank().
  • The xx_xs alias and coulomb_op tests limit the AO shells by LIBINT2_MAX_AM.
  • SONAME bump for opemuop: noted, yours at release time.

Verification (M2, Apple clang 17, gfortran 14)

  • The configuration of the failing lane (MAX_AM=2;2;1, ERI_MAX_AM=2;2;1, ENABLE_ERI=2, ERI3_MAX_AM=3;2, RKB_ERI3_MAX_AM=3, Release compiler, Release shared library with Fortran): check-libint2 17/17, unit 43 cases / 21,128,306 assertions, hf++ validates with the Hessian skipped, Fortran example and test pass.
  • The default CI configuration (MAX_AM=2;2, ERI/ERI3 derivative 1, RKB on, Debug, static): check-libint2compiler 2/2, check-libint2 13/13, unit 43 cases / 21,040,446 assertions; "RKB Coulomb integrals" 295,673 and "RKB integrals with solid harmonics shells" 694,742 assertions, unchanged.

@evaleev
evaleev merged commit 0ede003 into master Oct 26, 2026
13 checks passed
@evaleev
evaleev deleted the kshitij/feature/2e_rkb_ints branch October 26, 2026 02:51
evaleev added a commit that referenced this pull request Oct 26, 2026
Follow-ups to the reviews of #399: lmax checks, hartree-fock++ hessian coverage, compiler cleanups
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants