Skip to content

Follow-ups to the reviews of #399: lmax checks, hartree-fock++ hessian coverage, compiler cleanups - #450

Merged
evaleev merged 11 commits into
masterfrom
fix/rkb-review-followups
Oct 26, 2026
Merged

evaleev merged 11 commits into
masterfrom
fix/rkb-review-followups

Conversation

@evaleev

@evaleev evaleev commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Follow-ups to the reviews of #399 (the nonblocking items that were still open when it merged).

Engine

  • compute1()/compute2() check the shells against the Engine's max_l in all builds and throw Engine::lmax_exceeded. Before, this was only a Debug assert, so a Release build overran the stack/scratch sized for max_l; this is also the case on master before Relativistic 2e 3- and 4-center integrals #399.
  • lmax_exceeded::lmax_limit() is the max AM wherever it is thrown; initialize() used to report max AM + 1. what() now names the task and both values.
  • rkb_operators docs: the "folded" paragraph applies to the operators with one folded electron; opop_coulomb_opop already contains the −1 of i².
  • New unit tests:
    • the per-center check of compute2() (live whenever LIBINT2_ERI3_MAX_AM > LIBINT2_MAX_AM, as in CI);
    • the max_l check;
    • the limit reported by initialize();
    • the core evaluator rebuilt by set(BraKet).
  • set(BraKet)/set(Operator) resize the primitive data for the rank of the new braket. Before, initialize() resized it only when given max_nprim, so e.g. switching an Engine from 3 to 4 centers overran the buffer (found by the new set(BraKet) test under ASan). max_nprim() returns the value the Engine was sized for; it used to be derived from the size of the last computed shell pair.
  • compute2() evaluates the total derivative order (der + the operator's intrinsic order) at compile time instead of calling intrinsic_deriv_order() per primitive.

hartree-fock++ / CI

  • The validator accepted "2-body forces/hessian skipped" unconditionally. The features file now has the ERI max AM for each derivative order (LIBINT_ERI_MAX_AM_DERIVS), and the validator accepts a skip only if the kernels cannot handle the d shells of the basis.
  • The CI lane with LIBINT2_MAX_AM="2;2;1" generated 2nd-derivative ERIs up to p only. hartree-fock++ therefore skipped its Hessian, which was tested in no lane. The lane now generates them up to d (LIBINT2_ERI_MAX_AM="2;2;2").
  • api_basic_compile_test: the 2-center derivative checks also need the 2eri<d> kernels.
  • Fortran ERI test: the difference is measured relative to the largest element of the shell set. With 2nd derivatives over d shells, 2 of ~1400 elements differed by 5e-12 relative to themselves, 3e-15 in absolute terms; this is roundoff from differently ordered prim-data arithmetic.

Compiler

  • RKB fold relations: the symbolic sum leads with a +1 term, so the cross × cross components no longer multiply by −1.0. That multiply was also missing from the flop count.
  • The component-index range checks of the RKB/σ·p descriptors now run in all builds and check both bounds.
  • PairSwap::coupled ⇒ p1p2_swappable is a static_assert instead of asserts in the shell-set predicates.
  • SMultipole_1_1_int was outside the LIBINT_SUPPORT_ONEBODYINTS guard.
  • Stale comments, unused includes, a leftover flush.
  • RecurrenceRelation::is_alias(): an RR whose target is identical to its only child. handle_trivial_nodes() makes such a vertex refer to its child (when the child has no other parent and is not a target), and no code is generated or included for the RR. CR_11_Coulombσp_11 is one, so the coulomb_op kernels accumulate the derivative integrals directly instead of copying them first. In the configuration below, 19 3coulomb_op kernels change, CR_Coulombop_{0,1,2} are no longer generated, and every other generated file is unchanged.
  • Compiler core test: new DerivGaussV2 test (children of direct differentiation and of translational invariance, code shared by spectators but not by shells or directions, code-generating builds of both paths, CR_11_Coulombσp_11 is an alias). The test driver now exits nonzero if a test throws; it used to return 0 regardless.

Verified locally (macOS, Apple clang, gfortran 14) with the configuration of the modified CI lane (MAX_AM=2;2;1, ERI_MAX_AM=2;2;2, ENABLE_ERI=2, ERI3_MAX_AM=3;2, RKB on, RKB_ERI3_MAX_AM=3, ERI2 on, Fortran, Release):

  • check-libint2compiler passes 2/2 and check passes 17/17: 47 unit test cases, 134,570,582 assertions.
  • hartree-fock++ validates the 2-body Hessian; a faked "skipped" line is rejected.
  • The per-center section of the new lmax test is live in this configuration (3-center limit 3 > MAX_AM 2).
  • All unit tests also pass under AddressSanitizer (Debug headers against the generated library).
  • The full library was regenerated and rebuilt after the alias change, with stale generated files removed; check-libint2compiler and check pass again.

This PR was agent-generated and human-reviewed.

…stent lmax_exceeded

- compute1()/compute2() only asserted that the shells do not exceed the max_l
  of the Engine, hence a Release build overran the stack and scratch buffers
  sized for max_l; throw Engine::lmax_exceeded instead.
- lmax_exceeded thrown by initialize() reported the max AM + 1 as the limit,
  the one thrown by compute2() the max AM; report the max AM in both, and
  include the task and the limits in what().
- rkb_operators: the "folded" paragraph applies to the operators with one
  folded electron; opop_coulomb_opop already contains the -1 of i², as its
  prefactor column says.
- tests for the per-center checks of compute2(), the max_l check, the limit
  reported by initialize(), and the core evaluator rebuilt by set(BraKet).
…t do it; test the hessian in CI again

- the validator accepted "2-body forces/hessian skipped" whenever it saw it;
  it now reads the max AM of the ERI kernels for each derivative order from
  the features file (new LIBINT_ERI_MAX_AM_DERIVS) and accepts a skip only if
  that is below the AM of the basis.
- the CI lane with LIBINT2_MAX_AM="2;2;1" generated 2nd-derivative ERIs up
  to p only, so hartree-fock++ skipped its hessian, which then was not tested
  in any lane; generate them up to d (LIBINT2_ERI_MAX_AM="2;2;2").
- api_basic_compile_test: the 2-center derivative checks also need the
  2-center (2eri<d>) kernels.
- rkb_fold_make_children: lead the symbolic sum with a +1 term, so that the
  cross x cross components of opop_coulomb_opop no longer multiply by -1.0
  (which was also missing from the flop count).
- descriptors of the RKB and σ·p 1-body operators: check the component index
  (both bounds) in all builds.
- build_libint: PairSwap::coupled requires p1p2_swappable, check at compile
  time instead of asserting in the predicates; stale comments (translational
  invariance is handled by DerivGaussV2), unused includes, a leftover flush.
- master_ints_list: SMultipole_1_1_int was outside the
  LIBINT_SUPPORT_ONEBODYINTS guard of the list.
- comments: the reason for the DerivGaussV2TI label, "quaternions".
The C and Fortran tests compute the primitive data with differently ordered
arithmetic; with 2nd-derivative ERIs over d shells (now generated in the CI
lane with LIBINT2_ERI_MAX_AM="2;2;2") 2 of ~1400 elements, small differences
of large terms, differed by 5e-12 relative to themselves (3e-15 in absolute
terms). Measure the difference relative to the largest element of the shell
set instead.
…ll AM

The per-shell asserts against max_l ran before the new all-builds check,
so Debug builds aborted instead of throwing. Replace them with the throw
and drop the now-redundant check that followed the canonicalization.
@evaleev
evaleev force-pushed the fix/rkb-review-followups branch from 4bfc309 to d36ed43 Compare October 26, 2026 11:00
…tor)

initialize() resized primdata_ only when given max_nprim, so set(BraKet)
and set(Operator) kept the size for the old braket rank; e.g. switching
from a 3-center to a 4-center braket overran primdata_ (caught by the new
set(BraKet) test, crashing CI). The Engine now records max_nprim_, which
initialize() uses, and max_nprim() returns it instead of a value derived
from the size of the last computed ShellPair.
intrinsic_deriv_order() dispatches on the runtime operator, and compute2
called it in the loop over primitives; the operator and the derivative
order are template parameters of compute2, hence use a constexpr.
RecurrenceRelation::is_alias() (false by default) declares that the target
is identical, element by element and in the same layout, to the only child.
handle_trivial_nodes() then makes the vertex refer to the child, provided
the child has no other parent and is not a target, so no code is generated
for the RR; update_func_names() and ExtractRR skip referring vertices, so
neither is the RR's function included or generated.

CR_11_Coulombσp_11 (one derivative integral with unit coefficient per
component) is an alias: the 19 3coulomb_op prereq kernels of the RKB test
configuration accumulate the derivative integrals directly instead of
copying them first, and CR_Coulombop_{0,1,2} are no longer generated.
test9 checks the children of direct differentiation and of translational
invariance, that the generated code is shared by the spectators but not by
different shells or directions, builds shell sets that call both kinds of
DerivGaussV2 functions, and that CR_11_Coulombσp_11 is an alias. The test
driver returned 0 even if a test threw, hence could never fail.
@evaleev evaleev added this to the 2.14 milestone Oct 26, 2026
@evaleev
evaleev merged commit c77850a into master Oct 26, 2026
13 checks passed
@evaleev
evaleev deleted the fix/rkb-review-followups branch October 26, 2026 14:26
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.

1 participant