From 5e36fbe30cb4bb378835f4ae74abc6e595065b31 Mon Sep 17 00:00:00 2001 From: Daniel Wiczew Date: Tue, 20 Jan 2026 14:53:12 +0100 Subject: [PATCH 1/2] Fix L-ANC failures with $fix and fragmented Hessian - For GFN-FF large systems (N>500) ensure nvar accounts for fixset so L-ANC no longer aborts with k< --- cmake/modules/xtb-utils.cmake | 9 +++++++++ src/relaxation_engine.f90 | 27 ++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/cmake/modules/xtb-utils.cmake b/cmake/modules/xtb-utils.cmake index 0ad65c657..7eddc2955 100644 --- a/cmake/modules/xtb-utils.cmake +++ b/cmake/modules/xtb-utils.cmake @@ -25,6 +25,15 @@ macro( string(TOLOWER "${package}" _pkg_lc) string(TOUPPER "${package}" _pkg_uc) +# If the dependency was already added by another subproject (e.g., via +# FetchContent) it might only provide the plain target `${package}` without the +# namespaced alias `${package}::${package}` expected by xTB. Create the alias +# early to avoid re-adding the project and triggering duplicate-target errors. +if(TARGET "${package}" AND NOT TARGET "${package}::${package}") + add_library("${package}::${package}" INTERFACE IMPORTED) + target_link_libraries("${package}::${package}" INTERFACE "${package}") +endif() + # iterate through all methods foreach(method ${methods}) diff --git a/src/relaxation_engine.f90 b/src/relaxation_engine.f90 index 282ec2106..42af3ca33 100644 --- a/src/relaxation_engine.f90 +++ b/src/relaxation_engine.f90 @@ -572,18 +572,17 @@ subroutine l_ancopt & & gnorm=norm2(gradient), number=iter) endif - ! different DOF in case of frag hess + ! determine degrees of freedom nat3 = 3 * mol%n if (fragmented_hessian) then nvar = nat3 else nvar = nat3 - 6 - if(linear) nvar = nat3 - 5 - - if(fixset%n.gt.0) then ! exact fixing - nvar=nat3-3*fixset%n-3 - if(nvar.le.0) nvar=1 - endif + if (linear) nvar = nat3 - 5 + end if + if (fixset%n.gt.0) then ! exact fixing + nvar = nat3 - 3*fixset%n - 3 + if (nvar.le.0) nvar = 1 end if allocate( hdiag(nvar), source = 0.0_wp ) @@ -714,6 +713,20 @@ subroutine l_ancopt & if (k.ne.nvar) thr=thr*0.1_wp enddo + + ! If we could not collect enough modes (e.g., disconnected fragments can + ! yield many exactly-zero eigenvalues in the model Hessian), complete the + ! basis with the remaining near-zero modes and assign a safe minimum + ! curvature (opt%hlow) for preconditioning. + if (k.lt.nvar) then + do i=nat3,1,-1 + if (abs(eig(i)).le.thr .and. k.lt.nvar) then + k = k + 1 + trafo(1:nat3,k) = hess(1:nat3,i) + hessp(k+k*(k-1)/2) = opt%hlow + end if + end do + end if if(k.ne.nvar) then write(env%unit,*)'k=',k,' nvar=',nvar write(env%unit,*) 'ANC generation failed' From 1cb02d545bcb0d6cb0428fe0370e1d8a710a3279 Mon Sep 17 00:00:00 2001 From: Daniel Wiczew Date: Tue, 20 Jan 2026 15:31:06 +0100 Subject: [PATCH 2/2] test: add regression for GFN-FF $fix L-ANC Adds a new relaxation-engine unit test that runs L-ANC (lbfgs) ANC generation in GFN-FF mode with N>500 and an exact $fix set. This used to fail with "ANC generation failed" due to mismatched nvar handling; the test now guards against regressions. Signed-off-by: Daniel Wiczew --- test/unit/CMakeLists.txt | 1 + test/unit/main.f90 | 3 +- test/unit/meson.build | 1 + test/unit/relaxation_engine.f90 | 55 ------------ test/unit/test_relaxation_engine.f90 | 130 +++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 56 deletions(-) delete mode 100644 test/unit/relaxation_engine.f90 create mode 100644 test/unit/test_relaxation_engine.f90 diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index eacb75578..1e833f770 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -33,6 +33,7 @@ set( "gfn2" "gfnff" "hessian" + "relaxation-engine" "iff" "latticepoint" "molecule" diff --git a/test/unit/main.f90 b/test/unit/main.f90 index 761bb57b8..d3a5e4cfd 100644 --- a/test/unit/main.f90 +++ b/test/unit/main.f90 @@ -31,6 +31,7 @@ program tester use test_gfn2, only : collect_gfn2 use test_gfnff, only : collect_gfnff use test_hessian, only : collect_hessian + use test_relaxation_engine, only : collect_relaxation_engine use test_iff, only : collect_iff use test_latticepoint, only : collect_latticepoint use test_molecule, only : collect_molecule @@ -69,6 +70,7 @@ program tester new_testsuite("gfn2", collect_gfn2), & new_testsuite("gfnff", collect_gfnff), & new_testsuite("hessian", collect_hessian), & + new_testsuite("relaxation-engine", collect_relaxation_engine), & new_testsuite("iff", collect_iff), & new_testsuite("latticepoint", collect_latticepoint), & new_testsuite("molecule", collect_molecule), & @@ -252,4 +254,3 @@ subroutine clear_error(error) end subroutine clear_error end program tester - diff --git a/test/unit/meson.build b/test/unit/meson.build index 32d23ebfd..ec4a27b5f 100644 --- a/test/unit/meson.build +++ b/test/unit/meson.build @@ -44,6 +44,7 @@ tests = [ 'gfn2', 'gfnff', 'hessian', + 'relaxation-engine', 'iff', 'latticepoint', 'molecule', diff --git a/test/unit/relaxation_engine.f90 b/test/unit/relaxation_engine.f90 deleted file mode 100644 index ab194da31..000000000 --- a/test/unit/relaxation_engine.f90 +++ /dev/null @@ -1,55 +0,0 @@ -subroutine test_pbc_lancopt - use xtb_mctc_accuracy, only : wp - use assertion - - use xtb_type_molecule - - use model_phonons - - implicit none - - real(wp),parameter :: thr = 1.0e-9_wp - integer, parameter :: nat = 6 - integer, parameter :: at(nat) = [22,22, 8, 8, 8, 8] - real(wp),parameter :: xyz(3,nat) = reshape( & - &[0.00000000_wp, 0.00000000_wp, 0.00000000_wp, & - & 0.08499567_wp, 0.08499567_wp, 0.05476752_wp, & - & 0.04795456_wp, 0.04795456_wp, 0.00000000_wp, & - & 0.03704111_wp, 0.13295023_wp, 0.05476752_wp, & - & 0.12203678_wp, 0.12203678_wp, 0.00000000_wp, & - & 0.13295023_wp, 0.03704111_wp, 0.05476752_wp], shape(xyz)) - real(wp),parameter :: lattice(3,3) = reshape( & - &[0.16999134_wp, 0.00000000_wp, 0.00000000_wp, & - & 0.00000000_wp, 0.16999134_wp, 0.00000000_wp, & - & 0.00000000_wp, 0.00000000_wp, 0.10953503_wp], shape(lattice)) - integer, parameter :: wsc_rep(3) = [1,1,1] - - type(TMolecule) :: mol - type(mp_options) :: opt = mp_options(k_stretch = 0.04_wp, coupled = .true.) - - integer :: i,j,ij - integer :: nvar,npvar - real(wp), allocatable :: hess(:) - real(wp), allocatable :: phon(:,:) - real(wp), allocatable :: freq(:) - - integer :: info - integer :: lwork - integer :: liwork - integer, allocatable :: iwork(:) - real(wp),allocatable :: aux(:) - - call mol%allocate(nat) - mol%at = at - mol%xyz = xyz - mol%npbc = 3 - mol%pbc = .true. - mol%lattice = lattice - call mol%update - call generate_wsc(mol,mol%wsc,wsc_rep) - call generate_wsl(mol,mol%wsl,wsc_rep) - - - call terminate(afail+1) - -end subroutine test_pbc_lancopt diff --git a/test/unit/test_relaxation_engine.f90 b/test/unit/test_relaxation_engine.f90 new file mode 100644 index 000000000..cebc6d7af --- /dev/null +++ b/test/unit/test_relaxation_engine.f90 @@ -0,0 +1,130 @@ +! This file is part of xtb. +! SPDX-Identifier: LGPL-3.0-or-later +! +! xtb 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. +! +! xtb 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 Lesser General Public License for more details. +! +! You should have received a copy of the GNU Lesser General Public License +! along with xtb. If not, see . + +module test_relaxation_engine + use testdrive, only : new_unittest, unittest_type, error_type, check_ => check + implicit none + private + + public :: collect_relaxation_engine + +contains + +subroutine collect_relaxation_engine(testsuite) + type(unittest_type), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + new_unittest("gfnff-fix-lancopt", test_gfnff_fix_lancopt) & + ] + +end subroutine collect_relaxation_engine + + +subroutine test_gfnff_fix_lancopt(error) + use xtb_mctc_accuracy, only : wp + use xtb_type_environment, only : TEnvironment, init + use xtb_type_molecule, only : TMolecule, init + use xtb_type_restart, only : TRestart + use xtb_gfnff_calculator, only : TGFFCalculator, newGFFCalculator + use xtb_relaxation_engine, only : l_ancopt + use xtb_setparam, only : set, p_ext_gfnff, p_olev_normal + use xtb_fixparam, only : init_fix, clear_fix, fixset + use xtb_splitparam, only : init_split, atmass + use mctcpar_atomic_masses, only : atomic_mass + use xtb_mctc_convert, only : autoamu + + type(error_type), allocatable, intent(out) :: error + + integer, parameter :: nat = 510 + integer, parameter :: nfix = 500 + real(wp), parameter :: spacing = 2.8_wp + real(wp), parameter :: zigzag = 0.5_wp + real(wp), parameter :: frag_sep = 200.0_wp + + type(TEnvironment) :: env + type(TMolecule) :: mol + type(TRestart) :: chk + type(TGFFCalculator) :: calc + + integer, allocatable :: at(:) + real(wp), allocatable :: xyz(:,:) + real(wp), allocatable :: gradient(:, :) + real(wp) :: energy, egap, sigma(3, 3) + + logical :: fail, exitRun + integer :: saved_mode_extrun, saved_micro_opt + + integer :: i + + ! Regression test for: GFN-FF + L-ANC + $fix + (N > 500) used to abort in + ! ANC generation with e.g. "k=30 nvar=1530" / "ANC generation failed". + ! Core invariant: degrees of freedom must account for fixed atoms, + ! nvar = 3*N - 3*nfix - 3, + ! even when using the fragmented Hessian path in L-ANC. + + saved_mode_extrun = set%mode_extrun + saved_micro_opt = set%optset%micro_opt + set%mode_extrun = p_ext_gfnff + set%optset%micro_opt = 1 ! keep runtime low (1 LBFGS micro step) + + allocate(at(nat), source=6) + allocate(xyz(3, nat), source=0.0_wp) + + do i = 1, nat/2 + xyz(1, i) = spacing*real(i-1, wp) + xyz(2, i) = merge(zigzag, -zigzag, mod(i, 2) == 0) + end do + do i = nat/2 + 1, nat + xyz(1, i) = spacing*real(i-1-nat/2, wp) + xyz(2, i) = frag_sep + merge(zigzag, -zigzag, mod(i, 2) == 0) + end do + + call init(env) + call init(mol, at, xyz) + + call init_split(mol%n) + atmass = atomic_mass(mol%at) * autoamu + + call delete_file('charges') + call newGFFCalculator(env, mol, calc, '.param_gfnff.xtb', .false.) + call env%check(exitRun) + call check_(error, .not.exitRun) + if (allocated(error)) goto 100 + + call init_fix(mol%n) + fixset%n = nfix + do i = 1, nfix + fixset%atoms(i) = i + end do + + allocate(gradient(3, nat), source=0.0_wp) + energy = 0.0_wp + egap = 0.0_wp + sigma = 0.0_wp + + call l_ancopt(env, -1, mol, chk, calc, p_olev_normal, 1, energy, egap, gradient, sigma, 0, fail) + call check_(error, .not.fail) + +100 continue + call clear_fix + set%mode_extrun = saved_mode_extrun + set%optset%micro_opt = saved_micro_opt + call mol%deallocate + +end subroutine test_gfnff_fix_lancopt + + +end module test_relaxation_engine