Skip to content

Make HelFEM consumable: split the library, install the diatomic headers, namespace the include root - #348

Merged
susilehtola merged 3 commits into
masterfrom
consumable-libraries
Sep 20, 2026
Merged

susilehtola merged 3 commits into
masterfrom
consumable-libraries

Conversation

@susilehtola

Copy link
Copy Markdown
Owner

Another project wants HelFEM's diatomic coordinate machinery and its Coulomb/exchange builders. It couldn't have them. This makes that work, and fixes two related problems found along the way.

The findings come from building an install tree and inventorying it, plus writing a standalone consumer against it — not from reading CMakeLists.txt.

1. The gate was all-or-nothing, and the dependency it forced was fictitious

add_subdirectory(src) sat inside if(HELFEM_BINARIES), so obtaining a diatomic TwoDBasis meant building twelve executables and hard-requiring HDF5 + libxc — both PUBLIC on helfem-common, hence inherited by the consumer.

The diatomic basis uses neither. Of ~25 translation units in helfem-common, exactly nine touch HDF5 or libxc (checkpoint, dftfuncs, dftgrid*, atomdb, sadatom/*). None is in the diatomic path, whose entire closure is clean:

general/scf_helpers.cpp  clean     general/gaunt.cpp               clean
general/gsz.cpp          clean     general/spherical_harmonics.cpp clean
general/timer.cpp        clean     diatomic/quadrature.cpp         clean

One library mixing basis machinery with DFT and checkpointing lets its heaviest member set everyone's requirements.

So: helfem-fem, built unconditionally, carrying the geometry machinery. src/ is entered always and gates only the HDF5/libxc half. helfem-common keeps the rest and links helfem-fem rather than duplicating it.

The atomic basis does not join it. general/model_potential.cpp uses libxc's XC_LDA_X for the SAP potential, and atomic/TwoDBasis.h includes it. Making that optional would let atomic follow; the exclusion and its reason are recorded in src/CMakeLists.txt rather than the split quietly shipping narrower than described.

2. No diatomic headers were installed

include/helfem/ shipped atomic/TwoDBasis.h and four general/ headers, nothing else — so even with HELFEM_BINARIES=ON and helfem-common linked, there was no basis.h to include. The atomic side got this treatment for libatomscf; diatomic never did. Added, preserving the source layout so the relative-include chain in basis.h resolves unchanged.

3. The header layout could shadow a system header

libhelfem installed 37 headers flat into include/ — among them math.h, types.h, grid.h — and handed consumers -I${prefix}/include. Since -I paths precede system paths, a consumer's #include <math.h> resolved to HelFEM's, which then includes <types.h> expecting its own.

Everything now lands under include/helfem/, and the 204 include lines naming those headers across 102 files were rewritten to <helfem/X.h>. include/ holds exactly one entry: helfem/. The bare $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}> entry goes with it — it existed so a stale helfem.h at the binary root could be found, which is the same shadowing inside the build tree.

What the consumer test caught

A standalone project — find_package + helfem::fem + <helfem/diatomic/basis.h>, with no HelFEM source directory on its include path — turned up three further packaging faults that reading the CMake would not have:

fault effect
helfemConfig.cmake required OpenOrbitalOptimizer unconditionally find_package(helfem) failed outright on a HELFEM_BINARIES=OFF install
helfem_otr exported unconditionally dangling OpenTrustRegion::opentrustregion in that same install
export set published helfem::helfem-fem EXPORT_NAME now gives the helfem::fem the in-tree alias promises, archive still libhelfem-fem.a

It is committed as examples/diatomic_consumer.cpp, next to the existing atomic example, so the packaging has a regression guard instead of living in a scratch directory.

Verification

HELFEM_BINARIES=OFF   build rc 0, no HDF5 / libxc in the configure
consumer              Nbf=101  |S|=3.40e+03  |H|=3.59e+02  |J|=3.05e+04  |K|=4.34e+03  OK
HELFEM_BINARIES=ON    build rc 0
ctest -L unit         9/9
ctest (full enabled)  exit 0 — no failures

Nbf=101 is what the in-tree diatomic tests report on the same grid, so the basis is genuinely constructed rather than trivially empty. The full suite matters here because the integration cases run real SCF calculations: if any of the 204 rewritten includes had picked up a different header, the energies would have moved.

🤖 Generated with Claude Code

susilehtola and others added 3 commits September 20, 2026 16:58
and stop dumping generic names at the include root

Three problems, found by asking what an external project actually sees rather
than by reading CMakeLists: an install tree was built and inventoried, and a
standalone consumer was written against it.

1. THE GATE WAS ALL-OR-NOTHING, AND THE DEPENDENCY IT FORCED WAS FICTITIOUS.

add_subdirectory(src) sat inside if(HELFEM_BINARIES), so obtaining a diatomic
TwoDBasis meant building twelve executables and hard-requiring HDF5 + libxc --
both PUBLIC on helfem-common, hence inherited. The diatomic basis uses
neither. Of ~25 translation units in helfem-common exactly nine touch HDF5 or
libxc (checkpoint, dftfuncs, dftgrid*, atomdb, sadatom/*); none is in the
diatomic path, whose whole closure (gaunt, spherical_harmonics, gsz, timer,
scf_helpers, quadrature) is clean. One library mixing basis machinery with DFT
and checkpointing lets its heaviest member set everyone's requirements.

So: helfem-fem, built unconditionally, carrying the geometry machinery, with
src/ entered always and gating only the HDF5/libxc half. helfem-common keeps
the rest and links helfem-fem rather than duplicating it.

The ATOMIC basis does not join it: general/model_potential.cpp uses libxc's
XC_LDA_X for the SAP potential and atomic/TwoDBasis.h includes it. Making that
optional would let atomic follow; it is noted in src/CMakeLists.txt rather
than quietly shipped as a narrower split than intended.

2. NO DIATOMIC HEADERS WERE INSTALLED.

include/helfem/ shipped atomic/TwoDBasis.h and four general/ headers and
nothing else, so even with HELFEM_BINARIES=ON and helfem-common linked there
was no basis.h to include. The atomic side got this treatment for libatomscf;
diatomic never did. Added, preserving the source layout so the relative
include chain in basis.h resolves unchanged.

3. THE HEADER LAYOUT COULD SHADOW A SYSTEM HEADER.

libhelfem installed 37 headers FLAT into include/ -- among them math.h,
types.h, grid.h -- and handed consumers -I${prefix}/include. Since -I paths
precede system paths, a consumer's #include <math.h> resolved to HelFEM's,
which then includes <types.h> expecting its own. Everything now lands under
include/helfem/, and the 204 include lines that named those headers across 102
files were rewritten to <helfem/X.h>. include/ holds exactly one entry:
helfem/. The bare $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}> entry is gone
with it -- it existed so a stale helfem.h at the binary root could be found,
which is the same shadowing in the build tree.

WHAT THE CONSUMER TEST CAUGHT

A standalone project (find_package + helfem::fem + <helfem/diatomic/basis.h>,
with no HelFEM source directory on its include path) turned up three further
packaging faults that reading the CMake would not have:

  - helfemConfig.cmake required OpenOrbitalOptimizer unconditionally, though
    only helfem-common uses it, so find_package failed outright on a
    HELFEM_BINARIES=OFF install.
  - helfem_otr was exported unconditionally, leaving
    OpenTrustRegion::opentrustregion dangling in that same install.
  - the export set would have published helfem::helfem-fem; EXPORT_NAME gives
    consumers the helfem::fem the in-tree alias promises, with the archive
    still libhelfem-fem.a.

It now builds a diatomic basis and calls coulomb()/exchange() against an
installed HELFEM_BINARIES=OFF tree:

    consumer: Nbf=101  |S|=3.40e+03  |H|=3.59e+02  |J|=3.05e+04  |K|=4.34e+03

Nbf=101 is what the in-tree diatomic tests report on the same grid, so the
basis is built rather than trivially empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The atomic example next to it shows the radial FEM export; this one shows
the other half a consumer needs, and doubles as the regression guard for
the packaging. It compiles against an INSTALLED tree only -- no HelFEM
source directory on the include path -- which is what makes it a test of
the install layout rather than of the build tree, and is why it caught
three faults the in-tree build could not see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
install(EXPORT) refuses a target that appears more than once, and the new
helfem-fem install block listed legendre alongside the existing
helfem-common one:

    CMake Error: install(EXPORT "helfemTargets" ...) includes target
    "legendre" more than once in the export set.

CMake still writes usable build files after that error, so the build and
the whole test suite ran green on a tree whose configure had exited 1. I
missed it locally by discarding the configure output and separating the
commands with `;` rather than `&&` -- CI caught it as twelve missing
binaries.

Verified with every exit code checked this time: configure and build rc 0
in both HELFEM_BINARIES=ON and OFF, the executables present under
objdir/src, and the installed-tree consumer building and running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@susilehtola
susilehtola merged commit a2f57f6 into master Sep 20, 2026
1 check passed
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