From 0ac01ee246abdede77ca851e8550877d0672752e Mon Sep 17 00:00:00 2001 From: Bowen Zhang Date: Fri, 17 Apr 2026 21:59:42 +0800 Subject: [PATCH 1/2] feat(calculator): add hyper-xtb NN-xTB back-end ("hyper-crest") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New back-end `nnxtb` calls hyper-xtb's hxtb_nnxtb_* C API (persistent engine = MACE weights loaded once per crest invocation) via a Fortran iso_c_binding wrapper. Architecture mirrors the existing tblite_api plumbing so the rest of CREST (optimizer, metadynamics, CREGEN, etc.) sees it as just another engine. - src/calculator/hyperxtb_api.F90: new module. hyperxtb_data owns the HxtbNNxtbEngine* and the MACE model path. hyperxtb_setup is called on the first energy/gradient request; hyperxtb_singlepoint is the per-frame dispatch (flattens mol%xyz/at → zs+xyz buffers, calls hxtb_nnxtb_compute, unflattens grad). - src/calculator/api_engrad.f90: hyperxtb_engrad — same loadnew/OMP critical pattern as tblite_engrad. - src/calculator/calculator.F90: new jobtype%hyperxtb case in the per-calculation dispatch switch. - src/calculator/calc_type.f90: jobtype enum + jobdescription entry, hyperxtb / hyperxtb_model fields on calculation_settings, deallocate paths, and create_calclevel_shortcut aliases (nnxtb, hyperxtb). - src/parsing/parse_calcdata.f90: method=nnxtb|hyperxtb accepted in TOML; mace_model/hyperxtb_model/nnxtb_model key sets the .mxtb path. - CMakeLists.txt + config/CMakeLists.txt: WITH_HYPERXTB option (default OFF). When ON, requires -DHYPERXTB_ROOT and -DHYPER_MACE_ROOT; builds an hyperxtb::hyperxtb INTERFACE target carrying include/libs and defines WITH_HYPERXTB so the Fortran source compiles with the real bindings (stub error-stops otherwise). - docs/hyper_crest.md: build + usage walkthrough. Default build (WITH_HYPERXTB=OFF) is unchanged. --- CMakeLists.txt | 48 +++++++ config/CMakeLists.txt | 17 +-- docs/hyper_crest.md | 76 +++++++++++ src/calculator/CMakeLists.txt | 1 + src/calculator/api_engrad.f90 | 44 ++++++ src/calculator/calc_type.f90 | 24 +++- src/calculator/calculator.F90 | 4 + src/calculator/hyperxtb_api.F90 | 228 ++++++++++++++++++++++++++++++++ src/calculator/meson.build | 3 +- src/parsing/parse_calcdata.f90 | 6 + 10 files changed, 440 insertions(+), 11 deletions(-) create mode 100644 docs/hyper_crest.md create mode 100644 src/calculator/hyperxtb_api.F90 diff --git a/CMakeLists.txt b/CMakeLists.txt index 72d773cc..c9484410 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,51 @@ if(NOT TARGET "tblite::tblite" AND WITH_TBLITE) add_compile_definitions(WITH_TBLITE) endif() +# hyper-xtb + hyper-mace (NN-xTB backend). +# This is the new calculator back-end added by hyper-crest. HYPERXTB_ROOT +# must point at a built hyper-xtb tree (feat/nnxtb-capi branch or later) +# configured with -DHYPERXTB_WITH_NNXTB=ON. HYPER_MACE_ROOT is the built +# hyper-mace tree (xtb-implementation branch) exporting libmace.a. +# +# These are carried as a small interface target so the object/static/shared +# downstream targets can link against them uniformly. +if(WITH_HYPERXTB) + if(NOT HYPERXTB_ROOT) + message(FATAL_ERROR + "WITH_HYPERXTB=ON requires -DHYPERXTB_ROOT=/path/to/hyper-xtb (built with -DHYPERXTB_WITH_NNXTB=ON)") + endif() + if(NOT HYPER_MACE_ROOT) + message(FATAL_ERROR + "WITH_HYPERXTB=ON requires -DHYPER_MACE_ROOT=/path/to/hyper-mace (built libmace.a)") + endif() + find_library(HYPERXTB_LIB hyperxtb_core + HINTS "${HYPERXTB_ROOT}/build" "${HYPERXTB_ROOT}/build_linux" + NO_DEFAULT_PATH) + if(NOT HYPERXTB_LIB) + message(FATAL_ERROR + "libhyperxtb_core not found under ${HYPERXTB_ROOT}/build or build_linux") + endif() + find_library(HYPER_MACE_LIB mace + HINTS "${HYPER_MACE_ROOT}/build" "${HYPER_MACE_ROOT}/build_linux" + NO_DEFAULT_PATH) + if(NOT HYPER_MACE_LIB) + message(FATAL_ERROR + "libmace not found under ${HYPER_MACE_ROOT}/build or build_linux") + endif() + + add_library(hyperxtb::hyperxtb INTERFACE IMPORTED) + target_include_directories(hyperxtb::hyperxtb INTERFACE + "${HYPERXTB_ROOT}/include" + "${HYPER_MACE_ROOT}/include") + # hyperxtb_core references MACE symbols from its nnxtb_capi.o, so libmace + # must come after it on the link line. C++ runtime needed for the + # hyper-xtb TU that's linked in. + target_link_libraries(hyperxtb::hyperxtb INTERFACE + "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" stdc++) + add_compile_definitions(WITH_HYPERXTB) + message(STATUS "hyper-xtb NN-xTB backend: ON (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT})") +endif() + # GFN-FF if(NOT TARGET "gfnff::gfnff" AND WITH_GFNFF) find_package("gfnff" REQUIRED) @@ -153,6 +198,7 @@ if(WITH_OBJECT AND NOT STATICBUILD) $<$:pvol::pvol> $<$:toml-f::toml-f> $<$:lwoniom::lwoniom> + $<$:hyperxtb::hyperxtb> $<$:OpenMP::OpenMP_Fortran> ) @@ -204,6 +250,7 @@ target_link_libraries( $<$:pvol::pvol> $<$:toml-f::toml-f> $<$:lwoniom::lwoniom> + $<$:hyperxtb::hyperxtb> $<$:-static> ) @@ -249,6 +296,7 @@ if (WITH_OBJECT AND NOT STATICBUILD) $<$:pvol::pvol> $<$:toml-f::toml-f> $<$:lwoniom::lwoniom> + $<$:hyperxtb::hyperxtb> ) set_target_properties( diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index 7800906a..27f43672 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -29,14 +29,15 @@ install( ) # Options for enabling or disabling features -option(WITH_OpenMP "Enable OpenMP support" TRUE) -option(WITH_TBLITE "Enable support for tblite" TRUE) -option(WITH_TOMLF "Enable support for toml-f" TRUE) -option(WITH_GFN0 "Enable support for GFN0-xTB" TRUE) -option(WITH_GFNFF "Enable support for GFN-FF" TRUE) -option(WITH_LIBPVOL "Enable support for LIBPVOL" TRUE) -option(WITH_LWONIOM "Enable support for lwONIOM" TRUE) -option(WITH_TESTS "Enable unit tests" TRUE) +option(WITH_OpenMP "Enable OpenMP support" TRUE) +option(WITH_TBLITE "Enable support for tblite" TRUE) +option(WITH_TOMLF "Enable support for toml-f" TRUE) +option(WITH_GFN0 "Enable support for GFN0-xTB" TRUE) +option(WITH_GFNFF "Enable support for GFN-FF" TRUE) +option(WITH_LIBPVOL "Enable support for LIBPVOL" TRUE) +option(WITH_LWONIOM "Enable support for lwONIOM" TRUE) +option(WITH_HYPERXTB "Enable hyper-xtb NN-xTB back-end (requires HYPERXTB_ROOT and HYPER_MACE_ROOT)" FALSE) +option(WITH_TESTS "Enable unit tests" TRUE) option(STATICBUILD "Attempt to link everything statically" FALSE) set(PYTHON_BINDINGS OFF CACHE BOOL "Disable python bindings for submodules that have them" FORCE) diff --git a/docs/hyper_crest.md b/docs/hyper_crest.md new file mode 100644 index 00000000..9cf26ad6 --- /dev/null +++ b/docs/hyper_crest.md @@ -0,0 +1,76 @@ +# hyper-crest: CREST with hyper-xtb NN-xTB + +This fork adds `nnxtb` as a first-class CREST back-end, powered by +[hyper-xtb](https://github.com/talo/hyper-xtb) (NN-xTB branch) and +[hyper-mace](https://github.com/talo/hyper-mace) (ScaleShiftMACExTB). +The goal is CREST-speed conformer/rotamer sampling at DFT-level +accuracy by replacing Grimme xTB with a MACE-corrected GFN2-xTB. + +## Build + +You need built trees of hyper-xtb (`feat/nnxtb-capi` or later — the C API +is `include/hyperxtb_capi.h`'s `hxtb_nnxtb_*` entry points) and hyper-mace +(`xtb-implementation` branch). Both produce static libraries picked up +via `HYPERXTB_ROOT` / `HYPER_MACE_ROOT`. + +```sh +# 1. hyper-mace — builds libmace.a +cd $HYPER_MACE_ROOT && cmake -B build . && cmake --build build -j + +# 2. hyper-xtb with NN-xTB capi — builds libhyperxtb_core.a +cd $HYPERXTB_ROOT && cmake -B build . \ + -DHYPERXTB_WITH_NNXTB=ON \ + -DHYPER_MACE_ROOT=$HYPER_MACE_ROOT +cmake --build build -j + +# 3. hyper-crest (this tree) with WITH_HYPERXTB +cd $HYPER_CREST_ROOT && cmake -B build . \ + -DWITH_HYPERXTB=ON \ + -DHYPERXTB_ROOT=$HYPERXTB_ROOT \ + -DHYPER_MACE_ROOT=$HYPER_MACE_ROOT +cmake --build build -j +``` + +## Use + +Select the new back-end via the TOML `method = "nnxtb"` and point at the +MACE model binary: + +```toml +# input.toml +input = "mol.xyz" +runtype = "imtd-gc" + +[calculation.level] +method = "nnxtb" +mace_model = "/path/to/ScaleShiftMACExTB.mxtb" +chrg = 0 +``` + +```sh +crest input.toml +``` + +Also accepted via `create_calclevel_shortcut`: `crest mol.xyz --nnxtb` +(with the model path provided via TOML or future CLI flag). + +## What changes under the hood + +`src/calculator/hyperxtb_api.F90` binds to the hyper-xtb `hxtb_nnxtb_*` +C API via `iso_c_binding`. A single `HxtbNNxtbEngine*` is created during +`hyperxtb_setup` on the first energy/gradient request and reused for +every subsequent CREST evaluation in that run — MACE weights are loaded +exactly once per `crest` invocation, as required by the deployment +constraint for NN-xTB. + +Per-call flow inside `hxtb_nnxtb_compute`: +1. Build all-pairs neighbor list at MACE cutoff (Å) +2. `mace_xtb_compute` → element/global parameter deltas +3. Clamp deltas to `[-0.5, 0.5]` and run `nn_xtb_forward` + (GFN2-xTB SCF with deltas applied) +4. `mace_xtb_backward` on `(d_elem, d_glob)` → Cartesian chain-rule +5. Combine xTB and MACE gradient contributions (Hartree/Bohr) in place + +CREST sees a single dE/dR vector with the same sign convention as the +existing `tblite` and `gfn0` back-ends, so no downstream changes are +needed (optimizer, metadynamics, CREGEN, ensembles all just work). diff --git a/src/calculator/CMakeLists.txt b/src/calculator/CMakeLists.txt index 13e1cd2e..3a827c01 100644 --- a/src/calculator/CMakeLists.txt +++ b/src/calculator/CMakeLists.txt @@ -28,6 +28,7 @@ list(APPEND srcs "${dir}/oniom_hessian.F90" "${dir}/nonadiabatic.f90" "${dir}/tblite_api.F90" + "${dir}/hyperxtb_api.F90" "${dir}/api_helpers.F90" "${dir}/api_engrad.f90" "${dir}/gradreader.f90" diff --git a/src/calculator/api_engrad.f90 b/src/calculator/api_engrad.f90 index 2db81965..01b450e9 100644 --- a/src/calculator/api_engrad.f90 +++ b/src/calculator/api_engrad.f90 @@ -34,6 +34,7 @@ module api_engrad use gfn0_api use gfnff_api use libpvol_api + use hyperxtb_api use lj implicit none !>--- private module variables and parameters @@ -43,6 +44,7 @@ module api_engrad public :: gfn0_engrad,gfn0occ_engrad public :: gfnff_engrad public :: libpvol_engrad + public :: hyperxtb_engrad public :: lj_engrad !> RE-EXPORT !=========================================================================================! @@ -116,6 +118,48 @@ subroutine tblite_engrad(mol,calc,energy,grad,iostatus) return end subroutine tblite_engrad +!========================================================================================! + + subroutine hyperxtb_engrad(mol,calc,energy,grad,iostatus) +!********************************************************** +!* Interface singlepoint call between CREST and hyper-xtb's +!* NN-xTB engine. MACE weights are loaded on the first call +!* and reused for every subsequent evaluation — the whole +!* reason hyper-crest exists. +!********************************************************** + implicit none + type(coord) :: mol + type(calculation_settings) :: calc + + real(wp),intent(inout) :: energy + real(wp),intent(inout) :: grad(3,mol%nat) + integer,intent(out) :: iostatus + + iostatus = 0 + +!>--- one-time engine load. Mirrors the tblite_init / loadnew pattern so +!> the same bundle can be reused across conformers in a CREST run. + !$omp critical + if (.not.allocated(calc%hyperxtb)) then + allocate (calc%hyperxtb) + end if + if (.not.calc%hyperxtb%loaded) then + if (.not.allocated(calc%hyperxtb%mace_model_path) & + & .and. allocated(calc%hyperxtb_model)) then + calc%hyperxtb%mace_model_path = calc%hyperxtb_model + end if + call hyperxtb_setup(mol,calc%chrg,calc%hyperxtb) + end if + !$omp end critical + +!>--- engrad call + call initsignal() + call hyperxtb_singlepoint(mol,calc%chrg,calc%hyperxtb,energy,grad,iostatus) + if (iostatus /= 0) return + + return + end subroutine hyperxtb_engrad + !========================================================================================! subroutine gfn0_engrad(mol,calc,g0calc,energy,grad,iostatus) diff --git a/src/calculator/calc_type.f90 b/src/calculator/calc_type.f90 index 3b61bc49..ad846c1f 100644 --- a/src/calculator/calc_type.f90 +++ b/src/calculator/calc_type.f90 @@ -26,6 +26,7 @@ module calc_type use gfn0_api use gfnff_api,only:gfnff_data use libpvol_api,only:libpvol_calculator + use hyperxtb_api,only:hyperxtb_data,hyperxtb_cleanup !>--- other types use orca_type use lwoniom_module @@ -49,10 +50,11 @@ module calc_type integer :: gfnff = 9 integer :: libpvol = 10 integer :: lj = 11 + integer :: hyperxtb = 12 end type enum_jobtype type(enum_jobtype), parameter,public :: jobtype = enum_jobtype() - character(len=45),parameter,private :: jobdescription(12) = [ & + character(len=45),parameter,private :: jobdescription(13) = [ & & 'Unknown calculation type ', & & 'xTB calculation via external binary ', & & 'Generic script execution ', & @@ -64,7 +66,8 @@ module calc_type & 'GFN0*-xTB calculation via GFN0 lib ', & & 'GFN-FF calculation via GFNFF lib ', & & 'external pressure calculation via libpvol ', & - & 'Lennard-Jones potential calculation ' ] + & 'Lennard-Jones potential calculation ', & + & 'NN-xTB via hyper-xtb + hyper-mace ' ] !&> !=========================================================================================! @@ -160,6 +163,11 @@ module calc_type !>--- GFN-FF data type(gfnff_data),allocatable :: ff_dat +!>--- hyper-xtb (NN-xTB) data + type(hyperxtb_data),allocatable :: hyperxtb + !> Path to the ScaleShiftMACExTB `.mxtb` model file. User-supplied. + character(len=:),allocatable :: hyperxtb_model + !>--- libpvol data integer :: pvmodel = 1 !> libpvol model type (0=XHCFF, 1=PV) integer :: ngrid = 1202 !> lebedev grid points per atom @@ -357,6 +365,10 @@ subroutine calculation_deallocate_params(self) if(allocated(self%calcs(i)%g0calc)) deallocate(self%calcs(i)%g0calc) if(allocated(self%calcs(i)%ff_dat)) deallocate(self%calcs(i)%ff_dat) if(allocated(self%calcs(i)%libpvol)) deallocate(self%calcs(i)%libpvol) + if(allocated(self%calcs(i)%hyperxtb)) then + call hyperxtb_cleanup(self%calcs(i)%hyperxtb) + deallocate(self%calcs(i)%hyperxtb) + end if end do end if end subroutine calculation_deallocate_params @@ -918,6 +930,11 @@ subroutine calculation_settings_deallocate(self) if (allocated(self%g0calc)) deallocate (self%g0calc) if (allocated(self%ff_dat)) deallocate (self%ff_dat) if (allocated(self%libpvol)) deallocate (self%libpvol) + if (allocated(self%hyperxtb)) then + call hyperxtb_cleanup(self%hyperxtb) + deallocate (self%hyperxtb) + end if + if (allocated(self%hyperxtb_model)) deallocate (self%hyperxtb_model) self%id = 0 self%prch = stdout @@ -1229,6 +1246,9 @@ subroutine create_calclevel_shortcut(self,levelstring) case ('generic') self%id = jobtype%generic + case ('nnxtb','--nnxtb','hyperxtb','--hyperxtb') + self%id = jobtype%hyperxtb + end select call self%autocomplete(self%id) end subroutine create_calclevel_shortcut diff --git a/src/calculator/calculator.F90 b/src/calculator/calculator.F90 index 85e02162..28afd189 100644 --- a/src/calculator/calculator.F90 +++ b/src/calculator/calculator.F90 @@ -354,6 +354,10 @@ subroutine potential_core(molptr,calc,id,iostatus) !> GFN-FF api call gfnff_engrad(molptr,calc%calcs(id),calc%etmp(id),calc%grdtmp(:,1:pnat,id),iostatus) + case (jobtype%hyperxtb) + !> hyper-xtb NN-xTB api (MACE weights loaded once, reused here) + call hyperxtb_engrad(molptr,calc%calcs(id),calc%etmp(id),calc%grdtmp(:,1:pnat,id),iostatus) + case (jobtype%libpvol) !> libpvol call libpvol_engrad(molptr,calc%calcs(id),calc%etmp(id),calc%grdtmp(:,1:pnat,id),iostatus) diff --git a/src/calculator/hyperxtb_api.F90 b/src/calculator/hyperxtb_api.F90 new file mode 100644 index 00000000..4c93c65a --- /dev/null +++ b/src/calculator/hyperxtb_api.F90 @@ -0,0 +1,228 @@ +!================================================================================! +! This file is part of crest (hyper-crest fork). +! +! crest is free software: you can redistribute it and/or modify it under the +! terms of the GNU Lesser General Public License as published by the Free +! Software Foundation, either version 3 of the License, or (at your option) +! any later version. +!================================================================================! + +!====================================================! +! module hyperxtb_api +! Interface between CREST and the hyper-xtb NN-xTB +! persistent-engine C API (hxtb_nnxtb_*). +! +! A single HxtbNNxtbEngine is owned by hyperxtb_data +! and lives for the full sampling run, so the MACE +! weight-load cost is paid exactly once and every +! conformer evaluation reuses the same engine. +!====================================================! +module hyperxtb_api + use iso_fortran_env,only:wp => real64,stdout => output_unit +#ifdef WITH_HYPERXTB + use iso_c_binding,only:c_ptr,c_int,c_double,c_char,c_null_ptr,c_null_char, & + & c_associated +#endif + use strucrd,only:coord + implicit none + private + +!>--- hyper-xtb calculator bundle + type :: hyperxtb_data +#ifdef WITH_HYPERXTB + type(c_ptr) :: engine = c_null_ptr !> HxtbNNxtbEngine* (opaque handle) +#else + integer :: engine = 0 +#endif + !> Path to the ScaleShiftMACExTB `.mxtb` model file. + !> Must be set before calling hyperxtb_setup. + character(len=:),allocatable :: mace_model_path + !> Whether setup has been completed on this bundle. + logical :: loaded = .false. + end type hyperxtb_data + + public :: hyperxtb_data + public :: hyperxtb_setup + public :: hyperxtb_singlepoint + public :: hyperxtb_cleanup + +#ifdef WITH_HYPERXTB +!>--- ISO_C_BINDING to hyper-xtb C API (see include/hyperxtb_capi.h). + interface + function c_hxtb_nnxtb_init(mace_model_path) & + & bind(C,name="hxtb_nnxtb_init") result(eng) + import :: c_ptr,c_char + character(kind=c_char),dimension(*),intent(in) :: mace_model_path + type(c_ptr) :: eng + end function c_hxtb_nnxtb_init + + function c_hxtb_nnxtb_compute(eng,atoms_z,atoms_xyz,n_atoms,charge, & + & do_gradient,out_energy,out_gradient) & + & bind(C,name="hxtb_nnxtb_compute") result(rc) + import :: c_ptr,c_int,c_double + type(c_ptr),value :: eng + integer(c_int),dimension(*),intent(in) :: atoms_z + real(c_double),dimension(*),intent(in) :: atoms_xyz + integer(c_int),value :: n_atoms + integer(c_int),value :: charge + integer(c_int),value :: do_gradient + real(c_double),intent(out) :: out_energy + real(c_double),dimension(*),intent(inout) :: out_gradient + integer(c_int) :: rc + end function c_hxtb_nnxtb_compute + + subroutine c_hxtb_nnxtb_free(eng) bind(C,name="hxtb_nnxtb_free") + import :: c_ptr + type(c_ptr),value :: eng + end subroutine c_hxtb_nnxtb_free + end interface +#endif + +!========================================================================================! +!========================================================================================! +contains !> MODULE PROCEDURES START HERE +!========================================================================================! +!========================================================================================! + + subroutine hyperxtb_setup(mol,chrg,hxtb) +!***************************************************************** +!* Initialise the persistent NN-xTB engine. Called once per +!* calculation bundle; subsequent hyperxtb_singlepoint calls reuse +!* the loaded MACE weights. +!***************************************************************** + implicit none + type(coord),intent(in) :: mol + integer,intent(in) :: chrg + type(hyperxtb_data),intent(inout) :: hxtb +#ifdef WITH_HYPERXTB + integer :: i + character(kind=c_char),allocatable :: cpath(:) + + !> `mol` and `chrg` are part of the uniform setup signature used by every + !> CREST back-end (tblite_setup, gfn0_setup, ...). nn_xtb does not need + !> them at init time — charge is passed per-compute, and geometry is not + !> yet meaningful — but we keep the signature for symmetry. + if (mol%nat < 1) then + write (stdout,'(a)') 'ERROR: hyperxtb_setup received empty structure' + error stop + end if + + if (.not.allocated(hxtb%mace_model_path)) then + write (stdout,'(a)') 'ERROR: hyperxtb_setup called without mace_model_path set' + error stop + end if + + !> Build null-terminated C string for the C API. + allocate(cpath(len_trim(hxtb%mace_model_path)+1)) + do i = 1,len_trim(hxtb%mace_model_path) + cpath(i) = hxtb%mace_model_path(i:i) + end do + cpath(len_trim(hxtb%mace_model_path)+1) = c_null_char + + hxtb%engine = c_hxtb_nnxtb_init(cpath) + deallocate(cpath) + + if (.not.c_associated(hxtb%engine)) then + write (stdout,'(a,a)') 'ERROR: hxtb_nnxtb_init failed for MACE model ', & + & hxtb%mace_model_path + error stop + end if + + hxtb%loaded = .true. +#else + write (stdout,'(a)') 'ERROR: compiled without hyper-xtb support.' + write (stdout,'(a)') ' Rebuild with -DWITH_HYPERXTB=ON -DHYPERXTB_ROOT=... -DHYPER_MACE_ROOT=...' + error stop +#endif + end subroutine hyperxtb_setup + +!========================================================================================! + + subroutine hyperxtb_singlepoint(mol,chrg,hxtb,energy,grad,iostatus) +!***************************************************************** +!* Run a single NN-xTB energy + gradient call through the persistent +!* engine. grad is dE/dR in Hartree/Bohr, matching the rest of the +!* CREST calculator conventions. +!***************************************************************** + implicit none + type(coord),intent(in) :: mol + integer,intent(in) :: chrg + type(hyperxtb_data),intent(inout) :: hxtb + real(wp),intent(inout) :: energy + real(wp),intent(inout) :: grad(3,mol%nat) + integer,intent(out) :: iostatus +#ifdef WITH_HYPERXTB + integer(c_int),allocatable :: zs(:) + real(c_double),allocatable :: xyz_flat(:) + real(c_double),allocatable :: grad_flat(:) + real(c_double) :: e + integer(c_int) :: rc,n_c,chrg_c + integer :: i,n + + iostatus = 0 + + if (.not.hxtb%loaded) then + write (stdout,'(a)') 'ERROR: hyperxtb_singlepoint called before hyperxtb_setup' + iostatus = 1 + return + end if + + n = mol%nat + n_c = int(n,c_int) + chrg_c = int(chrg,c_int) + + allocate(zs(n)) + allocate(xyz_flat(3*n)) + allocate(grad_flat(3*n)) + do i = 1,n + zs(i) = int(mol%at(i),c_int) + xyz_flat(3*(i-1)+1) = real(mol%xyz(1,i),c_double) + xyz_flat(3*(i-1)+2) = real(mol%xyz(2,i),c_double) + xyz_flat(3*(i-1)+3) = real(mol%xyz(3,i),c_double) + end do + + rc = c_hxtb_nnxtb_compute(hxtb%engine,zs,xyz_flat,n_c,chrg_c, & + & 1_c_int,e,grad_flat) + iostatus = int(rc) + + if (iostatus == 0) then + energy = real(e,wp) + do i = 1,n + grad(1,i) = real(grad_flat(3*(i-1)+1),wp) + grad(2,i) = real(grad_flat(3*(i-1)+2),wp) + grad(3,i) = real(grad_flat(3*(i-1)+3),wp) + end do + end if + + deallocate(zs,xyz_flat,grad_flat) +#else + iostatus = 1 + energy = 0.0_wp + grad = 0.0_wp + write (stdout,'(a)') 'ERROR: compiled without hyper-xtb support' + error stop +#endif + end subroutine hyperxtb_singlepoint + +!========================================================================================! + + subroutine hyperxtb_cleanup(hxtb) +!***************************************************************** +!* Release the MACE model and any engine-held scratch. +!***************************************************************** + implicit none + type(hyperxtb_data),intent(inout) :: hxtb +#ifdef WITH_HYPERXTB + if (c_associated(hxtb%engine)) then + call c_hxtb_nnxtb_free(hxtb%engine) + hxtb%engine = c_null_ptr + end if + hxtb%loaded = .false. +#else + hxtb%loaded = .false. +#endif + end subroutine hyperxtb_cleanup + +!========================================================================================! +!========================================================================================! +end module hyperxtb_api diff --git a/src/calculator/meson.build b/src/calculator/meson.build index da3d2598..84e60cc8 100644 --- a/src/calculator/meson.build +++ b/src/calculator/meson.build @@ -26,8 +26,9 @@ srcs += files( 'oniom_hessian.F90', 'nonadiabatic.f90', 'tblite_api.F90', + 'hyperxtb_api.F90', 'api_helpers.F90', - 'api_engrad.f90', + 'api_engrad.f90', 'gradreader.f90', 'libpvol.F90', 'xtb_sc.f90', diff --git a/src/parsing/parse_calcdata.f90 b/src/parsing/parse_calcdata.f90 index 00768671..9fe676fc 100644 --- a/src/parsing/parse_calcdata.f90 +++ b/src/parsing/parse_calcdata.f90 @@ -250,6 +250,8 @@ subroutine parse_setting_auto(env,job,kv,rd) job%id = jobtype%unknown case ('lj','lennard-jones') job%id = jobtype%lj + case ('nnxtb','hyperxtb','nn-xtb','hyper-xtb') + job%id = jobtype%hyperxtb case default job%id = jobtype%unknown !>--- keyword was recognized, but invalid argument supplied @@ -303,6 +305,10 @@ subroutine parse_setting_auto(env,job,kv,rd) case ('efile') job%efile = kv%value_c + case ('mace_model','hyperxtb_model','nnxtb_model') + !> Path to the ScaleShiftMACExTB .mxtb file consumed by hxtb_nnxtb_init. + job%hyperxtb_model = kv%value_c + case ('tblite_level','tblite_hamiltonian') select case (kv%value_c) case ('gfn2','gfn2-xtb') From eda0a7e270660a4a1711ffde3cd40cdec380b858 Mon Sep 17 00:00:00 2001 From: Bowen Zhang Date: Mon, 20 Apr 2026 14:05:01 +0800 Subject: [PATCH 2/2] fix(cmake): link libmace_cuda + cudart when hyper-mace has CUDA support Downstream libcrest (static + shared) and the crest-exe target were failing to link with undefined references to cudaMalloc / cudaFree / cudaStreamCreate / mace_xtb_compute_cuda when hyper-xtb was built against a CUDA hyper-mace, because libhyperxtb_core.a now carries GPU code from the new src/nnxtb_capi_cuda.cu TU (via hyper-xtb#2/feat/nnxtb-capi). Probe for libmace_cuda alongside libmace, pull in CUDA::cudart when present, and wrap the three static archives plus -lgomp/-lpthread in a --start-group/--end-group so the symbol ordering is stable regardless of which sub-archive the linker walks first. --- CMakeLists.txt | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c9484410..bee7edb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,18 +121,43 @@ if(WITH_HYPERXTB) message(FATAL_ERROR "libmace not found under ${HYPER_MACE_ROOT}/build or build_linux") endif() + # libmace_cuda is optional — only present when hyper-mace was built with + # MACE_USE_CUDA=ON. When it is, the hxtb_nnxtb_* C API is compiled with + # HYPERXTB_WITH_NNXTB_CUDA and MACE forward/backward run on the device. + # libhyperxtb_core.a then references mace_cuda + cudart symbols, so we + # have to pick them up on the crest link line. + find_library(HYPER_MACE_CUDA_LIB mace_cuda + HINTS "${HYPER_MACE_ROOT}/build" "${HYPER_MACE_ROOT}/build_linux" + NO_DEFAULT_PATH) add_library(hyperxtb::hyperxtb INTERFACE IMPORTED) target_include_directories(hyperxtb::hyperxtb INTERFACE "${HYPERXTB_ROOT}/include" "${HYPER_MACE_ROOT}/include") # hyperxtb_core references MACE symbols from its nnxtb_capi.o, so libmace - # must come after it on the link line. C++ runtime needed for the - # hyper-xtb TU that's linked in. - target_link_libraries(hyperxtb::hyperxtb INTERFACE - "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" stdc++) + # must come after it on the link line. Wrap in --start-group/--end-group + # so static-lib symbol ordering doesn't matter, and append -lgomp/-lpthread + # for libmace's OpenMP references. C++ runtime needed for the hyper-xtb + # TU that's linked in. + if(HYPER_MACE_CUDA_LIB) + find_package(CUDAToolkit REQUIRED) + target_link_libraries(hyperxtb::hyperxtb INTERFACE + -Wl,--start-group + "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" "${HYPER_MACE_CUDA_LIB}" + gomp pthread + -Wl,--end-group + CUDA::cudart stdc++) + message(STATUS "hyper-xtb NN-xTB backend: ON + CUDA (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT})") + else() + target_link_libraries(hyperxtb::hyperxtb INTERFACE + -Wl,--start-group + "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" + gomp pthread + -Wl,--end-group + stdc++) + message(STATUS "hyper-xtb NN-xTB backend: ON (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT}, CPU-only)") + endif() add_compile_definitions(WITH_HYPERXTB) - message(STATUS "hyper-xtb NN-xTB backend: ON (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT})") endif() # GFN-FF