From fb074d1f72db318f8b53ebb55524dc2d77854a12 Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Mon, 7 Sep 2026 17:19:55 +0800 Subject: [PATCH 01/14] fix(hsolver): honor Hermitian upper triangles in native ELPA and cuSolver (cherry picked from commit 4186ff027431a46dd5d3d6ea087af2af1ffafffe) --- source/source_hsolver/diago_elpa_native.cpp | 80 ++++++++++++++++++- .../kernels/cuda/diag_cusolver.cu | 3 +- source/source_hsolver/test/CMakeLists.txt | 2 +- .../test/diago_lcao_cusolver_test.cpp | 19 +++++ .../source_hsolver/test/diago_lcao_test.cpp | 61 ++++++++++++++ 5 files changed, 160 insertions(+), 5 deletions(-) diff --git a/source/source_hsolver/diago_elpa_native.cpp b/source/source_hsolver/diago_elpa_native.cpp index 8a918377e89..1aaf0ff5fa9 100644 --- a/source/source_hsolver/diago_elpa_native.cpp +++ b/source/source_hsolver/diago_elpa_native.cpp @@ -3,6 +3,7 @@ #include "source_base/global_function.h" #include "source_base/module_external/blas_connector.h" #include "source_base/module_external/blacs_connector.h" +#include "source_base/module_external/scalapack_connector.h" #include "source_base/timer.h" #include "source_base/tool_quit.h" #include "source_hsolver/module_genelpa/elpa_new.h" @@ -71,6 +72,79 @@ void DiagoElpaNative::diag_pool(hamilt::MatrixBlock& h_mat, std::vector eigen(this->nlocal, 0.0); std::vector eigenvectors(narows * nacols); + // The complex LCAO matrices follow the LAPACK UPLO='U' convention: only + // their upper triangles are guaranteed to contain the Hermitian matrix. + // ELPA's native generalized solver consumes both triangles, so complete + // private Hermitian copies before the solve. + std::vector h_work; + std::vector s_work; + T* h_elpa = h_mat.p; + T* s_elpa = s_mat.p; + int decomposed_state = this->DecomposedState; + if (!std::is_same::value) + { + h_work.resize(narows * nacols); + s_work.resize(narows * nacols); + const int one = 1; + ScalapackConnector::tranc(nFull, + nFull, + T(1.0), + h_mat.p, + one, + one, + h_mat.desc, + T(0.0), + h_work.data(), + one, + one, + h_mat.desc); + ScalapackConnector::tranc(nFull, + nFull, + T(1.0), + s_mat.p, + one, + one, + s_mat.desc, + T(0.0), + s_work.data(), + one, + one, + s_mat.desc); + const auto local_to_global = [](const int local_index, + const int block_size, + const int process_coordinate, + const int source_coordinate, + const int process_count) { + if (source_coordinate < 0) + { + return local_index; + } + const int process_offset + = (process_coordinate - source_coordinate + process_count) % process_count; + return ((local_index / block_size) * process_count + process_offset) * block_size + + local_index % block_size; + }; + for (int local_col = 0; local_col < nacols; ++local_col) + { + const int global_col + = local_to_global(local_col, h_mat.desc[5], mypcol, h_mat.desc[7], npcols); + for (int local_row = 0; local_row < narows; ++local_row) + { + const int global_row + = local_to_global(local_row, h_mat.desc[4], myprow, h_mat.desc[6], nprows); + if (global_row <= global_col) + { + const int local_index = local_row + local_col * narows; + h_work[local_index] = h_mat.p[local_index]; + s_work[local_index] = s_mat.p[local_index]; + } + } + } + h_elpa = h_work.data(); + s_elpa = s_work.data(); + decomposed_state = 0; + } + if (elpa_init(20210430) != ELPA_OK) { fprintf(stderr, "Error: ELPA API version not supported"); @@ -114,11 +188,11 @@ void DiagoElpaNative::diag_pool(hamilt::MatrixBlock& h_mat, #endif elpa_generalized_eigenvectors(handle, - h_mat.p, - s_mat.p, + h_elpa, + s_elpa, eigen.data(), eigenvectors.data(), - this->DecomposedState, + decomposed_state, &success); elpa_deallocate(handle, &success); elpa_uninit(&success); diff --git a/source/source_hsolver/kernels/cuda/diag_cusolver.cu b/source/source_hsolver/kernels/cuda/diag_cusolver.cu index 642198cfd92..52e206483a2 100644 --- a/source/source_hsolver/kernels/cuda/diag_cusolver.cu +++ b/source/source_hsolver/kernels/cuda/diag_cusolver.cu @@ -9,7 +9,8 @@ Diag_Cusolver_gvd::Diag_Cusolver_gvd(){ itype = CUSOLVER_EIG_TYPE_1; // A*x = (lambda)*B*x jobz = CUSOLVER_EIG_MODE_VECTOR; // compute eigenvalues and eigenvectors. - uplo = CUBLAS_FILL_MODE_LOWER; + // LCAO supplies the authoritative upper triangle of the Hermitian H/S matrices. + uplo = CUBLAS_FILL_MODE_UPPER; d_A = NULL; d_B = NULL; diff --git a/source/source_hsolver/test/CMakeLists.txt b/source/source_hsolver/test/CMakeLists.txt index 96895a504ed..043c0596c1f 100644 --- a/source/source_hsolver/test/CMakeLists.txt +++ b/source/source_hsolver/test/CMakeLists.txt @@ -93,7 +93,7 @@ if (ENABLE_MPI) AddTest( TARGET MODULE_HSOLVER_LCAO LIBS parameter ELPA::ELPA base genelpa psi device - SOURCES diago_lcao_test.cpp ../diago_elpa.cpp ../diago_scalapack.cpp ../diago_lapack.cpp + SOURCES diago_lcao_test.cpp ../diago_elpa.cpp ../diago_elpa_native.cpp ../diago_scalapack.cpp ../diago_lapack.cpp ) else() AddTest( diff --git a/source/source_hsolver/test/diago_lcao_cusolver_test.cpp b/source/source_hsolver/test/diago_lcao_cusolver_test.cpp index 80669e49515..7f4cede9972 100644 --- a/source/source_hsolver/test/diago_lcao_cusolver_test.cpp +++ b/source/source_hsolver/test/diago_lcao_cusolver_test.cpp @@ -138,6 +138,21 @@ class DiagoPrepare return ok; } + void poison_lower_triangle() + { + // The distributed solver buffers are column-major. Keep the original + // row-major fixtures intact for the independent LAPACK reference. + for (int col = 0; col < nlocal; ++col) + { + for (int row = col + 1; row < nlocal; ++row) + { + const int index = row + col * nlocal; + this->h_local[index] = T(123.0 + row + col); + this->s_local[index] = T(0.0); + } + } + } + void print_hs() { if (!PRINT_HS) @@ -207,6 +222,10 @@ class DiagoPrepare { this->pb2d(); this->distribute_data(); + if (ks_solver == "cusolver") + { + this->poison_lower_triangle(); + } this->print_hs(); this->set_env(); diff --git a/source/source_hsolver/test/diago_lcao_test.cpp b/source/source_hsolver/test/diago_lcao_test.cpp index 60ef9427fd0..34ff9ed77ae 100644 --- a/source/source_hsolver/test/diago_lcao_test.cpp +++ b/source/source_hsolver/test/diago_lcao_test.cpp @@ -8,6 +8,7 @@ #include #ifdef __ELPA #include "source_hsolver/diago_elpa.h" +#include "source_hsolver/diago_elpa_native.h" #endif #include "source_base/module_external/scalapack_connector.h" @@ -139,6 +140,44 @@ class DiagoPrepare return ok; } +#ifdef __ELPA + void diago_native_with_poisoned_lower() + { + this->pb2d(); + this->distribute_data(); + int nprows; + int npcols; + int myprow; + int mypcol; + Cblacs_gridinfo(icontxt, &nprows, &npcols, &myprow, &mypcol); + for (int col = 0; col < hmtest.ncol; ++col) + { + const int global_col = (col / nb2d * npcols + mypcol) * nb2d + col % nb2d; + for (int row = 0; row < hmtest.nrow; ++row) + { + const int global_row = (row / nb2d * nprows + myprow) * nb2d + row % nb2d; + if (global_row > global_col) + { + const int index = row + col * hmtest.nrow; + h_local[index] = T(123.0 + global_row + global_col); + s_local[index] = T(0.0); + } + } + } + hmtest.h_local = h_local; + hmtest.s_local = s_local; + hsolver::DiagoElpaNative solver(nlocal, nbands, false); + solver.diag(&hmtest, psi, e_solver.data()); + EXPECT_EQ(hmtest.h_local, h_local); + EXPECT_EQ(hmtest.s_local, s_local); + // A second solve must not reuse an in-place decomposition of the + // private overlap copy or alter the caller's matrix storage. + solver.diag(&hmtest, psi, e_solver.data()); + EXPECT_EQ(hmtest.h_local, h_local); + EXPECT_EQ(hmtest.s_local, s_local); + } +#endif + void print_hs() { if (!PRINT_HS) @@ -371,6 +410,28 @@ INSTANTIATE_TEST_SUITE_P( DiagoPrepare>(0, 0, 1, 0, "scalapack_gvx", "H-KPoints-Si2.dat", "S-KPoints-Si2.dat"), DiagoPrepare>(0, 0, 32, 0, "scalapack_gvx", "H-KPoints-Si64.dat", "S-KPoints-Si64.dat"))); +#ifdef __ELPA +class DiagoElpaNativeUpperTest : public ::testing::TestWithParam +{ +}; + +TEST_P(DiagoElpaNativeUpperTest, PreservesInputsAndIgnoresLowerTriangle) +{ + DiagoPrepare> dp(0, 0, GetParam(), 0, "genelpa", + "H-KPoints-Si2.dat", "S-KPoints-Si2.dat"); + ASSERT_TRUE(dp.produce_HS()); + dp.diago_native_with_poisoned_lower(); + if (dp.myrank == 0) + { + dp.diago_lapack(); + std::stringstream out_info; + EXPECT_TRUE(dp.compare_eigen(out_info)) << out_info.str(); + } +} + +INSTANTIATE_TEST_SUITE_P(BlockSizes, DiagoElpaNativeUpperTest, ::testing::Values(1, 2, 3)); +#endif + int main(int argc, char** argv) { MPI_Init(&argc, &argv); From 97f062c3fcaf7bcc34c4d04c0f4793795ba08965 Mon Sep 17 00:00:00 2001 From: TaoXia Date: Sun, 6 Sep 2026 16:51:44 +0800 Subject: [PATCH 02/14] fix(hsolver): honor Hermitian upper triangle in genelpa (cherry picked from commit e27ef6629938d04ea1649d4779ad13a3931bc6eb) (cherry picked from commit 1c48b26a5cf06c81fd1d474303642b7d4efcb791) --- .../module_genelpa/elpa_new_complex.cpp | 7 ++++-- .../source_hsolver/test/diago_lcao_test.cpp | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/source/source_hsolver/module_genelpa/elpa_new_complex.cpp b/source/source_hsolver/module_genelpa/elpa_new_complex.cpp index cf28835942e..dd7926424df 100644 --- a/source/source_hsolver/module_genelpa/elpa_new_complex.cpp +++ b/source/source_hsolver/module_genelpa/elpa_new_complex.cpp @@ -79,7 +79,10 @@ int ELPA_Solver::generalized_eigenvector(std::complex* A, std::complex1) { timer(myid, "A*U^-1", "2.1a", t); @@ -105,7 +108,7 @@ int ELPA_Solver::generalized_eigenvector(std::complex* A, std::complex1) { timer(myid, "B*A^T", "2.1b", t); diff --git a/source/source_hsolver/test/diago_lcao_test.cpp b/source/source_hsolver/test/diago_lcao_test.cpp index 34ff9ed77ae..27a5d4bf21c 100644 --- a/source/source_hsolver/test/diago_lcao_test.cpp +++ b/source/source_hsolver/test/diago_lcao_test.cpp @@ -430,6 +430,31 @@ TEST_P(DiagoElpaNativeUpperTest, PreservesInputsAndIgnoresLowerTriangle) } INSTANTIATE_TEST_SUITE_P(BlockSizes, DiagoElpaNativeUpperTest, ::testing::Values(1, 2, 3)); + +TEST(DiagoElpaComplexTest, UsesAuthoritativeUpperTriangle) +{ + std::stringstream out_info; + DiagoPrepare> dp(0, 0, 1, 0, "genelpa", "H-KPoints-Si2.dat", "S-KPoints-Si2.dat"); + ASSERT_TRUE(dp.produce_HS()); + + if (dp.myrank == 0) + { + dp.diago_lapack(); + for (int row = 1; row < dp.nlocal; ++row) + { + for (int col = 0; col < row; ++col) + { + dp.h[row * dp.nlocal + col] = std::complex(17.0 + row + col, -13.0); + } + } + } + + dp.diago(); + if (dp.myrank == 0) + { + EXPECT_TRUE(dp.compare_eigen(out_info)) << out_info.str(); + } +} #endif int main(int argc, char** argv) From 75591414e9ba140e3c70231f3a19cbe7980979bb Mon Sep 17 00:00:00 2001 From: TaoXia Date: Sun, 6 Sep 2026 18:21:38 +0800 Subject: [PATCH 03/14] Fix second generalized ELPA transform (cherry picked from commit 53208bf2713db64359315d3f0359adfefbbb4a38) (cherry picked from commit 0cb191290e7b652b8b8e2356cdc06bf69a5cf847) --- source/source_hsolver/module_genelpa/elpa_new_complex.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/source_hsolver/module_genelpa/elpa_new_complex.cpp b/source/source_hsolver/module_genelpa/elpa_new_complex.cpp index dd7926424df..fdecac78edc 100644 --- a/source/source_hsolver/module_genelpa/elpa_new_complex.cpp +++ b/source/source_hsolver/module_genelpa/elpa_new_complex.cpp @@ -108,7 +108,10 @@ int ELPA_Solver::generalized_eigenvector(std::complex* A, std::complex1) { timer(myid, "B*A^T", "2.1b", t); From ced60046e606073243a2fa5fb04f809edd4a88fc Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Mon, 7 Sep 2026 17:34:26 +0800 Subject: [PATCH 04/14] fix(test): allocate the advertised LAPACK workspace (cherry picked from commit bb8688e640cf90f81ec12714b29d19089f077db7) --- source/source_hsolver/test/diago_elpa_utils.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/source_hsolver/test/diago_elpa_utils.h b/source/source_hsolver/test/diago_elpa_utils.h index 22e986935d9..843c53b6a51 100644 --- a/source/source_hsolver/test/diago_elpa_utils.h +++ b/source/source_hsolver/test/diago_elpa_utils.h @@ -167,7 +167,7 @@ void lapack_diago(double *hmatrix, double *smatrix, double *e, int &nFull) const char jobz = 'V'; // 'N':only calc eigenvalue, 'V': eigenvalues and eigenvectors const char uplo = 'U'; // Upper triangles int lwork = (nFull + 2) * nFull, info = 0; - double *ev = new double[nFull * nFull]; + double *ev = new double[lwork]; double *a = new double[nFull * nFull]; double *b = new double[nFull * nFull]; @@ -196,7 +196,7 @@ void lapack_diago(std::complex *hmatrix, std::complex *smatrix, const char uplo = 'U'; // Upper triangles int lwork = (nFull + 1) * nFull, info = 0; double *rwork = new double[3 * nFull - 2]; - std::complex *ev = new std::complex[nFull * nFull]; + std::complex *ev = new std::complex[lwork]; std::complex *a = new std::complex[nFull * nFull]; std::complex *b = new std::complex[nFull * nFull]; From 986d692f95154a9a18c1ac0ae374218971a2c946 Mon Sep 17 00:00:00 2001 From: TaoXia Date: Sun, 6 Sep 2026 16:51:44 +0800 Subject: [PATCH 05/14] fix(deltaspin): align lambda operator with Pauli moments (cherry picked from commit 1b6ecbf33e8f239937cca465208fe8bc921cd131) --- .../source_lcao/module_deltaspin/mi_tools.h | 20 ++++++++++++-- .../test/deltaspin_core_test.cpp | 26 +++++++++++++++++- .../module_operator_lcao/dspin_lcao.cpp | 27 +++++++++---------- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/source/source_lcao/module_deltaspin/mi_tools.h b/source/source_lcao/module_deltaspin/mi_tools.h index 5130aeb5cbd..b634e6c086c 100644 --- a/source/source_lcao/module_deltaspin/mi_tools.h +++ b/source/source_lcao/module_deltaspin/mi_tools.h @@ -1,11 +1,12 @@ #ifndef MI_TOOLS_H #define MI_TOOLS_H +#include "source_base/vector3.h" + +#include #include #include -#include "source_base/vector3.h" - /** * @file mi_tools.h * @brief Free-function utilities for computing atomic magnetic moments (Mi) @@ -23,6 +24,21 @@ namespace spinconstrain { +/** + * @brief Convert a Cartesian Pauli vector to a spinor-space operator. + * + * For sigma_y = [[0, -i], [i, 0]], lambda dot sigma is stored in row-major + * order as {lambda_z, lambda_x - i lambda_y, + * lambda_x + i lambda_y, -lambda_z}. + */ +inline std::array, 4> pauli_vector_to_spinor(const ModuleBase::Vector3& lambda) +{ + return {{std::complex(lambda.z, 0.0), + std::complex(lambda.x, -lambda.y), + std::complex(lambda.x, lambda.y), + std::complex(-lambda.z, 0.0)}}; +} + /** * @brief Convert spinor occupation matrix to magnetic moment vector using Pauli matrices. * diff --git a/source/source_lcao/module_deltaspin/test/deltaspin_core_test.cpp b/source/source_lcao/module_deltaspin/test/deltaspin_core_test.cpp index 30c036eb4e8..7d72449168b 100644 --- a/source/source_lcao/module_deltaspin/test/deltaspin_core_test.cpp +++ b/source/source_lcao/module_deltaspin/test/deltaspin_core_test.cpp @@ -1,8 +1,10 @@ +#include "source_lcao/module_deltaspin/mi_tools.h" + #include "gtest/gtest.h" +#include #include #include #include -#include /*********************************************************************** * Unit tests for DeltaSpin core algorithms. @@ -92,6 +94,28 @@ TEST_F(PauliToMomentTest, GeneralCase_AllComponents) EXPECT_NEAR(M.z, 0.2, 1e-15); } +TEST(PauliConventionTest, LambdaExpectationMatchesDotMoment) +{ + const double amplitude = 1.0 / std::sqrt(2.0); + const std::complex spinor[2] = {{amplitude, 0.0}, {0.0, amplitude}}; + const std::complex occ[4] = {std::conj(spinor[0]) * spinor[0], + std::conj(spinor[0]) * spinor[1], + std::conj(spinor[1]) * spinor[0], + std::conj(spinor[1]) * spinor[1]}; + const ModuleBase::Vector3 lambda(0.0, 2.0, 0.0); + const auto matrix = spinconstrain::pauli_vector_to_spinor(lambda); + const auto moment = spinconstrain::pauli_to_moment(occ, 1.0); + + const std::complex h_up = matrix[0] * spinor[0] + matrix[1] * spinor[1]; + const std::complex h_down = matrix[2] * spinor[0] + matrix[3] * spinor[1]; + const double expectation = (std::conj(spinor[0]) * h_up + std::conj(spinor[1]) * h_down).real(); + const double dot_moment = lambda.x * moment.x + lambda.y * moment.y + lambda.z * moment.z; + + EXPECT_NEAR(matrix[1].imag(), -2.0, 1e-15); + EXPECT_NEAR(matrix[2].imag(), 2.0, 1e-15); + EXPECT_NEAR(expectation, dot_moment, 1e-15); +} + // ===================================================================== // 2. calculate_delta_hcc: Pauli matrix expansion // diff --git a/source/source_lcao/module_operator_lcao/dspin_lcao.cpp b/source/source_lcao/module_operator_lcao/dspin_lcao.cpp index be47e41ba45..6d487b2fe21 100644 --- a/source/source_lcao/module_operator_lcao/dspin_lcao.cpp +++ b/source/source_lcao/module_operator_lcao/dspin_lcao.cpp @@ -1,10 +1,12 @@ #include "dspin_lcao.h" -#include "source_lcao/module_deltaspin/spin_constrain.h" -#include "source_base/timer.h" + #include "source_base/memory_recorder.h" -#include "source_base/tool_title.h" #include "source_base/parallel_reduce.h" +#include "source_base/timer.h" +#include "source_base/tool_title.h" #include "source_io/module_parameter/parameter.h" +#include "source_lcao/module_deltaspin/mi_tools.h" +#include "source_lcao/module_deltaspin/spin_constrain.h" template hamilt::DeltaSpin>::DeltaSpin(HS_Matrix_K* hsk_in, @@ -56,16 +58,13 @@ inline void cal_coeff_lambda(const std::vector& current_lambda, std::vec coefficients[1] = -current_lambda[0]; } inline void cal_coeff_lambda(const std::vector& current_lambda, std::vector>& coefficients) -{// {\lambda^{I,3}, \lambda^{I,1}+i\lambda^{I,2}, \lambda^{I,1}-i\lambda^{I,2}, -\lambda^{I,3}} - // The occupation matrix is built conj-first (occ[1]=conj(c_up)*c_dn, spin_constrain.cpp), - // and pauli_to_moment measures the physical +m_y from it (bare +Im). The lambda operator must - // drive that measured moment with the matching feedback sign, i.e. the pre-#7664 convention - // lambda_{ud}=lambda_x+i*lambda_y. #7664 flipped this together with the measurement (mirror-y); - // #7748 reverted the measurement but not this, leaving the constraint loop driving the y-mirror. - coefficients[0] = std::complex(current_lambda[2], 0.0); - coefficients[1] = std::complex(current_lambda[0] , current_lambda[1]); - coefficients[2] = std::complex(current_lambda[0] , -1 * current_lambda[1]); - coefficients[3] = std::complex(-1 * current_lambda[2], 0.0); +{ + const ModuleBase::Vector3 lambda(current_lambda[0], current_lambda[1], current_lambda[2]); + const auto spinor = spinconstrain::pauli_vector_to_spinor(lambda); + for (int is = 0; is < 4; ++is) + { + coefficients[is] = spinor[is]; + } } template @@ -635,4 +634,4 @@ void hamilt::DeltaSpin>::cal_PI_sub( template class hamilt::DeltaSpin>; template class hamilt::DeltaSpin, double>>; -template class hamilt::DeltaSpin, std::complex>>; \ No newline at end of file +template class hamilt::DeltaSpin, std::complex>>; From 07cb1cb2a4508a52930410cc4ea5990c2420890d Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Mon, 7 Sep 2026 18:27:23 +0800 Subject: [PATCH 06/14] refactor(xc): add C2 radial and exact spin-density maps --- source/source_hamilt/module_xc/CMakeLists.txt | 1 + .../module_xc/test/CMakeLists.txt | 5 + .../module_xc/test/test_xc_ncgga_radial.cpp | 278 ++++++++++++++++++ .../module_xc/xc_ncgga_radial.cpp | 95 ++++++ .../source_hamilt/module_xc/xc_ncgga_radial.h | 60 ++++ 5 files changed, 439 insertions(+) create mode 100644 source/source_hamilt/module_xc/test/test_xc_ncgga_radial.cpp create mode 100644 source/source_hamilt/module_xc/xc_ncgga_radial.cpp create mode 100644 source/source_hamilt/module_xc/xc_ncgga_radial.h diff --git a/source/source_hamilt/module_xc/CMakeLists.txt b/source/source_hamilt/module_xc/CMakeLists.txt index 0b6309a5a24..36300e091c1 100644 --- a/source/source_hamilt/module_xc/CMakeLists.txt +++ b/source/source_hamilt/module_xc/CMakeLists.txt @@ -1,6 +1,7 @@ add_library( xc_ OBJECT + xc_ncgga_radial.cpp xc_functional.cpp xc_pot.cpp xc_grad.cpp diff --git a/source/source_hamilt/module_xc/test/CMakeLists.txt b/source/source_hamilt/module_xc/test/CMakeLists.txt index 004901c2450..3d9dac3a5de 100644 --- a/source/source_hamilt/module_xc/test/CMakeLists.txt +++ b/source/source_hamilt/module_xc/test/CMakeLists.txt @@ -123,3 +123,8 @@ AddTest( ../../../source_base/module_fft/fft_cpu.cpp ${FFT_SRC} ) + +AddTest( + TARGET MODULE_HAMILT_XCTest_NCGGA_RADIAL + SOURCES test_xc_ncgga_radial.cpp ../xc_ncgga_radial.cpp +) diff --git a/source/source_hamilt/module_xc/test/test_xc_ncgga_radial.cpp b/source/source_hamilt/module_xc/test/test_xc_ncgga_radial.cpp new file mode 100644 index 00000000000..3ad9a9d19f0 --- /dev/null +++ b/source/source_hamilt/module_xc/test/test_xc_ncgga_radial.cpp @@ -0,0 +1,278 @@ +#include "../xc_ncgga_radial.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +double dot(const std::array& left, const std::array& right) +{ + double result = 0.0; + for (int component = 0; component < 3; ++component) + { + result += left[component] * right[component]; + } + return result; +} + +std::array shifted(const std::array& point, + const std::array& direction, + const double step) +{ + std::array result = point; + for (int component = 0; component < 3; ++component) + { + result[component] += step * direction[component]; + } + return result; +} + +double directional_hessian(const ModuleXC::NcggaRadialPoint& point, const std::array& direction) +{ + double result = 0.0; + for (int row = 0; row < 3; ++row) + { + for (int column = 0; column < 3; ++column) + { + result += direction[row] * point.jacobian(row, column) * direction[column]; + } + } + return result; +} + +ModuleXC::NcggaSpinMapPoint make_spin_map(const std::array& state, const double eta) +{ + const std::array magnetization = {{state[1], state[2], state[3]}}; + return ModuleXC::make_ncgga_spin_map_point(state[0], ModuleXC::make_ncgga_radial_point(magnetization, eta)); +} + +TEST(NcggaRadial, RejectsNonPositiveEta) +{ + const std::array magnetization = {{0.1, 0.0, 0.0}}; + EXPECT_THROW(ModuleXC::make_ncgga_radial_point(magnetization, 0.0), std::invalid_argument); + EXPECT_THROW(ModuleXC::make_ncgga_radial_point(magnetization, -0.5), std::invalid_argument); +} + +TEST(NcggaRadial, MatchesZeroInteriorJoinAndExteriorValues) +{ + const double eta = 0.5; + const ModuleXC::NcggaRadialPoint zero = ModuleXC::make_ncgga_radial_point({{0.0, 0.0, 0.0}}, eta); + EXPECT_DOUBLE_EQ(zero.value, 0.0); + for (int row = 0; row < 3; ++row) + { + EXPECT_DOUBLE_EQ(zero.direction[row], 0.0); + EXPECT_DOUBLE_EQ(zero.gradient[row], 0.0); + for (int column = 0; column < 3; ++column) + { + EXPECT_DOUBLE_EQ(zero.jacobian(row, column), 0.0); + } + } + + const ModuleXC::NcggaRadialPoint interior = ModuleXC::make_ncgga_radial_point({{0.5 * eta, 0.0, 0.0}}, eta); + EXPECT_DOUBLE_EQ(interior.value, 11.0 * eta / 32.0); + EXPECT_DOUBLE_EQ(interior.gradient[0], 23.0 / 16.0); + EXPECT_DOUBLE_EQ(interior.jacobian(0, 0), 3.0 / (2.0 * eta)); + EXPECT_DOUBLE_EQ(interior.jacobian(1, 1), 23.0 / (8.0 * eta)); + EXPECT_DOUBLE_EQ(interior.jacobian(2, 2), 23.0 / (8.0 * eta)); + + const ModuleXC::NcggaRadialPoint join = ModuleXC::make_ncgga_radial_point({{eta, 0.0, 0.0}}, eta); + EXPECT_DOUBLE_EQ(join.value, eta); + EXPECT_DOUBLE_EQ(join.gradient[0], 1.0); + EXPECT_DOUBLE_EQ(join.jacobian(0, 0), 0.0); + EXPECT_DOUBLE_EQ(join.jacobian(1, 1), 1.0 / eta); + EXPECT_DOUBLE_EQ(join.jacobian(2, 2), 1.0 / eta); + + const ModuleXC::NcggaRadialPoint exterior = ModuleXC::make_ncgga_radial_point({{3.0, 4.0, 0.0}}, eta); + EXPECT_DOUBLE_EQ(exterior.value, 5.0); + EXPECT_DOUBLE_EQ(exterior.gradient[0], 0.6); + EXPECT_DOUBLE_EQ(exterior.gradient[1], 0.8); + EXPECT_NEAR(exterior.jacobian(0, 0), 0.128, 1.0e-15); + EXPECT_NEAR(exterior.jacobian(0, 1), -0.096, 1.0e-15); + EXPECT_NEAR(exterior.jacobian(1, 1), 0.072, 1.0e-15); + EXPECT_NEAR(exterior.jacobian(2, 2), 0.2, 1.0e-15); +} + +TEST(NcggaRadial, CompactBranchConvergesC2AtJoin) +{ + const double eta = 0.5; + const double coarse_delta = 1.0e-3; + const double fine_delta = 0.5 * coarse_delta; + const ModuleXC::NcggaRadialPoint coarse + = ModuleXC::make_ncgga_radial_point({{eta * (1.0 - coarse_delta), 0.0, 0.0}}, eta); + const ModuleXC::NcggaRadialPoint fine + = ModuleXC::make_ncgga_radial_point({{eta * (1.0 - fine_delta), 0.0, 0.0}}, eta); + + EXPECT_LE(std::abs(fine.value - eta), 0.51 * std::abs(coarse.value - eta)); + EXPECT_LE(std::abs(fine.gradient[0] - 1.0), 0.26 * std::abs(coarse.gradient[0] - 1.0)); + EXPECT_LE(std::abs(fine.jacobian(0, 0)), 0.51 * std::abs(coarse.jacobian(0, 0))); + EXPECT_LE(std::abs(fine.jacobian(1, 1) - 1.0 / eta), 0.51 * std::abs(coarse.jacobian(1, 1) - 1.0 / eta)); +} + +TEST(NcggaRadial, CompactBranchConvergesC2AtOrigin) +{ + const double eta = 0.5; + const double coarse_radius = 1.0e-3 * eta; + const double fine_radius = 0.5 * coarse_radius; + const ModuleXC::NcggaRadialPoint coarse = ModuleXC::make_ncgga_radial_point({{coarse_radius, 0.0, 0.0}}, eta); + const ModuleXC::NcggaRadialPoint fine = ModuleXC::make_ncgga_radial_point({{fine_radius, 0.0, 0.0}}, eta); + + EXPECT_LE(fine.value, 0.13 * coarse.value); + EXPECT_LE(std::abs(fine.gradient[0]), 0.26 * std::abs(coarse.gradient[0])); + EXPECT_LE(std::abs(fine.jacobian(0, 0)), 0.51 * std::abs(coarse.jacobian(0, 0))); + EXPECT_LE(std::abs(fine.jacobian(1, 1)), 0.51 * std::abs(coarse.jacobian(1, 1))); +} + +TEST(NcggaRadial, GradientMatchesCentralFiniteDifference) +{ + const double eta = 0.5; + const double step = 2.0e-6 * eta; + const std::array direction = {{0.31, -0.27, 0.19}}; + const std::array interior = {{0.12, -0.08, 0.05}}; + const std::array exterior = {{0.62, -0.41, 0.23}}; + const std::array join = {{eta, 0.0, 0.0}}; + const std::array, 3> points = {{interior, exterior, join}}; + + for (std::size_t sample = 0; sample < points.size(); ++sample) + { + const ModuleXC::NcggaRadialPoint point = ModuleXC::make_ncgga_radial_point(points[sample], eta); + const double finite_difference + = (ModuleXC::make_ncgga_radial_point(shifted(points[sample], direction, step), eta).value + - ModuleXC::make_ncgga_radial_point(shifted(points[sample], direction, -step), eta).value) + / (2.0 * step); + EXPECT_NEAR(finite_difference, dot(point.gradient, direction), 2.0e-10); + } +} + +TEST(NcggaRadial, JacobianMatchesGradientFiniteDifference) +{ + const double eta = 0.5; + const double step = 2.0e-6 * eta; + const std::array direction = {{0.31, -0.27, 0.19}}; + const std::array interior = {{0.12, -0.08, 0.05}}; + const std::array exterior = {{0.62, -0.41, 0.23}}; + const std::array, 2> points = {{interior, exterior}}; + + for (std::size_t sample = 0; sample < points.size(); ++sample) + { + const ModuleXC::NcggaRadialPoint point = ModuleXC::make_ncgga_radial_point(points[sample], eta); + const ModuleXC::NcggaRadialPoint plus + = ModuleXC::make_ncgga_radial_point(shifted(points[sample], direction, step), eta); + const ModuleXC::NcggaRadialPoint minus + = ModuleXC::make_ncgga_radial_point(shifted(points[sample], direction, -step), eta); + for (int row = 0; row < 3; ++row) + { + double analytic = 0.0; + for (int column = 0; column < 3; ++column) + { + analytic += point.jacobian(row, column) * direction[column]; + EXPECT_NEAR(point.jacobian(row, column), point.jacobian(column, row), 1.0e-15); + } + const double finite_difference = (plus.gradient[row] - minus.gradient[row]) / (2.0 * step); + EXPECT_NEAR(finite_difference, analytic, 2.0e-9); + } + } +} + +TEST(NcggaRadial, DirectionalSecondDerivativeMatchesValueFiniteDifference) +{ + const double eta = 0.5; + const double step = 2.0e-4 * eta; + const std::array direction = {{0.31, -0.27, 0.19}}; + const std::array interior = {{0.12, -0.08, 0.05}}; + const std::array exterior = {{0.62, -0.41, 0.23}}; + const std::array, 2> points = {{interior, exterior}}; + + for (std::size_t sample = 0; sample < points.size(); ++sample) + { + const ModuleXC::NcggaRadialPoint point = ModuleXC::make_ncgga_radial_point(points[sample], eta); + const double plus = ModuleXC::make_ncgga_radial_point(shifted(points[sample], direction, step), eta).value; + const double minus = ModuleXC::make_ncgga_radial_point(shifted(points[sample], direction, -step), eta).value; + const double finite_difference = (plus - 2.0 * point.value + minus) / (step * step); + EXPECT_NEAR(finite_difference, directional_hessian(point, direction), 2.0e-7); + } +} + +TEST(NcggaRadial, SpinDensityMapJacobianMatchesFiniteDifferenceInEachBranch) +{ + const double eta = 0.5; + const double step = 2.0e-7; + const std::array, 5> states = {{{{1.20, 0.30, -0.20, 0.10}}, + {{-1.20, 0.30, -0.20, 0.10}}, + {{0.20, 0.42, -0.31, 0.16}}, + {{-0.20, 0.42, -0.31, 0.16}}, + {{0.90, 0.12, -0.08, 0.05}}}}; + + for (std::size_t sample = 0; sample < states.size(); ++sample) + { + const ModuleXC::NcggaSpinMapPoint point = make_spin_map(states[sample], eta); + EXPECT_GE(point.spin_density[0], 0.0); + EXPECT_GE(point.spin_density[1], 0.0); + EXPECT_NEAR(point.spin_density[0] + point.spin_density[1], std::abs(states[sample][0]), 1.0e-15); + EXPECT_NEAR(point.spin_density[0] - point.spin_density[1], point.clipped_magnitude, 1.0e-15); + + double maximum_error = 0.0; + for (int channel = 0; channel < 4; ++channel) + { + std::array plus_state = states[sample]; + std::array minus_state = states[sample]; + plus_state[channel] += step; + minus_state[channel] -= step; + const ModuleXC::NcggaSpinMapPoint plus = make_spin_map(plus_state, eta); + const ModuleXC::NcggaSpinMapPoint minus = make_spin_map(minus_state, eta); + for (int spin = 0; spin < 2; ++spin) + { + const double finite_difference = (plus.spin_density[spin] - minus.spin_density[spin]) / (2.0 * step); + maximum_error = std::max(maximum_error, std::abs(finite_difference - point.jacobian(spin, channel))); + EXPECT_NEAR(finite_difference, point.jacobian(spin, channel), 3.0e-9) + << "sample=" << sample << " spin=" << spin << " channel=" << channel; + } + } + std::cout << std::setprecision(17) << "NCGGA_SPIN_MAP_FD sample=" << sample + << " total_density=" << states[sample][0] << " absolute_density=" << point.absolute_density + << " radial_value=" << point.radial.value << " clipped_magnitude=" << point.clipped_magnitude + << " saturated=" << point.saturated << " step=" << step << " maximum_error=" << maximum_error << '\n'; + } +} + +TEST(NcggaRadial, SpinDensityMapDefinesAbsAndSaturationKinkConventions) +{ + const double eta = 0.1; + const ModuleXC::NcggaRadialPoint nonzero_radial = ModuleXC::make_ncgga_radial_point({{0.30, 0.0, 0.0}}, eta); + const ModuleXC::NcggaSpinMapPoint zero_density = ModuleXC::make_ncgga_spin_map_point(0.0, nonzero_radial); + EXPECT_TRUE(zero_density.saturated); + EXPECT_DOUBLE_EQ(zero_density.spin_density[0], 0.0); + EXPECT_DOUBLE_EQ(zero_density.spin_density[1], 0.0); + for (int spin = 0; spin < 2; ++spin) + { + for (int channel = 0; channel < 4; ++channel) + { + EXPECT_DOUBLE_EQ(zero_density.jacobian(spin, channel), 0.0); + } + } + + const ModuleXC::NcggaRadialPoint equality_radial = ModuleXC::make_ncgga_radial_point({{0.50, 0.0, 0.0}}, eta); + const ModuleXC::NcggaSpinMapPoint positive = ModuleXC::make_ncgga_spin_map_point(0.50, equality_radial); + const ModuleXC::NcggaSpinMapPoint negative = ModuleXC::make_ncgga_spin_map_point(-0.50, equality_radial); + EXPECT_TRUE(positive.saturated); + EXPECT_TRUE(negative.saturated); + EXPECT_DOUBLE_EQ(positive.jacobian(0, 0), 1.0); + EXPECT_DOUBLE_EQ(positive.jacobian(1, 0), 0.0); + EXPECT_DOUBLE_EQ(negative.jacobian(0, 0), -1.0); + EXPECT_DOUBLE_EQ(negative.jacobian(1, 0), 0.0); + for (int spin = 0; spin < 2; ++spin) + { + for (int channel = 1; channel < 4; ++channel) + { + EXPECT_DOUBLE_EQ(positive.jacobian(spin, channel), 0.0); + EXPECT_DOUBLE_EQ(negative.jacobian(spin, channel), 0.0); + } + } +} + +} // namespace diff --git a/source/source_hamilt/module_xc/xc_ncgga_radial.cpp b/source/source_hamilt/module_xc/xc_ncgga_radial.cpp new file mode 100644 index 00000000000..d01f4159b09 --- /dev/null +++ b/source/source_hamilt/module_xc/xc_ncgga_radial.cpp @@ -0,0 +1,95 @@ +#include "xc_ncgga_radial.h" + +#include +#include +#include + +namespace ModuleXC +{ + +double NcggaRadialPoint::jacobian(const int row, const int column) const +{ + const double identity = (row == column) ? 1.0 : 0.0; + return transverse_hessian * identity + (radial_hessian - transverse_hessian) * direction[row] * direction[column]; +} + +NcggaRadialPoint make_ncgga_radial_point(const std::array& magnetization, const double eta) +{ + if (!(eta > 0.0)) + { + throw std::invalid_argument("noncollinear GGA radial eta must be positive"); + } + + NcggaRadialPoint point; + const double magnitude = std::sqrt(magnetization[0] * magnetization[0] + magnetization[1] * magnetization[1] + + magnetization[2] * magnetization[2]); + if (magnitude == 0.0) + { + return point; + } + + for (int component = 0; component < 3; ++component) + { + point.direction[component] = magnetization[component] / magnitude; + } + + if (magnitude < eta) + { + const double x = magnitude / eta; + const double x2 = x * x; + const double x3 = x2 * x; + point.value = eta * x3 * (3.0 * x2 - 8.0 * x + 6.0); + point.transverse_hessian = x * (15.0 * x2 - 32.0 * x + 18.0) / eta; + point.radial_hessian = x * (60.0 * x2 - 96.0 * x + 36.0) / eta; + } + else + { + point.value = magnitude; + point.transverse_hessian = 1.0 / magnitude; + point.radial_hessian = 0.0; + } + + for (int component = 0; component < 3; ++component) + { + point.gradient[component] = point.transverse_hessian * magnetization[component]; + } + return point; +} + +double ncgga_lca_radial_eta() +{ + return 1.0e-3; +} + +double NcggaSpinMapPoint::jacobian(const int spin, const int channel) const +{ + if (channel == 0) + { + if (saturated) + { + return spin == 0 ? density_sign : 0.0; + } + return 0.5 * density_sign; + } + if (saturated) + { + return 0.0; + } + const double spin_sign = spin == 0 ? 0.5 : -0.5; + return spin_sign * radial.gradient[channel - 1]; +} + +NcggaSpinMapPoint make_ncgga_spin_map_point(const double total_density, const NcggaRadialPoint& radial) +{ + NcggaSpinMapPoint point; + point.radial = radial; + point.absolute_density = std::abs(total_density); + point.clipped_magnitude = std::min(radial.value, point.absolute_density); + point.spin_density[0] = 0.5 * (point.absolute_density + point.clipped_magnitude); + point.spin_density[1] = 0.5 * (point.absolute_density - point.clipped_magnitude); + point.density_sign = total_density > 0.0 ? 1.0 : total_density < 0.0 ? -1.0 : 0.0; + point.saturated = !(radial.value < point.absolute_density); + return point; +} + +} // namespace ModuleXC diff --git a/source/source_hamilt/module_xc/xc_ncgga_radial.h b/source/source_hamilt/module_xc/xc_ncgga_radial.h new file mode 100644 index 00000000000..13da072ed22 --- /dev/null +++ b/source/source_hamilt/module_xc/xc_ncgga_radial.h @@ -0,0 +1,60 @@ +#ifndef XC_NCGGA_RADIAL_H +#define XC_NCGGA_RADIAL_H + +#include + +namespace ModuleXC +{ + +struct NcggaRadialPoint +{ + // Value, gradient, and Hessian of the same radial scalar map. At zero, + // direction is represented by the zero vector because the scalar map has + // a unique zero gradient and zero Hessian there. + double value = 0.0; + std::array direction = {{0.0, 0.0, 0.0}}; + std::array gradient = {{0.0, 0.0, 0.0}}; + double transverse_hessian = 0.0; + double radial_hessian = 0.0; + + double jacobian(const int row, const int column) const; +}; + +// For r = |magnetization| and x = r / eta, the returned scalar is +// eta * x^3 * (3 x^2 - 8 x + 6), r < eta, +// r, r >= eta. +// The splice is C2 at both r = 0 and r = eta. Eta is explicit so this +// mathematical primitive does not choose policy for any XC mode. +NcggaRadialPoint make_ncgga_radial_point(const std::array& magnetization, const double eta); + +// The C2 regularization scale is part of the gga_grad=2 LCA functional, not a +// divide-by-zero guard. Other noncollinear modes do not inherit this policy. +double ncgga_lca_radial_eta(); + +struct NcggaSpinMapPoint +{ + NcggaRadialPoint radial; + double absolute_density = 0.0; + double clipped_magnitude = 0.0; + std::array spin_density = {{0.0, 0.0}}; + double density_sign = 0.0; + bool saturated = true; + + // spin is 0/1 for up/down; channel is 0 for the raw total density and + // 1..3 for mx,my,mz. + double jacobian(const int spin, const int channel) const; +}; + +// Compose a raw total density t=n+rho_core with one radial magnetization map: +// a = |t|, +// c = min(radial.value, a), +// rho_up = (a+c)/2, +// rho_down= (a-c)/2. +// The Jacobian follows that exact graph. At the abs kink t=0 it selects zero; +// at the clipping kink radial.value=a it selects the saturated branch. Eta and +// the radial policy remain explicit in make_ncgga_radial_point. +NcggaSpinMapPoint make_ncgga_spin_map_point(const double total_density, const NcggaRadialPoint& radial); + +} // namespace ModuleXC + +#endif From 0f95611f526e7774a589e9f300c9f338cedf5552 Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Mon, 7 Sep 2026 18:31:06 +0800 Subject: [PATCH 07/14] refactor(libxc): add weighted density and sigma sanitizer derivatives --- source/source_hamilt/module_xc/libxc_abacus.h | 22 ++ .../source_hamilt/module_xc/libxc_tools.cpp | 123 ++++++++++ .../module_xc/test/CMakeLists.txt | 2 +- .../module_xc/test/test_libxc_tools.cpp | 224 ++++++++++++++++++ 4 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 source/source_hamilt/module_xc/test/test_libxc_tools.cpp diff --git a/source/source_hamilt/module_xc/libxc_abacus.h b/source/source_hamilt/module_xc/libxc_abacus.h index 0e7b9f2df79..5ae57dde48c 100644 --- a/source/source_hamilt/module_xc/libxc_abacus.h +++ b/source/source_hamilt/module_xc/libxc_abacus.h @@ -19,6 +19,14 @@ class Charge; namespace XC_Functional_Libxc { + struct LibxcWeightedDerivatives + { + double energy_sum; + std::vector drho; + std::vector dsigma; + }; + + //------------------- // libxc_setup.cpp //------------------- @@ -137,6 +145,20 @@ namespace XC_Functional_Libxc const std::vector &rho, std::vector exc); + // Reverse the density and sigma sanitizers for the weighted energy + // accumulated by ABACUS. The result is in Hartree units and excludes the + // real-space grid weight and ModuleBase::e2. + extern LibxcWeightedDerivatives make_libxc_weighted_derivatives( + const xc_func_type &func, + const int nspin, + const std::size_t nrxx, + const std::vector &sgn, + const std::vector &rho, + const std::vector &sigma, + const std::vector &exc, + const std::vector &vrho, + const std::vector &vsigma); + // converting vtxc and v from vrho and vsigma (libxc=>abacus) extern std::pair convert_vtxc_v( const xc_func_type &func, diff --git a/source/source_hamilt/module_xc/libxc_tools.cpp b/source/source_hamilt/module_xc/libxc_tools.cpp index 9023f00901f..8dfd3828a64 100644 --- a/source/source_hamilt/module_xc/libxc_tools.cpp +++ b/source/source_hamilt/module_xc/libxc_tools.cpp @@ -211,6 +211,129 @@ double XC_Functional_Libxc::convert_etxc( return etxc; } +XC_Functional_Libxc::LibxcWeightedDerivatives +XC_Functional_Libxc::make_libxc_weighted_derivatives( + const xc_func_type &func, + const int nspin, + const std::size_t nrxx, + const std::vector &sgn, + const std::vector &rho, + const std::vector &sigma, + const std::vector &exc, + const std::vector &vrho, + const std::vector &vsigma) +{ + assert(nspin == 1 || nspin == 2); + assert(sgn.size() == nrxx * nspin); + assert(rho.size() == nrxx * nspin); + assert(exc.size() == nrxx); + assert(vrho.size() == nrxx * nspin); + assert(func.nspin == nspin); + + const bool is_gga + = func.info->family == XC_FAMILY_GGA || func.info->family == XC_FAMILY_HYB_GGA; + const std::size_t nsigma = nspin == 1 ? 1 : 3; + if (is_gga) + { + assert(sigma.size() == nrxx * nsigma); + assert(vsigma.size() == nrxx * nsigma); + } + + LibxcWeightedDerivatives weighted; + weighted.energy_sum = 0.0; + weighted.drho.assign(nrxx * nspin, 0.0); + if (is_gga) + { + weighted.dsigma.assign(nrxx * nsigma, 0.0); + } + + const double density_floor = func.dens_threshold; + const double sigma_floor = func.sigma_threshold * func.sigma_threshold; + double energy_sum = 0.0; + #ifdef _OPENMP + #pragma omp parallel for reduction(+:energy_sum) schedule(static, 512) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + double raw_density_sum = 0.0; + double sanitized_density_sum = 0.0; + double energy_weight = 0.0; + for (int is = 0; is < nspin; ++is) + { + const std::size_t index = ir * nspin + is; + raw_density_sum += rho[index]; + sanitized_density_sum += std::max(density_floor, rho[index]); + energy_weight += sgn[index] * rho[index]; + } + + // Libxc leaves all outputs zero below the total-density threshold. + // Inside that branch, the ABACUS weighted energy is locally constant. + if (raw_density_sum < density_floor) + { + continue; + } + + // ABACUS accumulates M*eps while Libxc differentiates Y*eps after + // y_s=max(T,rho_s). Hence d eps/d y_s=(vrho_s-eps)/Y. + energy_sum += energy_weight * exc[ir]; + const double libxc_weight = energy_weight / sanitized_density_sum; + for (int is = 0; is < nspin; ++is) + { + const std::size_t index = ir * nspin + is; + const double floor_jacobian = rho[index] > density_floor ? 1.0 : 0.0; + weighted.drho[index] + = sgn[index] * exc[ir] + + libxc_weight * floor_jacobian * (vrho[index] - exc[ir]); + } + + if (!is_gga) + { + continue; + } + + if (nspin == 1) + { + const double floor_jacobian = sigma[ir] > sigma_floor ? 1.0 : 0.0; + weighted.dsigma[ir] = libxc_weight * floor_jacobian * vsigma[ir]; + continue; + } + + const std::size_t sigma_index = 3 * ir; + const double sigma_uu = sigma[sigma_index]; + const double sigma_ud = sigma[sigma_index + 1]; + const double sigma_dd = sigma[sigma_index + 2]; + const double jacobian_uu = sigma_uu > sigma_floor ? 1.0 : 0.0; + const double jacobian_dd = sigma_dd > sigma_floor ? 1.0 : 0.0; + const double sanitized_uu = std::max(sigma_floor, sigma_uu); + const double sanitized_dd = std::max(sigma_floor, sigma_dd); + const double cross_limit = 0.5 * (sanitized_uu + sanitized_dd); + + double cross_to_diagonal = 0.0; + double cross_jacobian = 1.0; + if (sigma_ud < -cross_limit) + { + cross_to_diagonal = -0.5; + cross_jacobian = 0.0; + } + else if (sigma_ud > cross_limit) + { + cross_to_diagonal = 0.5; + cross_jacobian = 0.0; + } + + weighted.dsigma[sigma_index] + = libxc_weight * jacobian_uu + * (vsigma[sigma_index] + cross_to_diagonal * vsigma[sigma_index + 1]); + weighted.dsigma[sigma_index + 1] + = libxc_weight * cross_jacobian * vsigma[sigma_index + 1]; + weighted.dsigma[sigma_index + 2] + = libxc_weight * jacobian_dd + * (vsigma[sigma_index + 2] + cross_to_diagonal * vsigma[sigma_index + 1]); + } + weighted.energy_sum = energy_sum; + return weighted; +} + // converting vtxc and v from vrho and vsigma (libxc=>abacus) std::pair XC_Functional_Libxc::convert_vtxc_v( const xc_func_type &func, diff --git a/source/source_hamilt/module_xc/test/CMakeLists.txt b/source/source_hamilt/module_xc/test/CMakeLists.txt index 3d9dac3a5de..e5ce9e053a3 100644 --- a/source/source_hamilt/module_xc/test/CMakeLists.txt +++ b/source/source_hamilt/module_xc/test/CMakeLists.txt @@ -62,7 +62,7 @@ AddTest( AddTest( TARGET MODULE_HAMILT_XCTest_VXC LIBS parameter MPI::MPI_CXX Libxc::xc psi device container - SOURCES test_xc5.cpp ../xc_grad.cpp ../xc_grad_prepare.cpp ../xc_grad_kernel.cpp ../xc_grad_assemble.cpp ../xc_grad_wfc.cpp ../xc_grad_utils.cpp ../xc_functional.cpp + SOURCES test_xc5.cpp test_libxc_tools.cpp ../xc_grad.cpp ../xc_grad_prepare.cpp ../xc_grad_kernel.cpp ../xc_grad_assemble.cpp ../xc_grad_wfc.cpp ../xc_grad_utils.cpp ../xc_functional.cpp ../xc_lda_wrap.cpp ../xc_gga_wrap.cpp ../libxc_setup.cpp ../libxc_lda_wrap.cpp diff --git a/source/source_hamilt/module_xc/test/test_libxc_tools.cpp b/source/source_hamilt/module_xc/test/test_libxc_tools.cpp new file mode 100644 index 00000000000..0a5afe1ec3f --- /dev/null +++ b/source/source_hamilt/module_xc/test/test_libxc_tools.cpp @@ -0,0 +1,224 @@ +#include "../libxc_abacus.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include + +#ifdef __LIBXC + +TEST(LibxcSanitizer, FullPolarizationUsesTheWeightedEnergyDerivative) +{ + const std::array functional_ids = {{XC_LDA_X, XC_LDA_C_PZ}}; + const double density = 0.45; + + for (std::size_t ifunc = 0; ifunc < functional_ids.size(); ++ifunc) + { + xc_func_type func; + ASSERT_EQ(xc_func_init(&func, functional_ids[ifunc], XC_POLARIZED), 0); + xc_func_set_dens_threshold(&func, 1.0e-6); + + const auto evaluate = [&func](const double rho_up, + const double rho_down, + XC_Functional_Libxc::LibxcWeightedDerivatives* const weighted) { + const std::vector rho = {rho_up, rho_down}; + const std::vector mask = {1.0, 1.0}; + std::vector exc(1, 0.0); + std::vector vrho(2, 0.0); + xc_lda_exc_vxc(&func, 1, rho.data(), exc.data(), vrho.data()); + if (weighted != nullptr) + { + *weighted = XC_Functional_Libxc::make_libxc_weighted_derivatives(func, + 2, + 1, + mask, + rho, + std::vector(), + exc, + vrho, + std::vector()); + } + return (rho_up + rho_down) * exc[0]; + }; + + XC_Functional_Libxc::LibxcWeightedDerivatives weighted; + const double energy = evaluate(density, 0.0, &weighted); + ASSERT_EQ(weighted.drho.size(), 2U); + EXPECT_DOUBLE_EQ(weighted.energy_sum, energy); + + const double steps[] = {1.0e-3, 5.0e-4, 2.5e-4, 1.25e-4}; + std::array errors = {{0.0, 0.0, 0.0, 0.0}}; + for (std::size_t ieps = 0; ieps < 4; ++ieps) + { + const double step = steps[ieps]; + const double finite_difference + = (evaluate(density + step, 0.0, nullptr) - evaluate(density - step, 0.0, nullptr)) / (2.0 * step); + errors[ieps] = std::abs(weighted.drho[0] - finite_difference); + EXPECT_LE(errors[ieps], 2.0e-7 * std::max(1.0, std::abs(weighted.drho[0]))) + << "functional_id=" << functional_ids[ifunc] << ", step=" << step; + } + EXPECT_LE(errors[1], 0.4 * errors[0] + 1.0e-12); + EXPECT_LE(errors[2], 0.4 * errors[1] + 1.0e-12); + + const double inactive_density = 0.5 * func.dens_threshold; + const double inactive_step = 0.2 * func.dens_threshold; + XC_Functional_Libxc::LibxcWeightedDerivatives inactive_weighted; + evaluate(density, inactive_density, &inactive_weighted); + const double inactive_finite_difference = (evaluate(density, inactive_density + inactive_step, nullptr) + - evaluate(density, inactive_density - inactive_step, nullptr)) + / (2.0 * inactive_step); + EXPECT_NEAR(inactive_finite_difference, + inactive_weighted.drho[1], + 2.0e-8 * std::max(1.0, std::abs(inactive_finite_difference))); + xc_func_end(&func); + } +} + +TEST(LibxcSanitizer, GgaSigmaReverseMatchesTheWeightedEnergy) +{ + xc_func_type func; + ASSERT_EQ(xc_func_init(&func, XC_GGA_C_PBE, XC_POLARIZED), 0); + xc_func_set_dens_threshold(&func, 1.0e-6); + xc_func_set_sigma_threshold(&func, 1.0e-2); + + const std::vector mask = {1.0, 0.0}; + const std::vector density = {0.40, 0.20}; + const double sigma_floor = func.sigma_threshold * func.sigma_threshold; + const std::array, 5> sigma_states = {{{{0.040, 0.010, 0.030}}, + {{0.5 * sigma_floor, 0.0, 0.030}}, + {{0.040, 0.200, 0.030}}, + {{0.040, -0.200, 0.030}}, + {{0.5 * sigma_floor, 0.200, 0.030}}}}; + + const auto evaluate = [&func, &mask](const std::vector& rho, + const std::vector& sigma, + XC_Functional_Libxc::LibxcWeightedDerivatives* const weighted) { + std::vector exc(1, 0.0); + std::vector vrho(2, 0.0); + std::vector vsigma(3, 0.0); + xc_gga_exc_vxc(&func, 1, rho.data(), sigma.data(), exc.data(), vrho.data(), vsigma.data()); + if (weighted != nullptr) + { + *weighted + = XC_Functional_Libxc::make_libxc_weighted_derivatives(func, 2, 1, mask, rho, sigma, exc, vrho, vsigma); + } + return (mask[0] * rho[0] + mask[1] * rho[1]) * exc[0]; + }; + + for (std::size_t icase = 0; icase < sigma_states.size(); ++icase) + { + std::vector sigma(sigma_states[icase].begin(), sigma_states[icase].end()); + XC_Functional_Libxc::LibxcWeightedDerivatives weighted; + const double energy = evaluate(density, sigma, &weighted); + EXPECT_DOUBLE_EQ(weighted.energy_sum, energy); + ASSERT_EQ(weighted.drho.size(), 2U); + ASSERT_EQ(weighted.dsigma.size(), 3U); + + if (icase == 0) + { + for (int component = 0; component < 2; ++component) + { + std::vector perturbed_density = density; + const double step = 1.0e-6; + perturbed_density[component] += step; + const double energy_plus = evaluate(perturbed_density, sigma, nullptr); + perturbed_density[component] -= 2.0 * step; + const double energy_minus = evaluate(perturbed_density, sigma, nullptr); + const double finite_difference = (energy_plus - energy_minus) / (2.0 * step); + EXPECT_NEAR(finite_difference, + weighted.drho[component], + 2.0e-7 * std::max(1.0, std::abs(weighted.drho[component]))); + } + } + + if (icase == 1 || icase == 4) + { + EXPECT_DOUBLE_EQ(weighted.dsigma[0], 0.0); + } + if (icase >= 2) + { + EXPECT_DOUBLE_EQ(weighted.dsigma[1], 0.0); + } + + for (int component = 0; component < 3; ++component) + { + const double step = 1.0e-6; + sigma[component] += step; + const double energy_plus = evaluate(density, sigma, nullptr); + sigma[component] -= 2.0 * step; + const double energy_minus = evaluate(density, sigma, nullptr); + sigma[component] += step; + const double finite_difference = (energy_plus - energy_minus) / (2.0 * step); + EXPECT_NEAR(finite_difference, + weighted.dsigma[component], + 2.0e-7 * std::max(1.0, std::abs(weighted.dsigma[component]))) + << "case=" << icase << ", sigma component=" << component; + } + } + xc_func_end(&func); +} + +TEST(LibxcSanitizer, UnpolarizedSelfSigmaReverseMatchesTheWeightedEnergy) +{ + xc_func_type func; + ASSERT_EQ(xc_func_init(&func, XC_GGA_C_PBE, XC_UNPOLARIZED), 0); + xc_func_set_dens_threshold(&func, 1.0e-6); + xc_func_set_sigma_threshold(&func, 1.0e-2); + + const std::vector mask = {1.0}; + const std::vector density = {0.40}; + const double sigma_floor = func.sigma_threshold * func.sigma_threshold; + + const auto evaluate = [&func, &mask, &density](const double sigma_value, + XC_Functional_Libxc::LibxcWeightedDerivatives* const weighted) { + const std::vector sigma = {sigma_value}; + std::vector exc(1, 0.0); + std::vector vrho(1, 0.0); + std::vector vsigma(1, 0.0); + xc_gga_exc_vxc(&func, 1, density.data(), sigma.data(), exc.data(), vrho.data(), vsigma.data()); + if (weighted != nullptr) + { + *weighted = XC_Functional_Libxc::make_libxc_weighted_derivatives(func, + 1, + 1, + mask, + density, + sigma, + exc, + vrho, + vsigma); + } + return mask[0] * density[0] * exc[0]; + }; + + const double below_floor_sigma = 0.5 * sigma_floor; + const double below_floor_step = 0.2 * sigma_floor; + XC_Functional_Libxc::LibxcWeightedDerivatives below_floor_weighted; + const double below_floor_energy = evaluate(below_floor_sigma, &below_floor_weighted); + ASSERT_EQ(below_floor_weighted.dsigma.size(), 1U); + EXPECT_DOUBLE_EQ(below_floor_weighted.energy_sum, below_floor_energy); + EXPECT_DOUBLE_EQ(below_floor_weighted.dsigma[0], 0.0); + const double below_floor_finite_difference = (evaluate(below_floor_sigma + below_floor_step, nullptr) + - evaluate(below_floor_sigma - below_floor_step, nullptr)) + / (2.0 * below_floor_step); + EXPECT_NEAR(below_floor_finite_difference, below_floor_weighted.dsigma[0], 1.0e-12); + + const double above_floor_sigma = 0.040; + const double above_floor_step = 1.0e-6; + XC_Functional_Libxc::LibxcWeightedDerivatives above_floor_weighted; + const double above_floor_energy = evaluate(above_floor_sigma, &above_floor_weighted); + ASSERT_EQ(above_floor_weighted.dsigma.size(), 1U); + EXPECT_DOUBLE_EQ(above_floor_weighted.energy_sum, above_floor_energy); + EXPECT_GT(std::abs(above_floor_weighted.dsigma[0]), 1.0e-12); + const double above_floor_finite_difference = (evaluate(above_floor_sigma + above_floor_step, nullptr) + - evaluate(above_floor_sigma - above_floor_step, nullptr)) + / (2.0 * above_floor_step); + EXPECT_NEAR(above_floor_finite_difference, + above_floor_weighted.dsigma[0], + 2.0e-7 * std::max(1.0, std::abs(above_floor_weighted.dsigma[0]))); + + xc_func_end(&func); +} + +#endif From eeb18c5bc8cd6681128217f23efee5dcfb0b680a Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Mon, 7 Sep 2026 18:40:38 +0800 Subject: [PATCH 08/14] feat(xc): add gga_grad 1 and 2 with discrete variational potentials Based-on: https://github.com/deepmodeling/abacus-develop/pull/7758 Co-authored-by: dyzheng --- docs/advanced/input_files/input-main.md | 12 + docs/parameters.yaml | 13 + source/source_estate/module_pot/pot_xc.cpp | 11 +- .../source_estate/module_pot/pot_xc_fdm.cpp | 16 +- source/source_hamilt/module_xc/CMakeLists.txt | 3 +- source/source_hamilt/module_xc/libxc_abacus.h | 71 +- source/source_hamilt/module_xc/libxc_pot.cpp | 288 ++- .../source_hamilt/module_xc/libxc_tools.cpp | 206 +- .../module_xc/test/CMakeLists.txt | 45 +- .../source_hamilt/module_xc/test/test_xc3.cpp | 24 +- .../source_hamilt/module_xc/test/test_xc5.cpp | 389 +++- .../test/test_xc_functional_ncgga_sf.cpp | 1682 +++++++++++++++++ .../source_hamilt/module_xc/xc_functional.h | 2 + .../module_xc/xc_functional_ncgga_sf.cpp | 239 +++ .../module_xc/xc_functional_ncgga_sf.h | 37 + source/source_hamilt/module_xc/xc_grad.cpp | 4 + .../module_xc/xc_grad_internal.h | 1 + .../module_xc/xc_grad_prepare.cpp | 5 +- source/source_hamilt/module_xc/xc_pot.cpp | 15 +- source/source_io/module_hs/write_h_terms.cpp | 8 +- .../module_parameter/input_parameter.h | 1 + .../module_parameter/read_inp_estruc.cpp | 21 + source/source_io/test/read_input_ptest.cpp | 33 + source/source_io/test/support/INPUT | 2 + source/source_pw/module_pwdft/force_pw_cc.cpp | 16 +- source/source_pw/module_pwdft/stress_cc.cpp | 16 +- source/source_pw/module_pwdft/stress_gga.cpp | 3 +- 27 files changed, 2984 insertions(+), 179 deletions(-) create mode 100644 source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp create mode 100644 source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp create mode 100644 source/source_hamilt/module_xc/xc_functional_ncgga_sf.h diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 567f0f87640..de4e12456e4 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -94,6 +94,7 @@ - [pseudo\_rcut](#pseudo_rcut) - [pseudo\_mesh](#pseudo_mesh) - [nspin](#nspin) + - [gga\_grad](#gga_grad) - [smearing\_method](#smearing_method) - [smearing\_sigma](#smearing_sigma) - [smearing\_sigma\_temp](#smearing_sigma_temp) @@ -1326,6 +1327,17 @@ - 4: Noncollinear or spin-orbit calculations. Set nspin to 4 explicitly when noncolin or lspinorb is enabled. - **Default**: 1 +### gga_grad + +- **Type**: Integer +- **Description**: Selects the local spin mapping for LDA/GGA functionals in magnetic nspin=4 calculations. + - 0: preserves the original algorithm (default). + - 1: uses the local magnetization magnitude instead of the global quantization axis in the built-in GGA gradient correction. For LIBXC functionals, 0 and 1 are equivalent. + - 2: uses a C2-regularized magnetization magnitude with eta = 1e-3 in atomic density units. The spin densities are (abs(n + rho_core) +/- min(S_eta(m), abs(n + rho_core)))/2. GGA gradients are the local-map Jacobian applied to the FFT gradients of the four density channels. The potential reverses this same discrete energy graph, including the radial Hessian and density/sigma clipping branches; the GGA stress uses the corresponding metric derivative. + For r = |m| and x = r/eta, S_eta = eta*x^3*(3*x^2 - 8*x + 6) for r < eta, and S_eta = r otherwise. The regularization is part of the functional definition, including its first and second derivatives. + Mode 2 also uses this local map for the LDA contribution. Other spin configurations retain their existing behavior. +- **Default**: 0 + ### smearing_method - **Type**: String diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 3cc85903fc3..923d59b6308 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -686,6 +686,19 @@ parameters: default_value: "1" unit: "" availability: "" + - name: gga_grad + category: Electronic structure + type: Integer + description: | + Selects the local spin mapping for LDA/GGA functionals in magnetic nspin=4 calculations. + * 0: preserves the original algorithm (default). + * 1: uses the local magnetization magnitude instead of the global quantization axis in the built-in GGA gradient correction. For LIBXC functionals, 0 and 1 are equivalent. + * 2: uses a C2-regularized magnetization magnitude with eta = 1e-3 in atomic density units. The spin densities are (abs(n + rho_core) +/- min(S_eta(m), abs(n + rho_core)))/2. GGA gradients are the local-map Jacobian applied to the FFT gradients of the four density channels. The potential reverses this same discrete energy graph, including the radial Hessian and density/sigma clipping branches; the GGA stress uses the corresponding metric derivative. + For r = |m| and x = r/eta, S_eta = eta*x^3*(3*x^2 - 8*x + 6) for r < eta, and S_eta = r otherwise. The regularization is part of the functional definition, including its first and second derivatives. + Mode 2 also uses this local map for the LDA contribution. Other spin configurations retain their existing behavior. + default_value: "0" + unit: "" + availability: "" - name: smearing_method category: Electronic structure type: String diff --git a/source/source_estate/module_pot/pot_xc.cpp b/source/source_estate/module_pot/pot_xc.cpp index eb311cc9225..c7ba0da2208 100644 --- a/source/source_estate/module_pot/pot_xc.cpp +++ b/source/source_estate/module_pot/pot_xc.cpp @@ -14,9 +14,11 @@ namespace elecstate void PotXC::cal_v_eff(const Charge*const chg, const UnitCell*const ucell, ModuleBase::matrix& v_eff) { + const Parameter& parameters = PARAM; ModuleBase::TITLE("PotXC", "cal_veff"); ModuleBase::timer::start("PotXC", "cal_veff"); const int nrxx_current = chg->nrxx; + const int nspin = parameters.inp.nspin; //---------------------------------------------------------- // calculate the exchange-correlation potential @@ -33,7 +35,7 @@ void PotXC::cal_v_eff(const Charge*const chg, const UnitCell*const ucell, Module #endif const std::tuple etxc_vtxc_v = XC_Functional_Libxc::v_xc_meta(XC_Functional::get_func_id(), nrxx_current, ucell->omega, ucell->tpiba, chg, - PARAM.inp.nspin, hybrid_alpha, hse_omega); + nspin, hybrid_alpha, hse_omega); *(this->etxc_) = std::get<0>(etxc_vtxc_v); *(this->vtxc_) = std::get<1>(etxc_vtxc_v); v_eff += std::get<2>(etxc_vtxc_v); @@ -52,9 +54,10 @@ void PotXC::cal_v_eff(const Charge*const chg, const UnitCell*const ucell, Module #endif const std::tuple etxc_vtxc_v = XC_Functional::v_xc(nrxx_current, chg, ucell, - PARAM.inp.nspin, - PARAM.globalv.domag, - PARAM.globalv.domag_z, + nspin, + parameters.globalv.domag, + parameters.globalv.domag_z, + parameters.inp.gga_grad, hybrid_alpha, hse_omega); *(this->etxc_) = std::get<0>(etxc_vtxc_v); diff --git a/source/source_estate/module_pot/pot_xc_fdm.cpp b/source/source_estate/module_pot/pot_xc_fdm.cpp index 03349fe4e7e..1b0d813f5d8 100644 --- a/source/source_estate/module_pot/pot_xc_fdm.cpp +++ b/source/source_estate/module_pot/pot_xc_fdm.cpp @@ -16,6 +16,7 @@ PotXC_FDM::PotXC_FDM( const UnitCell*const ucell) : chg_0(chg_0_in) { + const Parameter& parameters = PARAM; this->rho_basis_ = rho_basis_in; this->dynamic_mode = true; this->fixed_mode = false; @@ -28,9 +29,10 @@ PotXC_FDM::PotXC_FDM( #endif const std::tuple etxc_vtxc_v_0 = XC_Functional::v_xc(this->chg_0->nrxx, this->chg_0, ucell, - PARAM.inp.nspin, - PARAM.globalv.domag, - PARAM.globalv.domag_z, + parameters.inp.nspin, + parameters.globalv.domag, + parameters.globalv.domag_z, + parameters.inp.gga_grad, hybrid_alpha, hse_omega); this->v_xc_0 = std::get<2>(etxc_vtxc_v_0); @@ -41,6 +43,7 @@ void PotXC_FDM::cal_v_eff( const UnitCell*const ucell, ModuleBase::matrix& v_eff) { + const Parameter& parameters = PARAM; ModuleBase::TITLE("PotXC_FDM", "cal_veff"); ModuleBase::timer::start("PotXC_FDM", "cal_veff"); @@ -66,9 +69,10 @@ void PotXC_FDM::cal_v_eff( #endif const std::tuple etxc_vtxc_v_01 = XC_Functional::v_xc(chg_01.nrxx, &chg_01, ucell, - PARAM.inp.nspin, - PARAM.globalv.domag, - PARAM.globalv.domag_z, + parameters.inp.nspin, + parameters.globalv.domag, + parameters.globalv.domag_z, + parameters.inp.gga_grad, hybrid_alpha, hse_omega); const ModuleBase::matrix &v_xc_01 = std::get<2>(etxc_vtxc_v_01); diff --git a/source/source_hamilt/module_xc/CMakeLists.txt b/source/source_hamilt/module_xc/CMakeLists.txt index 36300e091c1..495c5ae1997 100644 --- a/source/source_hamilt/module_xc/CMakeLists.txt +++ b/source/source_hamilt/module_xc/CMakeLists.txt @@ -1,8 +1,9 @@ add_library( xc_ OBJECT - xc_ncgga_radial.cpp xc_functional.cpp + xc_functional_ncgga_sf.cpp + xc_ncgga_radial.cpp xc_pot.cpp xc_grad.cpp xc_grad_prepare.cpp diff --git a/source/source_hamilt/module_xc/libxc_abacus.h b/source/source_hamilt/module_xc/libxc_abacus.h index 5ae57dde48c..022cfc11122 100644 --- a/source/source_hamilt/module_xc/libxc_abacus.h +++ b/source/source_hamilt/module_xc/libxc_abacus.h @@ -5,10 +5,12 @@ #include "source_base/matrix.h" #include "source_base/vector3.h" +#include "xc_ncgga_radial.h" #include #include +#include #include #include @@ -26,29 +28,41 @@ namespace XC_Functional_Libxc std::vector dsigma; }; + // Complete forward data for the gga_grad=2 noncollinear Libxc graph: + // rho_s = N_s(x), + // g_s = sum_A (d N_s / d x_A) G_h x_A. + // Keeping the local map and all input gradients together lets the reverse + // use the exact same branch choices and radial Hessian as the forward. + struct NclSfDiscreteData + { + std::vector spin_map; + std::vector rho; + std::vector>> spin_gradient; + std::array>, 3> grad_m; + }; //------------------- // libxc_setup.cpp //------------------- // sets functional type, which allows combination of LIBXC keyword connected by "+" - // for example, "XC_LDA_X+XC_LDA_C_PZ" + // for example: "XC_LDA_X+XC_LDA_C_PZ" extern std::pair> set_xc_type_libxc(const std::string& xc_func_in); /** * @brief instantiate the XC functional by its ID, and set the external parameters if provided. - * + * * @param func_id libxc ID of functional, see https://libxc.gitlab.io/functionals/ for details * @param xc_polarized 0: unpolarized, 1: spin-polarized - * @return std::vector - * + * @return std::vector + * * @note the functionality of this method is extended by supporting the user-defined - * external parameters of xc. However, there are several functionals' external - * parameters are pre-defined in the code, which herein we call those are - * "in-built" parameters. If the same functional ID is found in both in-built + * external parameters of xc. However, there are several functionals' external + * parameters are pre-defined in the code, which herein we call those are + * "in-built" parameters. If the same functional ID is found in both in-built * and external parameters, the external parameters will overwrite the in-built ones. * The external parameters can be passed here by keywords xc_exch_ext and - * xc_corr_ext in the input file. The expected format would be an XC ID + * xc_corr_ext in the input file. The expected format would be an XC ID * followed by a list of parameters. */ extern std::vector init_func( @@ -73,10 +87,24 @@ namespace XC_Functional_Libxc const int nspin, const bool domag, const bool domag_z, + const int gga_grad, const std::map* scaling_factor, const double hybrid_alpha, const double hse_omega); + // Reciprocal-metric derivative of the exact gga_grad=2 Libxc energy + // graph. The returned lower-triangular tensor is the unnormalized local + // grid sum; Stress_Func performs the pool reduction and divides by nxyz. + extern void gradcorr_ncgga_sf_libxc( + const std::vector& func_id, + const std::size_t nrxx, + const double tpiba, + const Charge* const chr, + const std::map* scaling_factor, + const double hybrid_alpha, + const double hse_omega, + std::vector& stress_gga); + // for mGGA functional extern std::tuple v_xc_meta( const std::vector &func_id, @@ -105,6 +133,25 @@ namespace XC_Functional_Libxc const std::size_t nrxx, const Charge* const chr); + // Build the exact gga_grad=2 local spin map and, when requested, its + // projected FFT-gradient graph. LDA-only callers set need_gradient=false. + extern NclSfDiscreteData make_ncl_sf_discrete_data( + const std::size_t nrxx, + const double tpiba, + const Charge* const chr, + const bool need_gradient); + + // Reverse one aggregate of all scaled Libxc components. The returned + // potential is already in (n,mx,my,mz) representation. An empty dsigma + // selects the LDA-only local reverse and performs no FFT divergence. + extern ModuleBase::matrix reverse_ncl_sf_discrete( + const std::size_t nrxx, + const NclSfDiscreteData& data, + const std::vector& drho, + const std::vector& dsigma, + const double tpiba, + const Charge* const chr); + // calculating grho extern std::vector>> cal_gdr( const int nspin, @@ -159,7 +206,7 @@ namespace XC_Functional_Libxc const std::vector &vrho, const std::vector &vsigma); - // converting vtxc and v from vrho and vsigma (libxc=>abacus) + // Convert collinear LibXC derivatives to the potential. extern std::pair convert_vtxc_v( const xc_func_type &func, const int nspin, @@ -183,12 +230,14 @@ namespace XC_Functional_Libxc const Charge* const chr); // convert v for NSPIN=4 + // has_mag: whether the calculation has (noncollinear) magnetization, + // i.e. domag || domag_z extern ModuleBase::matrix convert_v_nspin4( const std::size_t nrxx, const Charge* const chr, const std::vector &amag, - const ModuleBase::matrix &v); - + const ModuleBase::matrix &v, + const bool has_mag); //------------------- // libxc_lda_wrap.cpp diff --git a/source/source_hamilt/module_xc/libxc_pot.cpp b/source/source_hamilt/module_xc/libxc_pot.cpp index 7e69a9a7804..9955d7626b9 100644 --- a/source/source_hamilt/module_xc/libxc_pot.cpp +++ b/source/source_hamilt/module_xc/libxc_pot.cpp @@ -4,7 +4,6 @@ #include "libxc_abacus.h" #include "source_estate/module_charge/charge.h" #include "source_base/global_variable.h" -#include "source_io/module_parameter/parameter.h" #include "source_base/parallel_reduce.h" #include "source_base/timer.h" #include "source_base/tool_title.h" @@ -17,25 +16,31 @@ #include #include -std::tuple XC_Functional_Libxc::v_xc_libxc( // Peize Lin update for nspin==4 at 2023.01.14 - const std::vector &func_id, - const int &nrxx, // number of real-space grid - const double &omega, // volume of cell - const double tpiba, - const Charge* const chr, - const int nspin_in, - const bool domag, - const bool domag_z, - const std::map* scaling_factor, - const double hybrid_alpha, - const double hse_omega) +std::tuple XC_Functional_Libxc::v_xc_libxc( // Peize Lin update for nspin==4 at + // 2023.01.14 + const std::vector& func_id, + const int& nrxx, // number of real-space grid + const double& omega, // volume of cell + const double tpiba, + const Charge* const chr, + const int nspin_in, + const bool domag, + const bool domag_z, + const int gga_grad, + const std::map* scaling_factor, + const double hybrid_alpha, + const double hse_omega) { - ModuleBase::TITLE("XC_Functional_Libxc","v_xc_libxc"); - ModuleBase::timer::start("XC_Functional_Libxc","v_xc_libxc"); + ModuleBase::TITLE("XC_Functional_Libxc", "v_xc_libxc"); + ModuleBase::timer::start("XC_Functional_Libxc", "v_xc_libxc"); - const int nspin = - (nspin_in == 1 || ( nspin_in ==4 && !domag && !domag_z)) - ? 1 : 2; + const int nspin = (nspin_in == 1 || (nspin_in == 4 && !domag && !domag_z)) ? 1 : 2; + + // For nspin=4 with noncollinear magnetism, gga_grad=2 selects the + // regularized projected local-collinear graph; gga_grad=0/1 keeps the + // original collinear algorithm. + const bool has_mag = domag || domag_z; + const bool use_lca = (nspin_in == 4) && has_mag && gga_grad == 2; //---------------------------------------------------------- // xc_func_type is defined in Libxc package @@ -45,52 +50,81 @@ std::tuple XC_Functional_Libxc::v_xc_libxc( / //---------------------------------------------------------- std::vector funcs = XC_Functional_Libxc::init_func( - /* func_id = */ func_id, - /* xc_polarized = */ (1==nspin) ? XC_UNPOLARIZED : XC_POLARIZED, + /* func_id = */ func_id, + /* xc_polarized = */ (1 == nspin) ? XC_UNPOLARIZED : XC_POLARIZED, /* hybrid_alpha = */ hybrid_alpha, /* hse_omega = */ hse_omega); - const bool is_gga = [&funcs]() - { - for( xc_func_type &func : funcs ) + const bool is_gga = [&funcs]() { + for (xc_func_type& func: funcs) { - switch( func.info->family ) + switch (func.info->family) { - case XC_FAMILY_GGA: - case XC_FAMILY_HYB_GGA: - return true; + case XC_FAMILY_GGA: + case XC_FAMILY_HYB_GGA: + return true; } } return false; }(); // converting rho + // For nspin=4, the charge density has 4 components: + // rho[0] = total charge, rho[1..3] = magnetization (mx, my, mz) + // libxc works with spin-up/spin-down densities: + // rho_up = 0.5*(rho[0] + |m|), rho_dn = 0.5*(rho[0] - |m|) std::vector rho; std::vector amag; - if(1==nspin || 2==nspin_in) + XC_Functional_Libxc::NclSfDiscreteData sf_data; + if (1 == nspin || 2 == nspin_in) { rho = XC_Functional_Libxc::convert_rho(nspin, nrxx, chr); } + else if (use_lca) + { + // gga_grad=2 uses one complete local map for both the Libxc density + // input and the projected FFT-gradient graph. LDA-only functionals + // need the same local map but do not pay for gradients. + sf_data = XC_Functional_Libxc::make_ncl_sf_discrete_data(nrxx, tpiba, chr, is_gga); + rho = sf_data.rho; + } else { - std::tuple,std::vector> rho_amag = XC_Functional_Libxc::convert_rho_amag_nspin4(nspin, nrxx, chr); + std::tuple, std::vector> rho_amag + = XC_Functional_Libxc::convert_rho_amag_nspin4(nspin, nrxx, chr); rho = std::get<0>(std::move(rho_amag)); amag = std::get<1>(std::move(rho_amag)); } std::vector>> gdr; std::vector sigma; - if(is_gga) + if (is_gga) { - gdr = XC_Functional_Libxc::cal_gdr(nspin, nrxx, rho, tpiba, chr); + if (use_lca) + { + gdr = sf_data.spin_gradient; + } + else + gdr = XC_Functional_Libxc::cal_gdr(nspin, nrxx, rho, tpiba, chr); + sigma = XC_Functional_Libxc::convert_sigma(gdr); } double etxc = 0.0; double vtxc = 0.0; - ModuleBase::matrix v(nspin,nrxx); + ModuleBase::matrix v(use_lca ? 4 : nspin, nrxx); + XC_Functional_Libxc::LibxcWeightedDerivatives sf_weighted; + if (use_lca) + { + sf_weighted.energy_sum = 0.0; + sf_weighted.drho.assign(nrxx * nspin, 0.0); + if (is_gga) + { + sf_weighted.dsigma.assign(nrxx * 3, 0.0); + } + } - for( xc_func_type &func : funcs ) + for (xc_func_type& func: funcs) { // jiyy add for threshold constexpr double rho_threshold = 1E-6; @@ -99,97 +133,146 @@ std::tuple XC_Functional_Libxc::v_xc_libxc( / xc_func_set_dens_threshold(&func, rho_threshold); // sgn for threshold mask - const std::vector sgn = XC_Functional_Libxc::cal_sgn(rho_threshold, grho_threshold, func, nspin, nrxx, rho, sigma); + const std::vector sgn + = XC_Functional_Libxc::cal_sgn(rho_threshold, grho_threshold, func, nspin, nrxx, rho, sigma); - std::vector exc ( nrxx ); - std::vector vrho ( nrxx * nspin ); - std::vector vsigma( nrxx * ((1==nspin)?1:3) ); + std::vector exc(nrxx); + std::vector vrho(nrxx * nspin); + std::vector vsigma(nrxx * ((1 == nspin) ? 1 : 3)); - ModuleBase::timer::start("Libxc","xc_lda/gga_exc_vxc"); - switch( func.info->family ) + ModuleBase::timer::start("Libxc", "xc_lda/gga_exc_vxc"); + switch (func.info->family) { - case XC_FAMILY_LDA: - { - constexpr int nr_batch_size = 1024; - #ifdef _OPENMP - #pragma omp parallel for schedule(static, nr_batch_size) - #endif - for( int ir_start = 0; ir_start < nrxx; ir_start += nr_batch_size ) - { - const int ir_end = std::min(ir_start + nr_batch_size, nrxx); - const int nrxx_thread = ir_end - ir_start; - xc_lda_exc_vxc( - &func, - nrxx_thread, - rho.data() + ir_start * nspin, - exc.data() + ir_start, - vrho.data() + ir_start * nspin ); - } - break; - } - case XC_FAMILY_GGA: - case XC_FAMILY_HYB_GGA: + case XC_FAMILY_LDA: { + constexpr int nr_batch_size = 1024; +#ifdef _OPENMP +#pragma omp parallel for schedule(static, nr_batch_size) +#endif + for (int ir_start = 0; ir_start < nrxx; ir_start += nr_batch_size) { - constexpr int nr_batch_size = 1024; - #ifdef _OPENMP - #pragma omp parallel for schedule(static, nr_batch_size) - #endif - for( int ir_start = 0; ir_start < nrxx; ir_start += nr_batch_size ) - { - const int ir_end = std::min(ir_start + nr_batch_size, nrxx); - const int nrxx_thread = ir_end - ir_start; - xc_gga_exc_vxc( - &func, - nrxx_thread, - rho.data() + ir_start * nspin, - sigma.data() + ir_start * ((1==nspin)?1:3), - exc.data() + ir_start, - vrho.data() + ir_start * nspin, - vsigma.data() + ir_start * ((1==nspin)?1:3) ); - } - break; + const int ir_end = std::min(ir_start + nr_batch_size, nrxx); + const int nrxx_thread = ir_end - ir_start; + xc_lda_exc_vxc(&func, + nrxx_thread, + rho.data() + ir_start * nspin, + exc.data() + ir_start, + vrho.data() + ir_start * nspin); } - default: + break; + } + case XC_FAMILY_GGA: + case XC_FAMILY_HYB_GGA: { + constexpr int nr_batch_size = 1024; +#ifdef _OPENMP +#pragma omp parallel for schedule(static, nr_batch_size) +#endif + for (int ir_start = 0; ir_start < nrxx; ir_start += nr_batch_size) { - throw std::domain_error("func.info->family ="+std::to_string(func.info->family) - +" unfinished in "+std::string(__FILE__)+" line "+std::to_string(__LINE__)); - + const int ir_end = std::min(ir_start + nr_batch_size, nrxx); + const int nrxx_thread = ir_end - ir_start; + xc_gga_exc_vxc(&func, + nrxx_thread, + rho.data() + ir_start * nspin, + sigma.data() + ir_start * ((1 == nspin) ? 1 : 3), + exc.data() + ir_start, + vrho.data() + ir_start * nspin, + vsigma.data() + ir_start * ((1 == nspin) ? 1 : 3)); } + break; } - ModuleBase::timer::end("Libxc","xc_lda/gga_exc_vxc"); + default: { + throw std::domain_error("func.info->family =" + std::to_string(func.info->family) + " unfinished in " + + std::string(__FILE__) + " line " + std::to_string(__LINE__)); + } + } + ModuleBase::timer::end("Libxc", "xc_lda/gga_exc_vxc"); // added by jghan, 2024-10-10 double factor = 1.0; - if( scaling_factor ) + if (scaling_factor) { auto pair_factor = scaling_factor->find(func.info->number); - if( pair_factor != scaling_factor->end() ) - { factor = pair_factor->second; } + if (pair_factor != scaling_factor->end()) + { + factor = pair_factor->second; + } } - // time factor is added by jghan, 2024-10-10 + // Keep the established energy accumulation and reduction order. In + // gga_grad=2, reverse every sanitizer now, apply the component scaling, + // and aggregate before traversing the shared projected graph once. etxc += XC_Functional_Libxc::convert_etxc(nspin, nrxx, sgn, rho, exc) * factor; - const std::pair vtxc_v = XC_Functional_Libxc::convert_vtxc_v( - func, nspin, nrxx, - sgn, rho, gdr, - vrho, vsigma, - tpiba, chr); - vtxc += std::get<0>(vtxc_v) * factor; - v += std::get<1>(vtxc_v) * factor; + if (use_lca) + { + const XC_Functional_Libxc::LibxcWeightedDerivatives weighted + = XC_Functional_Libxc::make_libxc_weighted_derivatives(func, + nspin, + nrxx, + sgn, + rho, + sigma, + exc, + vrho, + vsigma); + for (std::size_t index = 0; index < sf_weighted.drho.size(); ++index) + { + sf_weighted.drho[index] += factor * weighted.drho[index]; + } + for (std::size_t index = 0; index < weighted.dsigma.size(); ++index) + { + sf_weighted.dsigma[index] += factor * weighted.dsigma[index]; + } + } + else + { + const std::pair vtxc_v + = XC_Functional_Libxc::convert_vtxc_v(func, nspin, nrxx, sgn, rho, gdr, vrho, vsigma, tpiba, chr); + vtxc += std::get<0>(vtxc_v) * factor; + v += std::get<1>(vtxc_v) * factor; + } } // end for( xc_func_type &func : funcs ) - if(4==nspin_in) + if (use_lca) { - v = XC_Functional_Libxc::convert_v_nspin4(nrxx, chr, amag, v); + v = XC_Functional_Libxc::reverse_ncl_sf_discrete(nrxx, + sf_data, + sf_weighted.drho, + sf_weighted.dsigma, + tpiba, + chr); } - //------------------------------------------------- - // for MPI, reduce the exchange-correlation energy - //------------------------------------------------- - #ifdef __MPI + if (4 == nspin_in && !use_lca) + { + v = XC_Functional_Libxc::convert_v_nspin4(nrxx, chr, amag, v, has_mag); + } + + if (use_lca) + { + // Define vtxc from the potential that this routine actually returns. + // The nonlinear core density belongs to the XC energy graph, but the + // electronic variational density here is the four-channel valence + // density stored in chr->rho. + vtxc = 0.0; +#ifdef _OPENMP +#pragma omp parallel for collapse(2) reduction(+ : vtxc) schedule(static, 256) +#endif + for (int channel = 0; channel < 4; ++channel) + { + for (int ir = 0; ir < nrxx; ++ir) + { + vtxc += v(channel, ir) * chr->rho[channel][ir]; + } + } + } + +//------------------------------------------------- +// for MPI, reduce the exchange-correlation energy +//------------------------------------------------- +#ifdef __MPI Parallel_Reduce::reduce_pool(etxc); Parallel_Reduce::reduce_pool(vtxc); - #endif +#endif etxc *= omega / chr->rhopw->nxyz; vtxc *= omega / chr->rhopw->nxyz; @@ -200,7 +283,6 @@ std::tuple XC_Functional_Libxc::v_xc_libxc( / return std::make_tuple( etxc, vtxc, std::move(v) ); } - //the interface to libxc xc_mgga_exc_vxc(xc_func,n,rho,grho,laplrho,tau,e,v1,v2,v3,v4) //xc_func : LIBXC data type, contains information on xc functional //n: size of array, nspin*nnr @@ -240,7 +322,7 @@ std::tuple XC_Functional_Li // https://www.tddft.org/programs/libxc/manual/libxc-5.1.x/ //---------------------------------------------------------- std::vector funcs = XC_Functional_Libxc::init_func( - /* func_id = */ func_id, + /* func_id = */ func_id, /* xc_polarized = */ (1==nspin) ? XC_UNPOLARIZED:XC_POLARIZED, /* hybrid_alpha = */ hybrid_alpha, /* hse_omega = */ hse_omega); diff --git a/source/source_hamilt/module_xc/libxc_tools.cpp b/source/source_hamilt/module_xc/libxc_tools.cpp index 8dfd3828a64..1579b4df3b8 100644 --- a/source/source_hamilt/module_xc/libxc_tools.cpp +++ b/source/source_hamilt/module_xc/libxc_tools.cpp @@ -3,7 +3,10 @@ #include "libxc_abacus.h" #include "xc_functional.h" #include "source_estate/module_charge/charge.h" -#include "source_io/module_parameter/parameter.h" + +#include +#include +#include // converting rho (abacus=>libxc) std::vector XC_Functional_Libxc::convert_rho( @@ -32,7 +35,7 @@ XC_Functional_Libxc::convert_rho_amag_nspin4( const std::size_t nrxx, const Charge* const chr) { - assert(PARAM.inp.nspin==4); + assert(nspin==2); // nspin here is the collapsed spin dimension for libxc (up/down) std::vector rho(nrxx*nspin); std::vector amag(nrxx); #ifdef _OPENMP @@ -51,6 +54,192 @@ XC_Functional_Libxc::convert_rho_amag_nspin4( return std::make_tuple(std::move(rho), std::move(amag)); } +XC_Functional_Libxc::NclSfDiscreteData +XC_Functional_Libxc::make_ncl_sf_discrete_data( + const std::size_t nrxx, + const double tpiba, + const Charge* const chr, + const bool need_gradient) +{ + constexpr int nspin = 2; + NclSfDiscreteData data; + data.spin_map.resize(nrxx); + data.rho.resize(nrxx * nspin); + + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 1024) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + const std::array magnetization + = {{chr->rho[1][ir], chr->rho[2][ir], chr->rho[3][ir]}}; + const ModuleXC::NcggaRadialPoint radial + = ModuleXC::make_ncgga_radial_point( + magnetization, ModuleXC::ncgga_lca_radial_eta()); + data.spin_map[ir] = ModuleXC::make_ncgga_spin_map_point( + chr->rho[0][ir] + chr->rho_core[ir], radial); + data.rho[ir * nspin] = data.spin_map[ir].spin_density[0]; + data.rho[ir * nspin + 1] = data.spin_map[ir].spin_density[1]; + } + + if (!need_gradient) + { + return data; + } + + std::vector> grad_total(nrxx); + std::vector real_field(nrxx); + std::vector> reciprocal(chr->rhopw->npw); + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 1024) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + real_field[ir] = chr->rho[0][ir] + chr->rho_core[ir]; + } + chr->rhopw->real2recip(real_field.data(), reciprocal.data()); + XC_Functional::grad_rho( + reciprocal.data(), grad_total.data(), chr->rhopw, tpiba); + + for (int mu = 0; mu < 3; ++mu) + { + data.grad_m[mu].resize(nrxx); + chr->rhopw->real2recip(chr->rho[mu + 1], reciprocal.data()); + XC_Functional::grad_rho( + reciprocal.data(), data.grad_m[mu].data(), chr->rhopw, tpiba); + } + + data.spin_gradient.resize(nspin); + for (int spin = 0; spin < nspin; ++spin) + { + data.spin_gradient[spin].resize(nrxx); + } + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 512) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + for (int spin = 0; spin < nspin; ++spin) + { + ModuleBase::Vector3 gradient + = data.spin_map[ir].jacobian(spin, 0) * grad_total[ir]; + for (int mu = 0; mu < 3; ++mu) + { + gradient += data.spin_map[ir].jacobian(spin, mu + 1) + * data.grad_m[mu][ir]; + } + data.spin_gradient[spin][ir] = gradient; + } + } + return data; +} + +ModuleBase::matrix XC_Functional_Libxc::reverse_ncl_sf_discrete( + const std::size_t nrxx, + const NclSfDiscreteData& data, + const std::vector& drho, + const std::vector& dsigma, + const double tpiba, + const Charge* const chr) +{ + constexpr int nspin = 2; + constexpr int nchannel = 4; + assert(data.spin_map.size() == nrxx); + assert(data.rho.size() == nrxx * nspin); + assert(drho.size() == nrxx * nspin); + + ModuleBase::matrix potential(nchannel, nrxx); + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 512) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + for (int channel = 0; channel < nchannel; ++channel) + { + potential(channel, ir) + = ModuleBase::e2 + * (data.spin_map[ir].jacobian(0, channel) + * drho[ir * nspin] + + data.spin_map[ir].jacobian(1, channel) + * drho[ir * nspin + 1]); + } + } + + if (dsigma.empty()) + { + return potential; + } + + assert(dsigma.size() == nrxx * 3); + assert(data.spin_gradient.size() == nspin); + for (int spin = 0; spin < nspin; ++spin) + { + assert(data.spin_gradient[spin].size() == nrxx); + } + for (int mu = 0; mu < 3; ++mu) + { + assert(data.grad_m[mu].size() == nrxx); + } + + std::vector> h_up(nrxx), h_down(nrxx); + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 512) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + const std::size_t sigma_index = 3 * ir; + h_up[ir] + = ModuleBase::e2 + * (2.0 * dsigma[sigma_index] * data.spin_gradient[0][ir] + + dsigma[sigma_index + 1] * data.spin_gradient[1][ir]); + h_down[ir] + = ModuleBase::e2 + * (2.0 * dsigma[sigma_index + 2] * data.spin_gradient[1][ir] + + dsigma[sigma_index + 1] * data.spin_gradient[0][ir]); + } + + std::vector> flux(nrxx); + std::vector divergence(nrxx); + for (int channel = 0; channel < nchannel; ++channel) + { + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 512) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + flux[ir] + = data.spin_map[ir].jacobian(0, channel) * h_up[ir] + + data.spin_map[ir].jacobian(1, channel) * h_down[ir]; + } + XC_Functional::grad_dot( + flux.data(), divergence.data(), chr->rhopw, tpiba); + + #ifdef _OPENMP + #pragma omp parallel for schedule(static, 512) + #endif + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + potential(channel, ir) -= divergence[ir]; + if (channel == 0 || data.spin_map[ir].saturated) + { + continue; + } + + const ModuleBase::Vector3 spin_flux + = 0.5 * (h_up[ir] - h_down[ir]); + double local_response = 0.0; + for (int nu = 0; nu < 3; ++nu) + { + local_response + += data.spin_map[ir].radial.jacobian(nu, channel - 1) + * (spin_flux * data.grad_m[nu][ir]); + } + potential(channel, ir) += local_response; + } + } + return potential; +} + // calculating grho std::vector>> XC_Functional_Libxc::cal_gdr( @@ -388,7 +577,6 @@ std::pair XC_Functional_Libxc::convert_vtxc_v( return std::make_pair(vtxc, std::move(v)); } - // dh for gga v std::vector> XC_Functional_Libxc::cal_dh( const int nspin, @@ -438,30 +626,30 @@ std::vector> XC_Functional_Libxc::cal_dh( return dh; } - // convert v for NSPIN=4 ModuleBase::matrix XC_Functional_Libxc::convert_v_nspin4( const std::size_t nrxx, const Charge* const chr, const std::vector &amag, - const ModuleBase::matrix &v) + const ModuleBase::matrix &v, + const bool has_mag) { //assert(nrxx>0); - assert(PARAM.inp.nspin==4); + constexpr int nspin4 = 4; constexpr double vanishing_charge = 1.0e-10; - ModuleBase::matrix v_nspin4(PARAM.inp.nspin, nrxx); + ModuleBase::matrix v_nspin4(nspin4, nrxx); for( int ir=0; ir vanishing_charge ) { const double vs = 0.5 * (v(0,ir)-v(1,ir)); - for(int ipol=1; ipolrho[ipol][ir] / amag[ir]; } diff --git a/source/source_hamilt/module_xc/test/CMakeLists.txt b/source/source_hamilt/module_xc/test/CMakeLists.txt index e5ce9e053a3..e9c47db8c3d 100644 --- a/source/source_hamilt/module_xc/test/CMakeLists.txt +++ b/source/source_hamilt/module_xc/test/CMakeLists.txt @@ -55,8 +55,8 @@ AddTest( ../libxc_lda_wrap.cpp ../libxc_gga_wrap.cpp ../libxc_mgga_wrap.cpp - ../xc_gga_corr.cpp ../xc_lda_corr.cpp - ../xc_gga_exch.cpp ../xc_lda_exch.cpp ../xc_hcth.cpp + ../xc_gga_corr.cpp ../xc_lda_corr.cpp + ../xc_gga_exch.cpp ../xc_lda_exch.cpp ../xc_hcth.cpp ) AddTest( @@ -71,6 +71,8 @@ AddTest( ../xc_gga_corr.cpp ../xc_lda_corr.cpp ../xc_gga_exch.cpp ../xc_lda_exch.cpp ../xc_hcth.cpp ../xc_pot.cpp + ../xc_functional_ncgga_sf.cpp + ../xc_ncgga_radial.cpp ../libxc_pot.cpp ../libxc_tools.cpp ../../../source_base/module_external/blas_connector_base.cpp ../../../source_base/module_external/blas_connector_vector.cpp ../../../source_base/module_external/blas_connector_matrix.cpp @@ -84,6 +86,35 @@ AddTest( ${FFT_SRC} ) +AddTest( + TARGET MODULE_HAMILT_XCTest_NCGGA_RADIAL + SOURCES test_xc_ncgga_radial.cpp ../xc_ncgga_radial.cpp +) + +AddTest( + TARGET MODULE_HAMILT_XCTest_NCGGA_DISCRETE_FD + LIBS xc_ planewave parameter MPI::MPI_CXX Libxc::xc psi device container base + SOURCES test_xc_functional_ncgga_sf.cpp +) +set_tests_properties( + MODULE_HAMILT_XCTest_NCGGA_DISCRETE_FD + PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1" +) +add_test( + NAME MODULE_HAMILT_XCTest_NCGGA_DISCRETE_FD_MPI2 + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 2 + ${MPIEXEC_PREFLAGS} + $ + ${MPIEXEC_POSTFLAGS} +) +set_tests_properties( + MODULE_HAMILT_XCTest_NCGGA_DISCRETE_FD_MPI2 + PROPERTIES + ENVIRONMENT "OMP_NUM_THREADS=1" + PROCESSORS 2 + WORKING_DIRECTORY $ +) + AddTest( TARGET MODULE_HAMILT_XCTest_SCANL_LAPL LIBS parameter MPI::MPI_CXX Libxc::xc @@ -124,7 +155,9 @@ AddTest( ${FFT_SRC} ) -AddTest( - TARGET MODULE_HAMILT_XCTest_NCGGA_RADIAL - SOURCES test_xc_ncgga_radial.cpp ../xc_ncgga_radial.cpp -) +# gradcorr dispatches the noncollinear stress path even in focused FFT tests. +foreach(xc_gradient_test MODULE_HAMILT_XCTest_GRADCORR MODULE_HAMILT_XCTest_LAPL) + target_sources(${xc_gradient_test} PRIVATE + ../xc_functional_ncgga_sf.cpp ../xc_ncgga_radial.cpp + ../libxc_pot.cpp ../libxc_tools.cpp ../../../source_base/timer.cpp) +endforeach() diff --git a/source/source_hamilt/module_xc/test/test_xc3.cpp b/source/source_hamilt/module_xc/test/test_xc3.cpp index d472d972cc9..0988923de36 100644 --- a/source/source_hamilt/module_xc/test/test_xc3.cpp +++ b/source/source_hamilt/module_xc/test/test_xc3.cpp @@ -92,13 +92,13 @@ class XCTest_GRADCORR : public XCTest double hybrid_alpha = 0.0; double hse_omega = 0.0; - XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,false,nspin1,domag,domag_z, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,true,nspin1,domag,domag_z, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,false,nspin1,domag,domag_z,0, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,true,nspin1,domag,domag_z,0, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,false,nspin2,domag,domag_z, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,true,nspin2,domag,domag_z, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,false,nspin2,domag,domag_z,0, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,true,nspin2,domag,domag_z,0, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,false,nspin4,domag_true,domag_z, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,false,nspin4,domag_true,domag_z,0, hybrid_alpha, hse_omega); } }; @@ -238,14 +238,14 @@ class XCTest_GRADCORR_HF : public XCTest const double hybrid_alpha = 1.0; const double hse_omega = 0.0; - XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,false,nspin1,domag,domag_z, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,true, nspin1,domag,domag_z, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,false,nspin1,domag,domag_z,0,hybrid_alpha,hse_omega); + XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,true, nspin1,domag,domag_z,0,hybrid_alpha,hse_omega); - XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,false,nspin2,domag,domag_z, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,true, nspin2,domag,domag_z, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,false,nspin2,domag,domag_z,0,hybrid_alpha,hse_omega); + XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,true, nspin2,domag,domag_z,0,hybrid_alpha,hse_omega); - XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,false,nspin4,domag_true,domag_z, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,true, nspin4,domag_true,domag_z, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,false,nspin4,domag_true,domag_z,0,hybrid_alpha,hse_omega); + XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,true, nspin4,domag_true,domag_z,0,hybrid_alpha,hse_omega); } }; @@ -343,4 +343,4 @@ TEST_F(XCTest_GRADWFC, set_xc_type) EXPECT_NEAR(grad[i+j*5].imag(),0,1e-8); } } -} \ No newline at end of file +} diff --git a/source/source_hamilt/module_xc/test/test_xc5.cpp b/source/source_hamilt/module_xc/test/test_xc5.cpp index 01aa214feae..cfc31756b94 100644 --- a/source/source_hamilt/module_xc/test/test_xc5.cpp +++ b/source/source_hamilt/module_xc/test/test_xc5.cpp @@ -8,6 +8,12 @@ #include "source_cell/cal_ux.h" #include "../../../source_base/parallel_reduce.h" +#include +#include +#include +#include +#include + /************************************************ * unit test of functionals ***********************************************/ @@ -22,7 +28,8 @@ class XCTest_VXC : public XCTest { protected: - double et1 = 0, vt1 = 0; + double et1 = 0; + double vt1 = 0; ModuleBase::matrix v1; double et2 = 0, vt2 = 0; @@ -82,13 +89,13 @@ class XCTest_VXC : public XCTest const double hybrid_alpha = XC_Functional::get_hybrid_alpha(); const double hse_omega = XC_Functional::get_hse_omega(); std::tuple etxc_vtxc_v - = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin1,domag,domag_z, hybrid_alpha, hse_omega); + = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin1,domag,domag_z,0, hybrid_alpha, hse_omega); et1 = std::get<0>(etxc_vtxc_v); vt1 = std::get<1>(etxc_vtxc_v); v1 = std::get<2>(etxc_vtxc_v); etxc_vtxc_v - = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin2,domag,domag_z, hybrid_alpha, hse_omega); + = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin2,domag,domag_z,0, hybrid_alpha, hse_omega); et2 = std::get<0>(etxc_vtxc_v); vt2 = std::get<1>(etxc_vtxc_v); v2 = std::get<2>(etxc_vtxc_v); @@ -126,7 +133,8 @@ class XCTest_VXC_Libxc : public XCTest { protected: - double et1 = 0, vt1 = 0; + double et1 = 0; + double vt1 = 0; ModuleBase::matrix v1; double et2 = 0, vt2 = 0; @@ -186,13 +194,13 @@ class XCTest_VXC_Libxc : public XCTest const double hybrid_alpha = XC_Functional::get_hybrid_alpha(); const double hse_omega = XC_Functional::get_hse_omega(); std::tuple etxc_vtxc_v - = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin1,domag,domag_z, hybrid_alpha, hse_omega); + = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin1,domag,domag_z,0, hybrid_alpha, hse_omega); et1 = std::get<0>(etxc_vtxc_v); vt1 = std::get<1>(etxc_vtxc_v); v1 = std::get<2>(etxc_vtxc_v); etxc_vtxc_v - = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin2,domag,domag_z, hybrid_alpha, hse_omega); + = XC_Functional::v_xc(rhopw.nrxx,&chr,&ucell,nspin2,domag,domag_z,0, hybrid_alpha, hse_omega); et2 = std::get<0>(etxc_vtxc_v); vt2 = std::get<1>(etxc_vtxc_v); v2 = std::get<2>(etxc_vtxc_v); @@ -230,7 +238,8 @@ class XCTest_VXC_meta : public XCTest { protected: - double et1 = 0, vt1 = 0; + double et1 = 0; + double vt1 = 0; ModuleBase::matrix v1,vtau1; double et2 = 0, vt2 = 0; @@ -353,6 +362,370 @@ TEST_F(XCTest_VXC_meta, set_xc_type) EXPECT_NEAR(vtau2(1,4),0.0311787189,1.0e-8); } +/************************************************ + * unit tests for the gga_grad keyword (nspin=4 + * noncollinear GGA gradient methods) + * + * Method 2 differentiates the complete discrete local-spin-map and FFT + * gradient graph. Its reverse is tested on a real PW grid in + * test_xc_functional_ncgga_sf.cpp. + ************************************************/ + +namespace +{ +constexpr int gga_grad_nrxx = 5; + +// build a mock 4-component charge on the mocked 5-point grid. +// pattern 0: m = (0,0,mz), mz>0, so m_hat = (0,0,1) everywhere +// pattern 1: m direction varies from point to point +// pattern 2: m . ux changes sign across the grid (ux = (0,1,2) in the mock) +struct Ns4Charge +{ + ModulePW::PW_Basis rhopw; + UnitCell ucell; + Charge chr; + + Ns4Charge(const int pattern) + { + rhopw.nrxx = gga_grad_nrxx; + rhopw.npw = gga_grad_nrxx; + rhopw.nmaxgr = gga_grad_nrxx; + rhopw.gcar = new ModuleBase::Vector3[gga_grad_nrxx]; + rhopw.nxyz = 1; + + ucell.tpiba = 1; + ucell.omega = 1; + ucell.magnet.lsign_ = true; + unitcell::cal_ux(ucell, 4); + + chr.rhopw = &(rhopw); + chr.rho = new double*[4]; + for (int is = 0; is < 4; ++is) + { + chr.rho[is] = new double[gga_grad_nrxx]; + } + chr.rhog = new std::complex*[2]; + chr.rhog[0] = new std::complex[gga_grad_nrxx]; + chr.rhog[1] = new std::complex[gga_grad_nrxx]; + chr.rho_core = new double[gga_grad_nrxx]; + chr.rhog_core = new std::complex[gga_grad_nrxx]; + + for (int i = 0; i < gga_grad_nrxx; ++i) + { + chr.rho[0][i] = 2.0 + i; + if (pattern == 1) + { + chr.rho[1][i] = 0.10 * (i + 1); + chr.rho[2][i] = 0.05 * (gga_grad_nrxx - i); + chr.rho[3][i] = 0.20 * (i + 1); + } + else if (pattern == 2) + { + chr.rho[1][i] = 0.0; + chr.rho[2][i] = (i % 2 == 0) ? 0.3 : -0.3; + chr.rho[3][i] = 0.05; + } + else + { + chr.rho[1][i] = 0.0; + chr.rho[2][i] = 0.0; + chr.rho[3][i] = 0.2 * (i + 1); + } + chr.rhog[0][i] = chr.rho[0][i]; + chr.rhog[1][i] = chr.rho[1][i]; + chr.rho_core[i] = 0; + chr.rhog_core[i] = 0; + rhopw.gcar[i] = 1; + } + } +}; + + + +struct Ns2LocalCharge +{ + ModulePW::PW_Basis rhopw; + Charge chr; + + Ns2LocalCharge() + { + rhopw.nrxx = 1; + rhopw.npw = 1; + rhopw.nmaxgr = 1; + rhopw.nxyz = 1; + rhopw.gcar = new ModuleBase::Vector3[1]; + rhopw.gcar[0] = 0.0; + + chr.rhopw = &rhopw; + chr.rho = new double*[2]; + chr.rhog = new std::complex*[2]; + for (int is = 0; is < 2; ++is) + { + chr.rho[is] = new double[1]; + chr.rhog[is] = new std::complex[1]; + chr.rhog[is][0] = 0.0; + } + chr.rho_core = new double[1]; + chr.rhog_core = new std::complex[1]; + chr.rho_core[0] = 0.0; + chr.rhog_core[0] = 0.0; + } +}; + +// run XC_Functional::v_xc for nspin=4 with noncollinear magnetism +std::tuple run_vxc_nspin4( + const std::string& functional, + const int pattern, + const int gga_grad) +{ + Ns4Charge mock(pattern); + XC_Functional::set_xc_type(functional); + return XC_Functional::v_xc(gga_grad_nrxx, + &mock.chr, + &mock.ucell, + 4, + true, + false, + gga_grad, + XC_Functional::get_hybrid_alpha(), + XC_Functional::get_hse_omega()); +} + +// compare two (etxc, vtxc, v) results +void expect_vxc_equal(const std::tuple& a, + const std::tuple& b, + const double tol) +{ + EXPECT_NEAR(std::get<0>(a), std::get<0>(b), tol); + EXPECT_NEAR(std::get<1>(a), std::get<1>(b), tol); + const ModuleBase::matrix& va = std::get<2>(a); + const ModuleBase::matrix& vb = std::get<2>(b); + ASSERT_EQ(va.nr, vb.nr); + ASSERT_EQ(va.nc, vb.nc); + for (int ir = 0; ir < va.nr; ++ir) + { + for (int ic = 0; ic < va.nc; ++ic) + { + EXPECT_NEAR(va(ir, ic), vb(ir, ic), tol); + } + } +} +} // namespace + +// m_hat = m/|m|, zero where |m| ~ 0 + + +// v_tot = 0.5*(v_up+v_dn), v_mu = 0.5*(v_up-v_dn)*m_hat_mu + + +// original conversion: has_mag=false leaves magnetic channels zero +TEST(GgaGradTools, ConvertVNspin4HasMag) +{ + Ns4Charge mock(0); + std::vector amag(gga_grad_nrxx); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + amag[ir] = mock.chr.rho[3][ir]; + } + ModuleBase::matrix v(2, gga_grad_nrxx); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + v(0, ir) = 1.0 + ir; + v(1, ir) = 0.5 * ir; + } + + const ModuleBase::matrix v_nomag + = XC_Functional_Libxc::convert_v_nspin4(gga_grad_nrxx, &mock.chr, amag, v, false); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + EXPECT_NEAR(v_nomag(0, ir), 0.5 * (v(0, ir) + v(1, ir)), 1e-14); + EXPECT_NEAR(v_nomag(1, ir), 0.0, 1e-14); + EXPECT_NEAR(v_nomag(2, ir), 0.0, 1e-14); + EXPECT_NEAR(v_nomag(3, ir), 0.0, 1e-14); + } + + const ModuleBase::matrix v_mag + = XC_Functional_Libxc::convert_v_nspin4(gga_grad_nrxx, &mock.chr, amag, v, true); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + const double vs = 0.5 * (v(0, ir) - v(1, ir)); + EXPECT_NEAR(v_mag(3, ir), vs * mock.chr.rho[3][ir] / amag[ir], 1e-14); + } +} + + + +// gga_grad=0 keeps the original built-in algorithm and must not crash +TEST(GgaGradVxc, BuiltinOriginalAlgorithmRuns) +{ + const auto r0 = run_vxc_nspin4("PBE", 1, 0); + EXPECT_EQ(std::get<2>(r0).nr, 4); + EXPECT_TRUE(std::isfinite(std::get<0>(r0))); + EXPECT_TRUE(std::isfinite(std::get<1>(r0))); +} + +// noncolin_rho with lsign=true defines up/down w.r.t. the global axis ux +// through sign(m . ux); with lsign=false, up is always the local |m| +TEST(GgaGradTools, NoncolinRhoGlobalAxis) +{ + Ns4Charge mock(2); // pattern 2: m . ux changes sign across the grid + const double* ux = mock.ucell.magnet.ux_; // (0,1,2) in the mock + + std::vector rup(gga_grad_nrxx), rdn(gga_grad_nrxx), neg(gga_grad_nrxx); + XC_Functional::noncolin_rho( + rup.data(), rdn.data(), neg.data(), mock.chr.rho, gga_grad_nrxx, ux, true); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + const double mx = mock.chr.rho[1][ir]; + const double my = mock.chr.rho[2][ir]; + const double mz = mock.chr.rho[3][ir]; + const double amag = std::sqrt(mx * mx + my * my + mz * mz); + const double sign = (mx * ux[0] + my * ux[1] + mz * ux[2] > 0) ? 1.0 : -1.0; + EXPECT_NEAR(rup[ir], 0.5 * (mock.chr.rho[0][ir] + sign * amag), 1e-14); + EXPECT_NEAR(rdn[ir], 0.5 * (mock.chr.rho[0][ir] - sign * amag), 1e-14); + } + // the sign really flips on this grid, i.e. the global axis matters here + EXPECT_NEAR(neg[0], 1.0, 1e-14); + EXPECT_NEAR(neg[1], -1.0, 1e-14); + + XC_Functional::noncolin_rho( + rup.data(), rdn.data(), neg.data(), mock.chr.rho, gga_grad_nrxx, ux, false); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + const double mx = mock.chr.rho[1][ir]; + const double my = mock.chr.rho[2][ir]; + const double mz = mock.chr.rho[3][ir]; + const double amag = std::sqrt(mx * mx + my * my + mz * mz); + EXPECT_NEAR(rup[ir], 0.5 * (mock.chr.rho[0][ir] + amag), 1e-14); + EXPECT_NEAR(rdn[ir], 0.5 * (mock.chr.rho[0][ir] - amag), 1e-14); + } +} + +// gga_grad=1 must ignore the global magnetization direction: with lsign_=true +// it has to give the same gradcorr result as gga_grad=0 with lsign_=false +TEST(GgaGradVxc, BuiltinGgaGrad1IgnoresGlobalAxis) +{ + XC_Functional::set_xc_type("PBE"); + const double hybrid_alpha = XC_Functional::get_hybrid_alpha(); + const double hse_omega = XC_Functional::get_hse_omega(); + + Ns4Charge mock_a(2); // lsign_ = true + double et1 = 0; + double vt1 = 0; + ModuleBase::matrix v1(4, gga_grad_nrxx); + std::vector dum; + XC_Functional::gradcorr(et1, vt1, v1, &mock_a.chr, &mock_a.rhopw, &mock_a.ucell, + dum, false, 4, true, false, 1, hybrid_alpha, hse_omega); + + Ns4Charge mock_b(2); + mock_b.ucell.magnet.lsign_ = false; + double et0 = 0; + double vt0 = 0; + ModuleBase::matrix v0(4, gga_grad_nrxx); + XC_Functional::gradcorr(et0, vt0, v0, &mock_b.chr, &mock_b.rhopw, &mock_b.ucell, + dum, false, 4, true, false, 0, hybrid_alpha, hse_omega); + + EXPECT_NEAR(et0, et1, 1e-12); + EXPECT_NEAR(vt0, vt1, 1e-12); + for (int is = 0; is < 4; ++is) + { + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + EXPECT_NEAR(v0(is, ir), v1(is, ir), 1e-12); + } + } +} + + + +TEST(GgaGradVxc, LibxcNspin4NearSaturationDifferentiatesTheWeightedEnergy) +{ + constexpr double density_threshold = 1.0e-6; + Ns4Charge mock(0); + for (int ir = 0; ir < gga_grad_nrxx; ++ir) + { + mock.chr.rho[0][ir] = 0.45; + mock.chr.rho[1][ir] = 0.0; + mock.chr.rho[2][ir] = 0.0; + mock.chr.rho[3][ir] = 0.45 - density_threshold; + } + const std::vector func_ids = {XC_LDA_X}; + const int gga_grad_modes[] = {2}; + for (const int gga_grad : gga_grad_modes) + { + const auto evaluate = [&, gga_grad]() + { + return XC_Functional_Libxc::v_xc_libxc(func_ids, + gga_grad_nrxx, + mock.ucell.omega, + mock.ucell.tpiba, + &mock.chr, + 4, + true, + false, + gga_grad, + nullptr, + 0.0, + 0.0); + }; + + const auto reference = evaluate(); + const int components[] = {0, 3}; + const double steps[] = {8.0e-8, 4.0e-8, 2.0e-8}; + for (const int component : components) + { + double analytic = std::get<2>(reference)(component, 0); + Parallel_Reduce::reduce_pool(analytic); + if (std::getenv("ABACUS_XC_FD_TRACE") != nullptr) + { + std::cout << std::setprecision(17) + << "XC_SANITIZER_REFERENCE case=nspin4_near_saturation" + << " gga_grad=" << gga_grad + << " component=" << component + << " energy=" << std::get<0>(reference) + << " vtxc=" << std::get<1>(reference) + << " analytic=" << analytic << std::endl; + } + const double original = mock.chr.rho[component][0]; + for (const double step : steps) + { + mock.chr.rho[component][0] = original + step; + const double energy_plus = std::get<0>(evaluate()); + mock.chr.rho[component][0] = original - step; + const double energy_minus = std::get<0>(evaluate()); + mock.chr.rho[component][0] = original; + + const double finite_difference = (energy_plus - energy_minus) / (2.0 * step); + if (std::getenv("ABACUS_XC_FD_TRACE") != nullptr) + { + std::cout << std::setprecision(17) + << "XC_SANITIZER_FD case=nspin4_near_saturation" + << " gga_grad=" << gga_grad + << " component=" << component + << " eps=" << step + << " analytic=" << analytic + << " finite_difference=" << finite_difference + << " absolute_error=" << std::abs(analytic - finite_difference) + << std::endl; + } + const double scale = std::max(1.0, std::max(std::abs(analytic), + std::abs(finite_difference))); + EXPECT_NEAR(analytic, finite_difference, 2.0e-8 * scale) + << "gga_grad=" << gga_grad + << ", component=" << component + << ", step=" << step; + } + } + } +} + +// for LIBXC, gga_grad=0 and 1 both keep the original collinear algorithm +TEST(GgaGradVxc, LibxcZeroEqualsOne) +{ + const auto r0 = run_vxc_nspin4("GGA_X_PBE+GGA_C_PBE", 1, 0); + const auto r1 = run_vxc_nspin4("GGA_X_PBE+GGA_C_PBE", 1, 1); + expect_vxc_equal(r0, r1, 1e-12); +} int main(int argc, char **argv) { @@ -361,4 +734,4 @@ int main(int argc, char **argv) int result = RUN_ALL_TESTS(); MPI_Finalize(); return result; -} \ No newline at end of file +} diff --git a/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp b/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp new file mode 100644 index 00000000000..0f5d6b92f47 --- /dev/null +++ b/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp @@ -0,0 +1,1682 @@ +#include "../libxc_abacus.h" +#include "../xc_functional.h" +#include "../xc_functional_ncgga_sf.h" +#include "../xc_ncgga_radial.h" +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_cell/unitcell.h" +#include "source_estate/module_charge/charge.h" + +#ifdef __MPI +#include "source_base/parallel_comm.h" +#include "source_base/parallel_global.h" +#include "source_base/parallel_reduce.h" + +#include +#endif + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// This focused target does not link the full elecstate object library. The +// fixture supplies vector-backed charge storage, so only the trivial lifetime +// boundary is needed here. +Charge::Charge() +{ +} +Charge::~Charge() +{ +} + +// This target links the production PW/XC objects but not the full cell object +// library. The stress entry point only reads UnitCell::tpiba and magnet.lsign_ +// in the parent implementation, so provide the same narrow lifetime boundary +// used by the existing XC focused tests. +UnitCell::UnitCell() +{ +} +UnitCell::~UnitCell() +{ +} +Magnetism::Magnetism() +{ +} +Magnetism::~Magnetism() +{ +} +SepPot::SepPot() +{ +} +SepPot::~SepPot() +{ +} +Sep_Cell::Sep_Cell() noexcept +{ +} +Sep_Cell::~Sep_Cell() noexcept +{ +} + +namespace +{ +int test_rank = 0; +int test_size = 1; + +double pool_sum(const double local) +{ + double global = local; + Parallel_Reduce::reduce_pool(global); + return global; +} + +double pool_min(const double local) +{ + double global = local; + Parallel_Reduce::reduce_min(global); + return global; +} + +double pool_max(const double local) +{ + double global = local; + Parallel_Reduce::reduce_max(global); + return global; +} + +bool is_pool_root() +{ + return test_rank == 0; +} + +std::uint64_t potential_fnv1a64(const ModuleBase::matrix& potential) +{ + static_assert(sizeof(double) == sizeof(std::uint64_t), "the potential hash requires 64-bit doubles"); + std::uint64_t hash = 14695981039346656037ULL; + for (int channel = 0; channel < potential.nr; ++channel) + { + for (int ir = 0; ir < potential.nc; ++ir) + { + std::uint64_t bits = 0; + const double value = potential(channel, ir); + std::memcpy(&bits, &value, sizeof(bits)); + for (int byte = 0; byte < 8; ++byte) + { + hash ^= (bits >> (8 * byte)) & 0xffULL; + hash *= 1099511628211ULL; + } + } + } + return hash; +} + +class RealPwNcgga : public testing::Test +{ + protected: + typedef std::tuple VxcResult; + typedef std::function Evaluator; + typedef std::function()> StressEvaluator; + + struct BranchMargins + { + double min_abs_total_density; + double min_signed_saturation_gap; + double max_signed_saturation_gap; + double min_magnitude; + double max_magnitude; + double min_eta_distance; + }; + + ModulePW::PW_Basis pw; + Charge charge; + std::array, 4> density; + std::array, 4> perturbation; + std::array density_pointer; + std::vector core_density; + std::vector> core_density_reciprocal; + std::array>, 2> charge_reciprocal; + std::array*, 2> charge_reciprocal_pointer; + + struct ReciprocalMetricState + { + std::vector> gcar; + std::array, 4> density; + std::vector core_density; + std::vector> core_density_reciprocal; + double omega = 0.0; + }; + + void SetUp() override + { +#ifdef __MPI + pw.initmpi(test_size, test_rank, MPI_COMM_WORLD); +#endif + // Keep tpiba away from one so an omitted or duplicated reciprocal- + // length factor cannot accidentally pass the adjoint test. + const double lat0 = 7.0; + const ModuleBase::Matrix3 lattice(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + // An odd z dimension gives unequal real-space slabs in the MPI2 test. + pw.initgrids(lat0, lattice, 24, 10, 9); + pw.initparameters(false, 80.0, 2, false); + pw.setuptransform(); + pw.collect_local_pw(); + + ASSERT_EQ(pw.nx, 24); + ASSERT_EQ(pw.ny, 10); + ASSERT_EQ(pw.nz, 9); + ASSERT_EQ(pw.nxyz, 2160); + ASSERT_GT(pw.npwtot, 100); + ASSERT_FALSE(pw.gamma_only); + ASSERT_NEAR(pw.tpiba, ModuleBase::TWO_PI / lat0, 1.0e-14); + ASSERT_GT(std::abs(pw.tpiba - 1.0), 5.0e-2); + + for (int channel = 0; channel < 4; ++channel) + { + density[channel].resize(pw.nrxx); + perturbation[channel].resize(pw.nrxx); + density_pointer[channel] = density[channel].data(); + } + core_density.resize(pw.nrxx); + core_density_reciprocal.resize(pw.npw); + for (int spin = 0; spin < 2; ++spin) + { + charge_reciprocal[spin].resize(pw.npw); + charge_reciprocal_pointer[spin] = charge_reciprocal[spin].data(); + } + + charge.rhopw = &pw; + charge.nrxx = pw.nrxx; + charge.nxyz = pw.nxyz; + charge.ngmc = pw.npw; + charge.nspin = 4; + charge.rho = density_pointer.data(); + charge.rhog = charge_reciprocal_pointer.data(); + charge.rho_core = core_density.data(); + charge.rhog_core = core_density_reciprocal.data(); + + for (int ir = 0; ir < pw.nrxx; ++ir) + { + // PW_Basis stores local real data as + // ir = iz_local + (iy + ix * ny) * nplane. + const int ix = ir / (pw.ny * pw.nplane); + const int iy = (ir / pw.nplane) % pw.ny; + const int iz = ir % pw.nplane + pw.startz_current; + const double x = ModuleBase::TWO_PI * static_cast(ix) / pw.nx; + const double y = ModuleBase::TWO_PI * static_cast(iy) / pw.ny; + const double z = ModuleBase::TWO_PI * static_cast(iz) / pw.nz; + const double total_density = 2.2 + 0.18 * std::sin(x + 0.21) + 0.13 * std::cos(4.0 * x - 0.17) + + 0.07 * std::sin(8.0 * x + 0.33) + 0.05 * std::cos(y - 0.26) + + 0.04 * std::sin(z + 0.31); + const double magnitude = 0.62 + 0.07 * std::cos(2.0 * x + 0.13) + 0.05 * std::sin(5.0 * x - 0.27) + + 0.03 * std::sin(y + 0.19) + 0.02 * std::cos(z - 0.23); + const double theta = 0.7 + 0.32 * std::sin(3.0 * x + 0.11) + 0.18 * std::cos(7.0 * x - 0.23) + + 0.10 * std::cos(y + 0.17) + 0.07 * std::sin(z - 0.29); + const double phi = 0.4 + 0.27 * std::cos(4.0 * x + 0.37) - 0.16 * std::sin(6.0 * x + 0.29) + + 0.09 * std::sin(y + z + 0.15); + density[0][ir] = total_density; + density[1][ir] = magnitude * std::sin(theta) * std::cos(phi); + density[2][ir] = magnitude * std::sin(theta) * std::sin(phi); + density[3][ir] = magnitude * std::cos(theta); + core_density[ir] = 0.15 + 0.03 * std::cos(3.0 * x - 0.14) + 0.02 * std::sin(6.0 * x + 0.25) + + 0.01 * std::cos(y - z + 0.18); + + perturbation[0][ir] + = 0.31 * std::cos(2.0 * x + 0.41) - 0.19 * std::sin(7.0 * x - 0.12) + 0.11 * std::cos(y + z - 0.16); + perturbation[1][ir] + = 0.27 * std::sin(x + 0.37) + 0.21 * std::cos(8.0 * x + 0.19) + 0.13 * std::sin(y - 0.24); + perturbation[2][ir] + = 0.29 * std::cos(3.0 * x - 0.22) - 0.17 * std::sin(6.0 * x + 0.31) + 0.12 * std::cos(z + 0.28); + perturbation[3][ir] + = 0.25 * std::sin(5.0 * x + 0.18) + 0.23 * std::cos(7.0 * x - 0.29) + 0.10 * std::sin(y - z + 0.32); + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + } + + VxcResult evaluate_builtin(const std::string& functional = "PBE") + { + XC_Functional::set_xc_type(functional); + UnitCell cell; + cell.omega = pw.omega; + cell.tpiba = pw.tpiba; + return XC_Functional::v_xc(pw.nrxx, + &charge, + &cell, + 4, + true, + false, + 2, + XC_Functional::get_hybrid_alpha(), + XC_Functional::get_hse_omega()); + } + +#ifdef __LIBXC + VxcResult evaluate_libxc(const std::vector& functionals, const std::map* scaling_factor) + { + return XC_Functional_Libxc::v_xc_libxc(functionals, + pw.nrxx, + pw.omega, + pw.tpiba, + &charge, + 4, + true, + false, + 2, + scaling_factor, + 0.0, + 0.0); + } + + VxcResult evaluate_libxc_gga(const std::map* scaling_factor = nullptr) + { + const std::vector functionals = {XC_GGA_X_PBE, XC_GGA_C_PBE}; + return evaluate_libxc(functionals, scaling_factor); + } + + VxcResult evaluate_libxc_lda() + { + const std::vector functionals = {XC_LDA_X, XC_LDA_C_PZ}; + return evaluate_libxc(functionals, nullptr); + } + +#endif + + void set_uniform_state(const double total_density, const std::array& magnetization) + { + const std::array constant_direction = {{0.17, -0.11, 0.13, 0.09}}; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[0][ir] = total_density; + for (int mu = 0; mu < 3; ++mu) + { + density[mu + 1][ir] = magnetization[mu]; + } + core_density[ir] = 0.0; + for (int channel = 0; channel < 4; ++channel) + { + perturbation[channel][ir] += constant_direction[channel]; + } + } + std::fill(core_density_reciprocal.begin(), core_density_reciprocal.end(), std::complex(0.0, 0.0)); + } + + void set_inside_eta_state() + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const int ix = ir / (pw.ny * pw.nplane); + const int iy = (ir / pw.nplane) % pw.ny; + const int iz = ir % pw.nplane + pw.startz_current; + const double x = ModuleBase::TWO_PI * static_cast(ix) / pw.nx; + const double y = ModuleBase::TWO_PI * static_cast(iy) / pw.ny; + const double z = ModuleBase::TWO_PI * static_cast(iz) / pw.nz; + density[0][ir] = 0.030 + 0.002 * std::cos(x - y + 0.2); + density[1][ir] = 2.8e-4 + 0.6e-4 * std::sin(2.0 * x + 0.1); + density[2][ir] = -2.1e-4 + 0.5e-4 * std::cos(3.0 * x - z + 0.3); + density[3][ir] = 1.7e-4 + 0.4e-4 * std::sin(y + z - 0.2); + core_density[ir] = 0.004 + 0.001 * std::cos(2.0 * x + z - 0.1); + perturbation[0][ir] += 0.07; + perturbation[1][ir] += 0.13; + perturbation[2][ir] -= 0.11; + perturbation[3][ir] += 0.09; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + } + + void set_negative_gga_state() + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const int ix = ir / (pw.ny * pw.nplane); + const int iy = (ir / pw.nplane) % pw.ny; + const int iz = ir % pw.nplane + pw.startz_current; + const double x = ModuleBase::TWO_PI * static_cast(ix) / pw.nx; + const double y = ModuleBase::TWO_PI * static_cast(iy) / pw.ny; + const double z = ModuleBase::TWO_PI * static_cast(iz) / pw.nz; + density[0][ir] = -1.55 - 0.10 * std::cos(x - y + 0.2) - 0.05 * std::sin(3.0 * x + z - 0.1); + density[1][ir] = 0.30 + 0.05 * std::sin(2.0 * x + 0.1); + density[2][ir] = -0.24 + 0.04 * std::cos(3.0 * x - z + 0.3); + density[3][ir] = 0.20 + 0.03 * std::sin(y + z - 0.2); + core_density[ir] = -0.12 - 0.02 * std::cos(2.0 * x + z - 0.1); + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + } + + void set_saturated_gga_state() + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const int ix = ir / (pw.ny * pw.nplane); + const int iy = (ir / pw.nplane) % pw.ny; + const int iz = ir % pw.nplane + pw.startz_current; + const double x = ModuleBase::TWO_PI * static_cast(ix) / pw.nx; + const double y = ModuleBase::TWO_PI * static_cast(iy) / pw.ny; + const double z = ModuleBase::TWO_PI * static_cast(iz) / pw.nz; + density[0][ir] = 0.43 + 0.04 * std::cos(x - y + 0.2) + 0.02 * std::sin(3.0 * x + z - 0.1); + density[1][ir] = 0.64 + 0.06 * std::sin(2.0 * x + 0.1); + density[2][ir] = 0.34 + 0.05 * std::cos(3.0 * x - z + 0.3); + density[3][ir] = 0.28 + 0.04 * std::sin(y + z - 0.2); + core_density[ir] = 0.05 + 0.01 * std::cos(2.0 * x + z - 0.1); + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + } + + void set_zero_core_density() + { + std::fill(core_density.begin(), core_density.end(), 0.0); + std::fill(core_density_reciprocal.begin(), core_density_reciprocal.end(), std::complex(0.0, 0.0)); + } + + ReciprocalMetricState capture_reciprocal_metric_state() const + { + ReciprocalMetricState state; + state.gcar.assign(pw.gcar, pw.gcar + pw.npw); + state.density = density; + state.core_density = core_density; + state.core_density_reciprocal = core_density_reciprocal; + state.omega = pw.omega; + return state; + } + + void restore_reciprocal_metric_state(const ReciprocalMetricState& state) + { + ASSERT_EQ(state.gcar.size(), static_cast(pw.npw)); + for (int ig = 0; ig < pw.npw; ++ig) + { + pw.gcar[ig] = state.gcar[ig]; + } + for (int channel = 0; channel < 4; ++channel) + { + ASSERT_EQ(state.density[channel].size(), density[channel].size()); + std::copy(state.density[channel].begin(), state.density[channel].end(), density[channel].begin()); + } + ASSERT_EQ(state.core_density.size(), core_density.size()); + std::copy(state.core_density.begin(), state.core_density.end(), core_density.begin()); + ASSERT_EQ(state.core_density_reciprocal.size(), core_density_reciprocal.size()); + std::copy(state.core_density_reciprocal.begin(), + state.core_density_reciprocal.end(), + core_density_reciprocal.begin()); + pw.omega = state.omega; + } + + static void add_matrix_element(ModuleBase::Matrix3& matrix, const int row, const int column, const double value) + { + ASSERT_GE(row, 0); + ASSERT_LT(row, 3); + ASSERT_GE(column, 0); + ASSERT_LT(column, 3); + double* element = nullptr; + if (row == 0 && column == 0) + element = &matrix.e11; + if (row == 0 && column == 1) + element = &matrix.e12; + if (row == 0 && column == 2) + element = &matrix.e13; + if (row == 1 && column == 0) + element = &matrix.e21; + if (row == 1 && column == 1) + element = &matrix.e22; + if (row == 1 && column == 2) + element = &matrix.e23; + if (row == 2 && column == 0) + element = &matrix.e31; + if (row == 2 && column == 1) + element = &matrix.e32; + if (row == 2 && column == 2) + element = &matrix.e33; + ASSERT_NE(element, nullptr); + *element += value; + } + + double evaluate_reciprocal_metric_deformation(const ReciprocalMetricState& state, + const int stress_row, + const int stress_column, + const double epsilon, + const bool homogeneous_density_scaling, + const Evaluator& evaluator) + { + restore_reciprocal_metric_state(state); + + // gcar is stored as a row vector. Under r' = F r, reciprocal + // vectors transform as k' = k F^{-1}. Perturb F_{column,row}; its + // derivative contracts exactly with the lower-triangle convention + // sum_r h_row * g_column used by production stress_gga. + ModuleBase::Matrix3 deformation; + add_matrix_element(deformation, stress_column, stress_row, epsilon); + const double determinant = deformation.Det(); + const ModuleBase::Matrix3 inverse = deformation.Inverse(); + for (int ig = 0; ig < pw.npw; ++ig) + { + pw.gcar[ig] = state.gcar[ig] * inverse; + } + + if (homogeneous_density_scaling) + { + pw.omega = state.omega * determinant; + for (int channel = 0; channel < 4; ++channel) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[channel][ir] = state.density[channel][ir] / determinant; + } + } + for (int ir = 0; ir < pw.nrxx; ++ir) + { + core_density[ir] = state.core_density[ir] / determinant; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + } + + const double energy = std::get<0>(evaluator()); + restore_reciprocal_metric_state(state); + return energy; + } + + std::vector evaluate_gradient_stress_dispatch(const std::string& functional) + { + XC_Functional::set_xc_type(functional); + UnitCell cell; + cell.tpiba = pw.tpiba; + cell.magnet.lsign_ = false; + double dummy_energy = 0.0; + double dummy_vtxc = 0.0; + ModuleBase::matrix dummy_potential; + std::vector stress; + XC_Functional::gradcorr(dummy_energy, + dummy_vtxc, + dummy_potential, + &charge, + &pw, + &cell, + stress, + true, + 4, + true, + false, + 2, + 0.0, + 0.0); + EXPECT_EQ(stress.size(), 9U); + return stress; + } + + std::vector evaluate_builtin_gradient_stress() + { + return evaluate_gradient_stress_dispatch("PBE"); + } + +#ifdef __LIBXC + std::vector evaluate_libxc_pbe_gradient_stress_dispatch() + { + return evaluate_gradient_stress_dispatch("GGA_X_PBE+GGA_C_PBE"); + } +#endif + + void expect_gradient_stress_metric_derivative(const std::string& mode, + const Evaluator& energy_evaluator, + const StressEvaluator& stress_evaluator, + const double relative_tolerance, + const double absolute_floor) + { + const ReciprocalMetricState state = capture_reciprocal_metric_state(); + const std::vector local_stress = stress_evaluator(); + const std::array eps_values = {{2.0e-3, 1.0e-3, 5.0e-4, 2.5e-4}}; + + for (int row = 0; row < 3; ++row) + { + for (int column = 0; column <= row; ++column) + { + SCOPED_TRACE(mode + " metric component " + std::to_string(row) + std::to_string(column)); + const int index = row * 3 + column; + const double analytic = pool_sum(local_stress[index]) / pw.nxyz; + std::array errors = {{0.0, 0.0, 0.0, 0.0}}; + for (int ieps = 0; ieps < 4; ++ieps) + { + const double epsilon = eps_values[ieps]; + const double energy_plus + = evaluate_reciprocal_metric_deformation(state, row, column, epsilon, false, energy_evaluator); + const double energy_minus + = evaluate_reciprocal_metric_deformation(state, row, column, -epsilon, false, energy_evaluator); + const double finite_difference = -(energy_plus - energy_minus) / (2.0 * epsilon * state.omega); + errors[ieps] = std::abs(finite_difference - analytic); + if (is_pool_root()) + { + const double relative_scale + = std::max(1.0e-30, std::max(std::abs(analytic), std::abs(finite_difference))); + const double order = ieps == 0 || errors[ieps] == 0.0 + ? 0.0 + : std::log(errors[ieps - 1] / errors[ieps]) / std::log(2.0); + std::cout << std::setprecision(17) << "NCGGA_STRESS_METRIC_FD mode=" << mode << " row=" << row + << " column=" << column << " eps=" << epsilon << " analytic=" << analytic + << " finite_difference=" << finite_difference << " absolute_error=" << errors[ieps] + << " relative_error=" << errors[ieps] / relative_scale + << " convergence_order=" << order << '\n'; + } + } + // Stress components can be much smaller than one. Scaling + // the tolerance by max(1, |sigma|) would hide a persistent + // O(10%) slope offset on the regularized radial branch. + // The absolute floor only covers the observed FFT/energy + // roundoff plateau; the relative term still resolves every + // nonzero component in this fixture. + const double scale = std::max(1.0e-10, std::abs(analytic)); + const double tolerance = relative_tolerance * scale + absolute_floor; + EXPECT_LE(*std::min_element(errors.begin(), errors.end()), tolerance); + // Once the absolute error is at the MPI energy-roundoff + // plateau, individual refinements need not remain monotone. + // Require the central-difference O(eps^2) contraction only + // while both adjacent errors are still resolved above that + // plateau. The closure check above remains unconditional. + const double roundoff_plateau = 10.0 * absolute_floor; + if (errors[0] > roundoff_plateau && errors[1] > roundoff_plateau) + { + EXPECT_LE(errors[1], 0.4 * errors[0] + tolerance); + } + if (errors[1] > roundoff_plateau && errors[2] > roundoff_plateau) + { + EXPECT_LE(errors[2], 0.4 * errors[1] + tolerance); + } + } + } + } + + void expect_builtin_gradient_stress_metric_derivative(const std::string& mode) + { + expect_gradient_stress_metric_derivative( + mode, + [this]() { return evaluate_builtin(); }, + [this]() { return evaluate_builtin_gradient_stress(); }, + 2.0e-5, + 2.0e-13); + } + + void expect_full_xc_diagonal_stress(const std::string& mode, + const Evaluator& energy_evaluator, + const StressEvaluator& stress_evaluator) + { + // The returned vtxc is a valence-only four-channel inner product. + // Core deformation is accounted separately by stress_cc in the full + // PW stress path, so this isolated XC diagonal identity requires a + // zero core density. + set_zero_core_density(); + ASSERT_EQ(pool_max(*std::max_element(core_density.begin(), core_density.end())), 0.0); + const ReciprocalMetricState state = capture_reciprocal_metric_state(); + const VxcResult base = energy_evaluator(); + const std::vector local_stress = stress_evaluator(); + const double diagonal_local_term = -(std::get<0>(base) - std::get<1>(base)) / state.omega; + const std::array eps_values = {{2.0e-3, 1.0e-3, 5.0e-4, 2.5e-4}}; + + for (int diagonal = 0; diagonal < 3; ++diagonal) + { + SCOPED_TRACE(mode + " full diagonal " + std::to_string(diagonal)); + const double gradient_correction = pool_sum(local_stress[diagonal * 3 + diagonal]) / pw.nxyz; + const double analytic = diagonal_local_term + gradient_correction; + std::array errors = {{0.0, 0.0, 0.0, 0.0}}; + for (int ieps = 0; ieps < 4; ++ieps) + { + const double epsilon = eps_values[ieps]; + const double energy_plus = evaluate_reciprocal_metric_deformation(state, + diagonal, + diagonal, + epsilon, + true, + energy_evaluator); + const double energy_minus = evaluate_reciprocal_metric_deformation(state, + diagonal, + diagonal, + -epsilon, + true, + energy_evaluator); + const double finite_difference = -(energy_plus - energy_minus) / (2.0 * epsilon * state.omega); + errors[ieps] = std::abs(finite_difference - analytic); + if (is_pool_root()) + { + const double relative_scale + = std::max(1.0e-30, std::max(std::abs(analytic), std::abs(finite_difference))); + const double order = ieps == 0 || errors[ieps] == 0.0 + ? 0.0 + : std::log(errors[ieps - 1] / errors[ieps]) / std::log(2.0); + std::cout << std::setprecision(17) << "NCGGA_STRESS_FULL_FD mode=" << mode + << " diagonal=" << diagonal << " eps=" << epsilon + << " gradient_correction=" << gradient_correction + << " local_diagonal=" << diagonal_local_term << " analytic=" << analytic + << " finite_difference=" << finite_difference << " absolute_error=" << errors[ieps] + << " relative_error=" << errors[ieps] / relative_scale << " convergence_order=" << order + << '\n'; + } + } + const double scale = std::max(1.0, std::abs(analytic)); + EXPECT_LE(*std::min_element(errors.begin(), errors.end()), 5.0e-8 * scale); + EXPECT_LE(errors[1], 0.4 * errors[0] + 1.0e-8 * scale); + EXPECT_LE(errors[2], 0.4 * errors[1] + 1.0e-8 * scale); + } + } + + void expect_builtin_full_xc_diagonal_stress(const std::string& mode) + { + expect_full_xc_diagonal_stress( + mode, + [this]() { return evaluate_builtin(); }, + [this]() { return evaluate_builtin_gradient_stress(); }); + } + + BranchMargins report_branch_margins(const std::string& mode) + { + constexpr double lca_eta = 1.0e-3; + double local_min_abs_density = std::numeric_limits::max(); + double local_min_gap = std::numeric_limits::max(); + double local_max_gap = -std::numeric_limits::max(); + double local_min_magnitude = std::numeric_limits::max(); + double local_max_magnitude = 0.0; + double local_min_eta_distance = std::numeric_limits::max(); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double total = density[0][ir] + core_density[ir]; + const double magnitude = std::sqrt(density[1][ir] * density[1][ir] + density[2][ir] * density[2][ir] + + density[3][ir] * density[3][ir]); + const ModuleXC::NcggaRadialPoint radial + = ModuleXC::make_ncgga_radial_point({{density[1][ir], density[2][ir], density[3][ir]}}, lca_eta); + const double gap = std::abs(total) - radial.value; + local_min_abs_density = std::min(local_min_abs_density, std::abs(total)); + local_min_gap = std::min(local_min_gap, gap); + local_max_gap = std::max(local_max_gap, gap); + local_min_magnitude = std::min(local_min_magnitude, magnitude); + local_max_magnitude = std::max(local_max_magnitude, magnitude); + local_min_eta_distance = std::min(local_min_eta_distance, std::abs(magnitude - lca_eta)); + } + BranchMargins margins; + margins.min_abs_total_density = pool_min(local_min_abs_density); + margins.min_signed_saturation_gap = pool_min(local_min_gap); + margins.max_signed_saturation_gap = pool_max(local_max_gap); + margins.min_magnitude = pool_min(local_min_magnitude); + margins.max_magnitude = pool_max(local_max_magnitude); + margins.min_eta_distance = pool_min(local_min_eta_distance); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_BRANCH mode=" << mode + << " min_abs_total_density=" << margins.min_abs_total_density + << " min_signed_saturation_gap=" << margins.min_signed_saturation_gap + << " max_signed_saturation_gap=" << margins.max_signed_saturation_gap + << " min_magnitude=" << margins.min_magnitude << " max_magnitude=" << margins.max_magnitude + << " min_eta_distance=" << margins.min_eta_distance << '\n'; + } + return margins; + } + + void expect_vtxc_matches_returned_potential(const std::string& mode, const Evaluator& evaluate) + { + const VxcResult result = evaluate(); + const ModuleBase::matrix& potential = std::get<2>(result); + ASSERT_EQ(potential.nr, 4); + ASSERT_EQ(potential.nc, pw.nrxx); + double local_inner_product = 0.0; + for (int channel = 0; channel < 4; ++channel) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_inner_product += potential(channel, ir) * density[channel][ir]; + } + } + const double direct = pw.omega / pw.nxyz * pool_sum(local_inner_product); + const double reported = std::get<1>(result); + const double scale = std::max(1.0, std::max(std::abs(reported), std::abs(direct))); + EXPECT_NEAR(reported, direct, 2.0e-12 * scale); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_VTXC mode=" << mode << " energy=" << std::get<0>(result) + << " reported=" << reported << " direct=" << direct + << " absolute_error=" << std::abs(reported - direct) + << " potential_fnv1a64=" << potential_fnv1a64(potential) << '\n'; + } + } + + void expect_directional_derivatives_at_steps(const std::string& mode, + const Evaluator& evaluate, + const std::vector& eps_values) + { + ASSERT_GE(eps_values.size(), 3U); + const VxcResult base = evaluate(); + const ModuleBase::matrix& potential = std::get<2>(base); + ASSERT_EQ(potential.nr, 4); + ASSERT_EQ(potential.nc, pw.nrxx); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_ENERGY mode=" << mode << " energy=" << std::get<0>(base) + << " vtxc=" << std::get<1>(base) << '\n'; + } + + for (int channel = 0; channel < 4; ++channel) + { + SCOPED_TRACE(std::string("density channel ") + std::to_string(channel)); + double local_analytic = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_analytic += potential(channel, ir) * perturbation[channel][ir]; + } + const double analytic = pw.omega / pw.nxyz * pool_sum(local_analytic); + std::vector finite_difference(eps_values.size(), 0.0); + std::vector errors(eps_values.size(), 0.0); + + for (std::size_t ieps = 0; ieps < eps_values.size(); ++ieps) + { + const double eps = eps_values[ieps]; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[channel][ir] += eps * perturbation[channel][ir]; + } + const double energy_plus = std::get<0>(evaluate()); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[channel][ir] -= 2.0 * eps * perturbation[channel][ir]; + } + const double energy_minus = std::get<0>(evaluate()); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[channel][ir] += eps * perturbation[channel][ir]; + } + finite_difference[ieps] = (energy_plus - energy_minus) / (2.0 * eps); + errors[ieps] = std::abs(finite_difference[ieps] - analytic); + } + + const double scale = std::max(1.0, std::max(std::abs(analytic), std::abs(finite_difference.back()))); + EXPECT_LE(*std::min_element(errors.begin(), errors.end()), 3.0e-8 * scale); + EXPECT_LE(errors[1], 0.4 * errors[0] + 5.0e-9 * scale); + EXPECT_LE(errors[2], 0.4 * errors[1] + 5.0e-9 * scale); + if (is_pool_root()) + { + for (std::size_t ieps = 0; ieps < eps_values.size(); ++ieps) + { + const double relative_scale + = std::max(1.0e-30, std::max(std::abs(analytic), std::abs(finite_difference[ieps]))); + const double order = ieps == 0 || errors[ieps] == 0.0 + ? 0.0 + : std::log(errors[ieps - 1] / errors[ieps]) / std::log(2.0); + std::cout << std::setprecision(17) << "NCGGA_FD mode=" << mode << " channel=" << channel + << " eps=" << eps_values[ieps] << " analytic=" << analytic + << " finite_difference=" << finite_difference[ieps] << " absolute_error=" << errors[ieps] + << " relative_error=" << errors[ieps] / relative_scale << " convergence_order=" << order + << '\n'; + } + } + } + } + + void expect_core_directional_derivative(const std::string& mode, const Evaluator& evaluate) + { + const VxcResult base = evaluate(); + const ModuleBase::matrix& potential = std::get<2>(base); + double local_analytic = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_analytic += potential(0, ir) * perturbation[0][ir]; + } + const double analytic = pw.omega / pw.nxyz * pool_sum(local_analytic); + const std::array eps_values = {{1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3}}; + std::array errors = {{0.0, 0.0, 0.0, 0.0}}; + + for (int ieps = 0; ieps < 4; ++ieps) + { + const double eps = eps_values[ieps]; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + core_density[ir] += eps * perturbation[0][ir]; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const double energy_plus = std::get<0>(evaluate()); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + core_density[ir] -= 2.0 * eps * perturbation[0][ir]; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const double energy_minus = std::get<0>(evaluate()); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + core_density[ir] += eps * perturbation[0][ir]; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const double finite_difference = (energy_plus - energy_minus) / (2.0 * eps); + errors[ieps] = std::abs(finite_difference - analytic); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_CORE_FD mode=" << mode << " eps=" << eps + << " analytic=" << analytic << " finite_difference=" << finite_difference + << " absolute_error=" << errors[ieps] << '\n'; + } + } + const double scale = std::max(1.0, std::abs(analytic)); + EXPECT_LE(*std::min_element(errors.begin(), errors.end()), 3.0e-8 * scale); + EXPECT_LE(errors[1], 0.4 * errors[0] + 5.0e-9 * scale); + EXPECT_LE(errors[2], 0.4 * errors[1] + 5.0e-9 * scale); + } + + void expect_core_translation_force(const std::string& mode, const Evaluator& evaluate) + { + const VxcResult base = evaluate(); + const ModuleBase::matrix& potential = std::get<2>(base); + std::vector> reciprocal(pw.npw); + std::vector> core_gradient(pw.nrxx); + pw.real2recip(core_density.data(), reciprocal.data()); + XC_Functional::grad_rho(reciprocal.data(), core_gradient.data(), &pw, pw.tpiba); + double local_analytic = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_analytic += potential(0, ir) * core_gradient[ir].x; + } + const double analytic_force = pw.omega / pw.nxyz * pool_sum(local_analytic); + const std::vector original_core = core_density; + const std::array eps_values = {{2.0e-2, 1.0e-2, 5.0e-3, 2.5e-3}}; + std::array errors = {{0.0, 0.0, 0.0, 0.0}}; + for (int ieps = 0; ieps < 4; ++ieps) + { + const double eps = eps_values[ieps]; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + core_density[ir] = original_core[ir] - eps * core_gradient[ir].x; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const double energy_plus = std::get<0>(evaluate()); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + core_density[ir] = original_core[ir] + eps * core_gradient[ir].x; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const double energy_minus = std::get<0>(evaluate()); + std::copy(original_core.begin(), original_core.end(), core_density.begin()); + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const double finite_difference_force = -(energy_plus - energy_minus) / (2.0 * eps); + errors[ieps] = std::abs(finite_difference_force - analytic_force); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_CORE_FORCE_FD mode=" << mode << " eps=" << eps + << " analytic=" << analytic_force << " finite_difference=" << finite_difference_force + << " absolute_error=" << errors[ieps] << '\n'; + } + } + const double scale = std::max(1.0, std::abs(analytic_force)); + EXPECT_LE(*std::min_element(errors.begin(), errors.end()), 3.0e-8 * scale); + EXPECT_LE(errors[1], 0.4 * errors[0] + 5.0e-9 * scale); + EXPECT_LE(errors[2], 0.4 * errors[1] + 5.0e-9 * scale); + } + + void expect_core_repartition_invariance(const Evaluator& evaluate) + { + const VxcResult original = evaluate(); + const ModuleBase::matrix original_potential = std::get<2>(original); + double local_expected_vtxc_change = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double transfer = 0.04 * perturbation[0][ir]; + density[0][ir] += transfer; + core_density[ir] -= transfer; + local_expected_vtxc_change += original_potential(0, ir) * transfer; + } + pw.real2recip(core_density.data(), core_density_reciprocal.data()); + const VxcResult repartitioned = evaluate(); + const ModuleBase::matrix& repartitioned_potential = std::get<2>(repartitioned); + EXPECT_NEAR(std::get<0>(original), + std::get<0>(repartitioned), + 5.0e-11 * std::max(1.0, std::abs(std::get<0>(original)))); + for (int channel = 0; channel < 4; ++channel) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + EXPECT_NEAR(repartitioned_potential(channel, ir), + original_potential(channel, ir), + 8.0e-11 * std::max(1.0, std::abs(original_potential(channel, ir)))); + } + } + const double expected_vtxc_change = pw.omega / pw.nxyz * pool_sum(local_expected_vtxc_change); + const double actual_vtxc_change = std::get<1>(repartitioned) - std::get<1>(original); + EXPECT_NEAR(actual_vtxc_change, expected_vtxc_change, 8.0e-11 * std::max(1.0, std::abs(expected_vtxc_change))); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_CORE_REPARTITION expected_vtxc_change=" << expected_vtxc_change + << " actual_vtxc_change=" << actual_vtxc_change << '\n'; + } + } + + void expect_local_rotation_torque(const std::string& mode, const Evaluator& evaluate) + { + const VxcResult base = evaluate(); + const ModuleBase::matrix& potential = std::get<2>(base); + const std::vector original_mx = density[1]; + const std::vector original_my = density[2]; + double local_analytic = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_analytic + += perturbation[0][ir] * (potential(2, ir) * original_mx[ir] - potential(1, ir) * original_my[ir]); + } + const double analytic = pw.omega / pw.nxyz * pool_sum(local_analytic); + const std::array eps_values = {{1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3}}; + std::array errors = {{0.0, 0.0, 0.0, 0.0}}; + + for (int ieps = 0; ieps < 4; ++ieps) + { + const double eps = eps_values[ieps]; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double angle = eps * perturbation[0][ir]; + density[1][ir] = std::cos(angle) * original_mx[ir] - std::sin(angle) * original_my[ir]; + density[2][ir] = std::sin(angle) * original_mx[ir] + std::cos(angle) * original_my[ir]; + } + const double energy_plus = std::get<0>(evaluate()); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double angle = -eps * perturbation[0][ir]; + density[1][ir] = std::cos(angle) * original_mx[ir] - std::sin(angle) * original_my[ir]; + density[2][ir] = std::sin(angle) * original_mx[ir] + std::cos(angle) * original_my[ir]; + } + const double energy_minus = std::get<0>(evaluate()); + std::copy(original_mx.begin(), original_mx.end(), density[1].begin()); + std::copy(original_my.begin(), original_my.end(), density[2].begin()); + const double finite_difference = (energy_plus - energy_minus) / (2.0 * eps); + errors[ieps] = std::abs(finite_difference - analytic); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_TORQUE_FD mode=" << mode << " eps=" << eps + << " analytic=" << analytic << " finite_difference=" << finite_difference + << " absolute_error=" << errors[ieps] << '\n'; + } + } + const double scale = std::max(1.0, std::abs(analytic)); + EXPECT_LE(*std::min_element(errors.begin(), errors.end()), 3.0e-8 * scale); + EXPECT_LE(errors[1], 0.4 * errors[0] + 5.0e-9 * scale); + EXPECT_LE(errors[2], 0.4 * errors[1] + 5.0e-9 * scale); + } + + void expect_magnetization_inversion(const std::string& mode, const Evaluator& evaluate) + { + const VxcResult original = evaluate(); + const ModuleBase::matrix original_potential = std::get<2>(original); + for (int mu = 1; mu < 4; ++mu) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[mu][ir] = -density[mu][ir]; + } + } + const VxcResult inverted = evaluate(); + const ModuleBase::matrix& inverted_potential = std::get<2>(inverted); + + double local_charge_error = 0.0; + double local_spin_error = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_charge_error + = std::max(local_charge_error, std::abs(inverted_potential(0, ir) - original_potential(0, ir))); + for (int mu = 1; mu < 4; ++mu) + { + local_spin_error + = std::max(local_spin_error, std::abs(inverted_potential(mu, ir) + original_potential(mu, ir))); + } + } + const double charge_error = pool_max(local_charge_error); + const double spin_error = pool_max(local_spin_error); + const double energy_error = std::abs(std::get<0>(inverted) - std::get<0>(original)); + const double vtxc_error = std::abs(std::get<1>(inverted) - std::get<1>(original)); + const double energy_scale = std::max(1.0, std::abs(std::get<0>(original))); + const double vtxc_scale = std::max(1.0, std::abs(std::get<1>(original))); + EXPECT_LE(energy_error, 3.0e-11 * energy_scale); + EXPECT_LE(vtxc_error, 3.0e-11 * vtxc_scale); + EXPECT_LE(charge_error, 5.0e-11); + EXPECT_LE(spin_error, 5.0e-11); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_INVERSION mode=" << mode << " energy_error=" << energy_error + << " vtxc_error=" << vtxc_error << " max_charge_error=" << charge_error + << " max_spin_error=" << spin_error << '\n'; + } + } + + void expect_global_spin_rotation_covariance(const std::string& mode, const Evaluator& evaluate) + { + const VxcResult original = evaluate(); + const ModuleBase::matrix original_potential = std::get<2>(original); + const double angle = 0.371; + const double cosine = std::cos(angle); + const double sine = std::sin(angle); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double mx = density[1][ir]; + const double my = density[2][ir]; + density[1][ir] = cosine * mx - sine * my; + density[2][ir] = sine * mx + cosine * my; + } + const VxcResult rotated = evaluate(); + const ModuleBase::matrix& rotated_potential = std::get<2>(rotated); + + double local_charge_error = 0.0; + double local_spin_error = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + local_charge_error + = std::max(local_charge_error, std::abs(rotated_potential(0, ir) - original_potential(0, ir))); + const double expected_x = cosine * original_potential(1, ir) - sine * original_potential(2, ir); + const double expected_y = sine * original_potential(1, ir) + cosine * original_potential(2, ir); + local_spin_error = std::max(local_spin_error, std::abs(rotated_potential(1, ir) - expected_x)); + local_spin_error = std::max(local_spin_error, std::abs(rotated_potential(2, ir) - expected_y)); + local_spin_error + = std::max(local_spin_error, std::abs(rotated_potential(3, ir) - original_potential(3, ir))); + } + const double charge_error = pool_max(local_charge_error); + const double spin_error = pool_max(local_spin_error); + const double energy_error = std::abs(std::get<0>(rotated) - std::get<0>(original)); + const double vtxc_error = std::abs(std::get<1>(rotated) - std::get<1>(original)); + const double energy_scale = std::max(1.0, std::abs(std::get<0>(original))); + const double vtxc_scale = std::max(1.0, std::abs(std::get<1>(original))); + EXPECT_LE(energy_error, 3.0e-11 * energy_scale); + EXPECT_LE(vtxc_error, 3.0e-11 * vtxc_scale); + EXPECT_LE(charge_error, 5.0e-11); + EXPECT_LE(spin_error, 8.0e-11); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_GLOBAL_ROTATION mode=" << mode << " angle=" << angle + << " energy_error=" << energy_error << " vtxc_error=" << vtxc_error + << " max_charge_error=" << charge_error << " max_spin_error=" << spin_error << '\n'; + } + } + + void expect_zero_magnetization_is_regular(const std::string& mode, const Evaluator& evaluate) + { + for (int mu = 1; mu < 4; ++mu) + { + std::fill(density[mu].begin(), density[mu].end(), 0.0); + } + const VxcResult result = evaluate(); + const ModuleBase::matrix& potential = std::get<2>(result); + double local_maximum_spin_potential = 0.0; + EXPECT_TRUE(std::isfinite(std::get<0>(result))); + EXPECT_TRUE(std::isfinite(std::get<1>(result))); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + EXPECT_TRUE(std::isfinite(potential(0, ir))); + for (int mu = 1; mu < 4; ++mu) + { + EXPECT_TRUE(std::isfinite(potential(mu, ir))); + local_maximum_spin_potential = std::max(local_maximum_spin_potential, std::abs(potential(mu, ir))); + } + } + const double maximum_spin_potential = pool_max(local_maximum_spin_potential); + EXPECT_DOUBLE_EQ(maximum_spin_potential, 0.0); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_ZERO_MAG mode=" << mode << " energy=" << std::get<0>(result) + << " vtxc=" << std::get<1>(result) << " max_spin_potential=" << maximum_spin_potential << '\n'; + } + } +}; + +TEST_F(RealPwNcgga, PerturbationsSurviveThePwCutoff) +{ + for (int channel = 0; channel < 4; ++channel) + { + std::vector> reciprocal(pw.npw); + pw.real2recip(perturbation[channel].data(), reciprocal.data()); + double local_norm2 = 0.0; + for (int ig = 0; ig < pw.npw; ++ig) + { + local_norm2 += std::norm(reciprocal[ig]); + } + EXPECT_GT(pool_sum(local_norm2), 1.0e-4); + } +} + +TEST_F(RealPwNcgga, GradAndDivAreNegativeAdjoints) +{ + std::vector scalar(pw.nrxx); + std::vector> vector_field(pw.nrxx); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const int ix = ir / (pw.ny * pw.nplane); + const int iy = (ir / pw.nplane) % pw.ny; + const int iz = ir % pw.nplane + pw.startz_current; + const double x = ModuleBase::TWO_PI * static_cast(ix) / pw.nx; + const double y = ModuleBase::TWO_PI * static_cast(iy) / pw.ny; + const double z = ModuleBase::TWO_PI * static_cast(iz) / pw.nz; + scalar[ir] = 0.4 * std::sin(3.0 * x + 0.2) - 0.3 * std::cos(8.0 * x - 0.1) + 0.2 * std::sin(y - z + 0.4); + vector_field[ir].x = 0.7 * std::cos(2.0 * x + 0.3) + 0.2 * std::sin(7.0 * x); + vector_field[ir].y = 0.3 * std::sin(y - 0.4) + 0.1 * std::cos(x + z); + vector_field[ir].z = -0.25 * std::cos(z + 0.1) + 0.08 * std::sin(x - y); + } + + std::vector> reciprocal(pw.npw); + std::vector> gradient(pw.nrxx); + std::vector divergence(pw.nrxx); + pw.real2recip(scalar.data(), reciprocal.data()); + XC_Functional::grad_rho(reciprocal.data(), gradient.data(), &pw, pw.tpiba); + XC_Functional::grad_dot(vector_field.data(), divergence.data(), &pw, pw.tpiba); + + double local_identity = 0.0; + double local_norm = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double left = vector_field[ir] * gradient[ir]; + const double right = divergence[ir] * scalar[ir]; + local_identity += left + right; + local_norm += std::abs(left) + std::abs(right); + } + const double identity = pw.omega / pw.nxyz * pool_sum(local_identity); + const double norm = pw.omega / pw.nxyz * pool_sum(local_norm); + EXPECT_GT(norm, 1.0e-4); + EXPECT_LE(std::abs(identity), 5.0e-11 * std::max(1.0, norm)); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_ADJOINT identity=" << identity << " norm=" << norm + << " scaled_error=" << std::abs(identity) / norm << '\n'; + } +} + +TEST_F(RealPwNcgga, ProjectedLcaGraphRequiresDiscreteFluxReverse) +{ + // This eta is the documented gga_grad=2 policy. Keep it local so the + // identical test-only patch compiles on the behavior commit's parent. + constexpr double lca_eta = 1.0e-3; + typedef std::array>, 3> Gradients; + + const auto gradients = [&]() { + Gradients result; + std::vector> reciprocal(pw.npw); + for (int mu = 0; mu < 3; ++mu) + { + result[mu].resize(pw.nrxx); + pw.real2recip(density[mu + 1].data(), reciprocal.data()); + XC_Functional::grad_rho(reciprocal.data(), result[mu].data(), &pw, pw.tpiba); + } + return result; + }; + + const auto graph_energy = [&]() { + const Gradients grad_m = gradients(); + double local_energy = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const std::array magnetization = {{density[1][ir], density[2][ir], density[3][ir]}}; + const ModuleXC::NcggaRadialPoint radial = ModuleXC::make_ncgga_radial_point(magnetization, lca_eta); + ModuleBase::Vector3 projected; + for (int nu = 0; nu < 3; ++nu) + { + projected += radial.gradient[nu] * grad_m[nu][ir]; + } + local_energy += 0.5 * (projected * projected); + } + return pw.omega / pw.nxyz * pool_sum(local_energy); + }; + + const Gradients grad_m = gradients(); + std::vector> projected(pw.nrxx); + std::array, 3> exact_potential; + std::array, 3> old_surrogate; + std::vector projected_divergence(pw.nrxx); + std::vector> flux(pw.nrxx); + std::vector divergence(pw.nrxx); + for (int mu = 0; mu < 3; ++mu) + { + exact_potential[mu].resize(pw.nrxx); + old_surrogate[mu].resize(pw.nrxx); + } + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const std::array magnetization = {{density[1][ir], density[2][ir], density[3][ir]}}; + const ModuleXC::NcggaRadialPoint radial = ModuleXC::make_ncgga_radial_point(magnetization, lca_eta); + for (int nu = 0; nu < 3; ++nu) + { + projected[ir] += radial.gradient[nu] * grad_m[nu][ir]; + } + } + XC_Functional::grad_dot(projected.data(), projected_divergence.data(), &pw, pw.tpiba); + + double maximum_surrogate_error = 0.0; + double maximum_exact_error = 0.0; + const std::array eps_values = {{1.0e-3, 5.0e-4, 2.5e-4, 1.25e-4, 6.25e-5}}; + for (int mu = 0; mu < 3; ++mu) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const std::array magnetization = {{density[1][ir], density[2][ir], density[3][ir]}}; + const ModuleXC::NcggaRadialPoint radial = ModuleXC::make_ncgga_radial_point(magnetization, lca_eta); + flux[ir] = radial.gradient[mu] * projected[ir]; + } + XC_Functional::grad_dot(flux.data(), divergence.data(), &pw, pw.tpiba); + + double local_exact_projection = 0.0; + double local_surrogate_projection = 0.0; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const std::array magnetization = {{density[1][ir], density[2][ir], density[3][ir]}}; + const ModuleXC::NcggaRadialPoint radial = ModuleXC::make_ncgga_radial_point(magnetization, lca_eta); + double local_response = 0.0; + for (int nu = 0; nu < 3; ++nu) + { + local_response += radial.jacobian(nu, mu) * (projected[ir] * grad_m[nu][ir]); + } + exact_potential[mu][ir] = local_response - divergence[ir]; + old_surrogate[mu][ir] = -radial.gradient[mu] * projected_divergence[ir]; + local_exact_projection += exact_potential[mu][ir] * perturbation[mu + 1][ir]; + local_surrogate_projection += old_surrogate[mu][ir] * perturbation[mu + 1][ir]; + } + const double exact = pw.omega / pw.nxyz * pool_sum(local_exact_projection); + const double surrogate = pw.omega / pw.nxyz * pool_sum(local_surrogate_projection); + + std::array exact_errors = {{0.0, 0.0, 0.0, 0.0, 0.0}}; + std::array surrogate_errors = {{0.0, 0.0, 0.0, 0.0, 0.0}}; + for (int ieps = 0; ieps < 5; ++ieps) + { + const double eps = eps_values[ieps]; + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[mu + 1][ir] += eps * perturbation[mu + 1][ir]; + } + const double energy_plus = graph_energy(); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[mu + 1][ir] -= 2.0 * eps * perturbation[mu + 1][ir]; + } + const double energy_minus = graph_energy(); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[mu + 1][ir] += eps * perturbation[mu + 1][ir]; + } + const double finite_difference = (energy_plus - energy_minus) / (2.0 * eps); + exact_errors[ieps] = std::abs(exact - finite_difference); + surrogate_errors[ieps] = std::abs(surrogate - finite_difference); + maximum_exact_error = std::max(maximum_exact_error, exact_errors[ieps]); + maximum_surrogate_error = std::max(maximum_surrogate_error, surrogate_errors[ieps]); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "NCGGA_PROJECTED_REVERSE channel=" << mu + 1 << " eps=" << eps + << " exact=" << exact << " old_surrogate=" << surrogate + << " finite_difference=" << finite_difference << " exact_error=" << exact_errors[ieps] + << " surrogate_error=" << surrogate_errors[ieps] << '\n'; + } + } + const double scale = std::max(1.0, std::max(std::abs(exact), std::abs(surrogate))); + EXPECT_LE(*std::min_element(exact_errors.begin(), exact_errors.end()), 2.0e-8 * scale); + EXPECT_GT(*std::min_element(surrogate_errors.begin(), surrogate_errors.end()), 2.0e-7 * scale); + } + EXPECT_GT(maximum_surrogate_error, 100.0 * maximum_exact_error); +} + +#ifdef __LIBXC + +TEST_F(RealPwNcgga, LibxcGgaGrad2VtxcBookkeepingUsesFinalReturnedPotential) +{ + const std::vector functionals = {XC_LDA_X, XC_GGA_C_PBE}; + const Evaluator evaluate = [this, &functionals]() { return evaluate_libxc(functionals, nullptr); }; + expect_vtxc_matches_returned_potential("libxc_gga2_mixed", evaluate); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2VtxcEqualsFinalValencePotentialInnerProduct) +{ + report_branch_margins("libxc_vtxc_smooth"); + expect_vtxc_matches_returned_potential("libxc_gga2_smooth", [this]() { return evaluate_libxc_gga(); }); + + set_negative_gga_state(); + report_branch_margins("libxc_vtxc_negative"); + expect_vtxc_matches_returned_potential("libxc_gga2_negative", [this]() { return evaluate_libxc_gga(); }); + + set_saturated_gga_state(); + report_branch_margins("libxc_vtxc_saturated"); + expect_vtxc_matches_returned_potential("libxc_gga2_saturated", [this]() { return evaluate_libxc_gga(); }); + + set_inside_eta_state(); + report_branch_margins("libxc_vtxc_inside_eta"); + expect_vtxc_matches_returned_potential("libxc_gga2_inside_eta", [this]() { return evaluate_libxc_gga(); }); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2IsDiscreteGradientOnSmoothProjectedBranch) +{ + const BranchMargins margins = report_branch_margins("libxc_smooth"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + EXPECT_GT(margins.min_eta_distance, 0.3); + expect_directional_derivatives_at_steps("libxc_gga2_smooth", + [this]() { return evaluate_libxc_gga(); }, + {1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2IsDiscreteGradientInsideRadialEta) +{ + set_inside_eta_state(); + const BranchMargins margins = report_branch_margins("libxc_inside_eta"); + EXPECT_GT(margins.min_abs_total_density, 0.025); + EXPECT_LT(margins.max_magnitude, 6.0e-4); + EXPECT_GT(margins.min_eta_distance, 4.0e-4); + expect_directional_derivatives_at_steps( + "libxc_gga2_inside_eta", + [this]() { return evaluate_libxc_gga(); }, + {2.0e-4, 1.0e-4, 5.0e-5, 2.5e-5, 1.25e-5, 6.25e-6, 3.125e-6, 1.5625e-6, 7.8125e-7, 3.90625e-7}); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2DifferentiatesNegativeDensityBranch) +{ + set_negative_gga_state(); + const BranchMargins margins = report_branch_margins("libxc_negative"); + EXPECT_GT(margins.min_abs_total_density, 1.3); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + expect_directional_derivatives_at_steps("libxc_gga2_negative", + [this]() { return evaluate_libxc_gga(); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2DifferentiatesSaturatedGgaBranch) +{ + set_saturated_gga_state(); + const BranchMargins margins = report_branch_margins("libxc_saturated"); + EXPECT_LT(margins.max_signed_saturation_gap, -0.1); + expect_directional_derivatives_at_steps("libxc_gga2_saturated", + [this]() { return evaluate_libxc_gga(); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, LibxcLdaGgaGrad2DifferentiatesLocalMapBranches) +{ + set_uniform_state(-1.4, {{0.20, -0.16, 0.18}}); + const BranchMargins negative = report_branch_margins("libxc_lda_negative"); + EXPECT_GT(negative.min_abs_total_density, 1.3); + EXPECT_GT(negative.min_signed_saturation_gap, 1.0); + expect_directional_derivatives_at_steps("libxc_lda_gga2_negative", + [this]() { return evaluate_libxc_lda(); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); + + set_uniform_state(0.45, {{0.65, 0.30, 0.20}}); + const BranchMargins saturated = report_branch_margins("libxc_lda_saturated"); + EXPECT_LT(saturated.max_signed_saturation_gap, -0.25); + expect_directional_derivatives_at_steps("libxc_lda_gga2_saturated", + [this]() { return evaluate_libxc_lda(); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2DifferentiatesCoreDensityAndLocalRotation) +{ + const BranchMargins margins = report_branch_margins("libxc_core_rotation"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + expect_core_directional_derivative("libxc_gga2_core", [this]() { return evaluate_libxc_gga(); }); + expect_core_translation_force("libxc_gga2_core_translation", [this]() { return evaluate_libxc_gga(); }); + expect_local_rotation_torque("libxc_gga2_local_rotation", [this]() { return evaluate_libxc_gga(); }); + expect_core_repartition_invariance([this]() { return evaluate_libxc_gga(); }); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2RespectsSpinInversionAndZeroLimit) +{ + report_branch_margins("libxc_symmetry"); + expect_magnetization_inversion("libxc_gga2", [this]() { return evaluate_libxc_gga(); }); + expect_zero_magnetization_is_regular("libxc_gga2", [this]() { return evaluate_libxc_gga(); }); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2IsCovariantUnderGlobalSpinRotation) +{ + report_branch_margins("libxc_global_rotation"); + expect_global_spin_rotation_covariance("libxc_gga2", [this]() { return evaluate_libxc_gga(); }); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2DifferentiatesNonuniformFunctionalScaling) +{ + const std::map scaling_factor = {{XC_GGA_X_PBE, 0.37}, {XC_GGA_C_PBE, 1.23}}; + const BranchMargins margins = report_branch_margins("libxc_scaled"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "LIBXC_SCALING mode=libxc_gga2_scaled" + << " exchange=" << scaling_factor.at(XC_GGA_X_PBE) + << " correlation=" << scaling_factor.at(XC_GGA_C_PBE) << '\n'; + } + const Evaluator evaluate = [this, &scaling_factor]() { return evaluate_libxc_gga(&scaling_factor); }; + + // A finite difference alone cannot prove that component scaling was + // applied: energy and potential could omit the same factors and remain + // self-consistent. Independently require linearity against unscaled + // exchange-only and correlation-only evaluations. + const VxcResult exchange = evaluate_libxc({XC_GGA_X_PBE}, nullptr); + const VxcResult correlation = evaluate_libxc({XC_GGA_C_PBE}, nullptr); + const VxcResult scaled = evaluate(); + const double exchange_factor = scaling_factor.at(XC_GGA_X_PBE); + const double correlation_factor = scaling_factor.at(XC_GGA_C_PBE); + const double expected_energy + = exchange_factor * std::get<0>(exchange) + correlation_factor * std::get<0>(correlation); + const double expected_vtxc + = exchange_factor * std::get<1>(exchange) + correlation_factor * std::get<1>(correlation); + const double energy_error = std::abs(std::get<0>(scaled) - expected_energy); + const double vtxc_error = std::abs(std::get<1>(scaled) - expected_vtxc); + double local_potential_error = 0.0; + for (int channel = 0; channel < 4; ++channel) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double expected = exchange_factor * std::get<2>(exchange)(channel, ir) + + correlation_factor * std::get<2>(correlation)(channel, ir); + local_potential_error + = std::max(local_potential_error, std::abs(std::get<2>(scaled)(channel, ir) - expected)); + } + } + const double potential_error = pool_max(local_potential_error); + EXPECT_LE(energy_error, 5.0e-11 * std::max(1.0, std::abs(expected_energy))); + EXPECT_LE(vtxc_error, 5.0e-11 * std::max(1.0, std::abs(expected_vtxc))); + EXPECT_LE(potential_error, 8.0e-11); + if (is_pool_root()) + { + std::cout << std::setprecision(17) << "LIBXC_SCALING_LINEARITY mode=libxc_gga2_scaled" + << " energy_error=" << energy_error << " vtxc_error=" << vtxc_error + << " max_potential_error=" << potential_error << '\n'; + } + + expect_directional_derivatives_at_steps("libxc_gga2_scaled", evaluate, {1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); + expect_vtxc_matches_returned_potential("libxc_gga2_scaled", evaluate); +} + +TEST_F(RealPwNcgga, LibxcGgaGrad2DifferentiatesMixedLdaAndGgaComponents) +{ + const std::vector functionals = {XC_LDA_X, XC_GGA_C_PBE}; + const Evaluator evaluate = [this, &functionals]() { return evaluate_libxc(functionals, nullptr); }; + const BranchMargins margins = report_branch_margins("libxc_mixed_lda_gga"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + expect_directional_derivatives_at_steps("libxc_gga2_mixed_lda_gga", + evaluate, + {1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); + expect_vtxc_matches_returned_potential("libxc_gga2_mixed_lda_gga", evaluate); +} + + + + + + + + +#endif + +TEST_F(RealPwNcgga, BuiltinGgaGrad2VtxcEqualsFinalValencePotentialInnerProduct) +{ + expect_vtxc_matches_returned_potential("smooth", [this]() { return evaluate_builtin(); }); + + set_negative_gga_state(); + expect_vtxc_matches_returned_potential("negative", [this]() { return evaluate_builtin(); }); + + set_saturated_gga_state(); + expect_vtxc_matches_returned_potential("saturated", [this]() { return evaluate_builtin(); }); + + set_inside_eta_state(); + expect_vtxc_matches_returned_potential("inside_eta", [this]() { return evaluate_builtin(); }); +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2IsDiscreteGradientOnSmoothProjectedBranch) +{ + const BranchMargins margins = report_branch_margins("smooth"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + EXPECT_GT(margins.min_eta_distance, 0.3); + expect_directional_derivatives_at_steps("builtin_gga2_smooth", + [this]() { return evaluate_builtin(); }, + {1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2IsDiscreteGradientInsideRadialEta) +{ + set_inside_eta_state(); + const BranchMargins margins = report_branch_margins("inside_eta"); + EXPECT_GT(margins.min_abs_total_density, 0.025); + EXPECT_LT(margins.max_magnitude, 6.0e-4); + EXPECT_GT(margins.min_eta_distance, 4.0e-4); + expect_directional_derivatives_at_steps( + "builtin_gga2_inside_eta", + [this]() { return evaluate_builtin(); }, + {2.0e-4, 1.0e-4, 5.0e-5, 2.5e-5, 1.25e-5, 6.25e-6, 3.125e-6, 1.5625e-6, 7.8125e-7, 3.90625e-7}); +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2DifferentiatesNegativeDensityBranch) +{ + set_negative_gga_state(); + const BranchMargins margins = report_branch_margins("negative"); + EXPECT_GT(margins.min_abs_total_density, 1.3); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + expect_directional_derivatives_at_steps("builtin_gga2_negative", + [this]() { return evaluate_builtin(); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2DifferentiatesSaturatedGgaBranch) +{ + set_saturated_gga_state(); + const BranchMargins margins = report_branch_margins("saturated"); + EXPECT_LT(margins.max_signed_saturation_gap, -0.1); + expect_directional_derivatives_at_steps("builtin_gga2_saturated_gga", + [this]() { return evaluate_builtin(); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, BuiltinLdaGgaGrad2DifferentiatesLocalMapBranches) +{ + set_uniform_state(-1.4, {{0.20, -0.16, 0.18}}); + expect_directional_derivatives_at_steps("builtin_lda_gga2_negative", + [this]() { return evaluate_builtin("PZ"); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); + + set_uniform_state(0.45, {{0.65, 0.30, 0.20}}); + expect_directional_derivatives_at_steps("builtin_lda_gga2_saturated", + [this]() { return evaluate_builtin("PZ"); }, + {5.0e-3, 2.5e-3, 1.25e-3, 6.25e-4}); +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2DifferentiatesCoreDensityAndLocalRotation) +{ + expect_core_directional_derivative("builtin_gga2_core", [this]() { return evaluate_builtin(); }); + expect_core_translation_force("builtin_gga2_core_translation", [this]() { return evaluate_builtin(); }); + expect_local_rotation_torque("builtin_gga2_rotation", [this]() { return evaluate_builtin(); }); + expect_core_repartition_invariance([this]() { return evaluate_builtin(); }); +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2RespectsSpinSymmetriesAndZeroLimit) +{ + const VxcResult original = evaluate_builtin(); + const ModuleBase::matrix original_potential = std::get<2>(original); + for (int mu = 1; mu < 4; ++mu) + { + for (int ir = 0; ir < pw.nrxx; ++ir) + { + density[mu][ir] = -density[mu][ir]; + } + } + const VxcResult inverted = evaluate_builtin(); + const ModuleBase::matrix& inverted_potential = std::get<2>(inverted); + EXPECT_NEAR(std::get<0>(original), std::get<0>(inverted), 3.0e-11 * std::max(1.0, std::abs(std::get<0>(original)))); + EXPECT_NEAR(std::get<1>(original), std::get<1>(inverted), 3.0e-11 * std::max(1.0, std::abs(std::get<1>(original)))); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + EXPECT_NEAR(inverted_potential(0, ir), original_potential(0, ir), 3.0e-11); + for (int mu = 1; mu < 4; ++mu) + { + EXPECT_NEAR(inverted_potential(mu, ir), + -original_potential(mu, ir), + 5.0e-11 * std::max(1.0, std::abs(original_potential(mu, ir)))); + } + } + + for (int mu = 1; mu < 4; ++mu) + { + std::fill(density[mu].begin(), density[mu].end(), 0.0); + } + const VxcResult zero = evaluate_builtin(); + EXPECT_TRUE(std::isfinite(std::get<0>(zero))); + EXPECT_TRUE(std::isfinite(std::get<1>(zero))); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + EXPECT_TRUE(std::isfinite(std::get<2>(zero)(0, ir))); + EXPECT_DOUBLE_EQ(std::get<2>(zero)(1, ir), 0.0); + EXPECT_DOUBLE_EQ(std::get<2>(zero)(2, ir), 0.0); + EXPECT_DOUBLE_EQ(std::get<2>(zero)(3, ir), 0.0); + } +} + +TEST_F(RealPwNcgga, BuiltinGgaGrad2IsCovariantUnderGlobalSpinRotation) +{ + const VxcResult original = evaluate_builtin(); + const ModuleBase::matrix original_potential = std::get<2>(original); + const double angle = 0.371; + const double cosine = std::cos(angle); + const double sine = std::sin(angle); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + const double mx = density[1][ir]; + const double my = density[2][ir]; + density[1][ir] = cosine * mx - sine * my; + density[2][ir] = sine * mx + cosine * my; + } + const VxcResult rotated = evaluate_builtin(); + const ModuleBase::matrix& rotated_potential = std::get<2>(rotated); + EXPECT_NEAR(std::get<0>(original), std::get<0>(rotated), 3.0e-11 * std::max(1.0, std::abs(std::get<0>(original)))); + EXPECT_NEAR(std::get<1>(original), std::get<1>(rotated), 3.0e-11 * std::max(1.0, std::abs(std::get<1>(original)))); + for (int ir = 0; ir < pw.nrxx; ++ir) + { + EXPECT_NEAR(rotated_potential(0, ir), original_potential(0, ir), 3.0e-11); + const double expected_x = cosine * original_potential(1, ir) - sine * original_potential(2, ir); + const double expected_y = sine * original_potential(1, ir) + cosine * original_potential(2, ir); + EXPECT_NEAR(rotated_potential(1, ir), expected_x, 5.0e-11 * std::max(1.0, std::abs(expected_x))); + EXPECT_NEAR(rotated_potential(2, ir), expected_y, 5.0e-11 * std::max(1.0, std::abs(expected_y))); + EXPECT_NEAR(rotated_potential(3, ir), + original_potential(3, ir), + 5.0e-11 * std::max(1.0, std::abs(original_potential(3, ir)))); + } +} + + + + + + + + + +} // namespace + +#ifdef __MPI +int main(int argc, char** argv) +{ + int threads = 1; + Parallel_Global::read_pal_param(argc, argv, test_size, threads, test_rank); + POOL_WORLD = MPI_COMM_WORLD; + KP_WORLD = MPI_COMM_NULL; + INT_BGROUP = MPI_COMM_NULL; + BP_WORLD = MPI_COMM_NULL; + GRID_WORLD = MPI_COMM_NULL; + DIAG_WORLD = MPI_COMM_NULL; + testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); + Parallel_Global::finalize_mpi(); + return result; +} +#endif diff --git a/source/source_hamilt/module_xc/xc_functional.h b/source/source_hamilt/module_xc/xc_functional.h index 66ac7adb21b..59607e15f3d 100644 --- a/source/source_hamilt/module_xc/xc_functional.h +++ b/source/source_hamilt/module_xc/xc_functional.h @@ -52,6 +52,7 @@ class XC_Functional const int nspin, const bool domag, const bool domag_z, + const int gga_grad, const double hybrid_alpha, const double hse_omega); @@ -233,6 +234,7 @@ class XC_Functional const int nspin, const bool domag, const bool domag_z, + const int gga_grad, const double hybrid_alpha, const double hse_omega); diff --git a/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp b/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp new file mode 100644 index 00000000000..cb4e31eba5f --- /dev/null +++ b/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp @@ -0,0 +1,239 @@ +#include "xc_functional_ncgga_sf.h" + +#include "source_base/parallel_reduce.h" +#include "source_base/timer.h" +#include "source_base/vector3.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_estate/module_charge/charge.h" +#include "xc_functional.h" +#include "xc_ncgga_radial.h" + +#include +#include +#include + +namespace ModuleXC +{ +namespace NCGGA_SF_Builtin +{ + +std::tuple v_xc_ncgga_sf_builtin(const int& nrxx, + const double& omega, + const double tpiba, + const Charge* const chr) +{ + ModuleBase::TITLE("XC_Functional", "v_xc_ncgga_sf_builtin"); + ModuleBase::timer::start("XC_Functional", "v_xc_ncgga_sf_builtin"); + + // Regularized projected local-collinear energy: rho_s = N_s(n + rho_core, m), + // g_s = sum_A J_sA G_h x_A. Reverse the same discrete graph, including + // both -D_h(sum_s J_sA h_s) and the local Hessian response of N_s. + ModulePW::PW_Basis* rhopw = chr->rhopw; + const int npw = rhopw->npw; + const double e2 = ModuleBase::e2; + constexpr double vanishing = 1e-10; + constexpr double epsr = 1e-6; + const bool is_gga = (XC_Functional::get_func_type() == 2 || XC_Functional::get_func_type() == 4); + + std::vector rhotmp1(nrxx); + std::vector rhotmp2(nrxx); + std::vector spin_map(nrxx); + + for (int ir = 0; ir < nrxx; ++ir) + { + const double mx = chr->rho[1][ir]; + const double my = chr->rho[2][ir]; + const double mz = chr->rho[3][ir]; + + const std::array magnetization = {{mx, my, mz}}; + const NcggaRadialPoint radial = make_ncgga_radial_point(magnetization, ncgga_lca_radial_eta()); + spin_map[ir] = make_ncgga_spin_map_point(chr->rho[0][ir] + chr->rho_core[ir], radial); + rhotmp1[ir] = spin_map[ir].spin_density[0]; + rhotmp2[ir] = spin_map[ir].spin_density[1]; + } + + std::vector> rhogsum1(npw); + std::vector> tmp_recip(npw); + rhopw->real2recip(chr->rho[0], rhogsum1.data()); + for (int ig = 0; ig < npw; ++ig) + rhogsum1[ig] += chr->rhog_core[ig]; + + std::vector> gdr1(nrxx); + std::vector> gdr2(nrxx); + std::vector> grad_rho(nrxx); + std::array>, 3> grad_m; + for (int mu = 0; mu < 3; ++mu) + { + grad_m[mu].resize(nrxx); + } + std::vector> gdr_mag(nrxx); + XC_Functional::grad_rho(rhogsum1.data(), gdr1.data(), rhopw, tpiba); + + for (int ir = 0; ir < nrxx; ++ir) + { + grad_rho[ir] = gdr1[ir]; + + gdr1[ir] = spin_map[ir].jacobian(0, 0) * grad_rho[ir]; + gdr2[ir] = spin_map[ir].jacobian(1, 0) * grad_rho[ir]; + } + for (int is = 1; is <= 3; ++is) + { + rhopw->real2recip(chr->rho[is], tmp_recip.data()); + XC_Functional::grad_rho(tmp_recip.data(), gdr_mag.data(), rhopw, tpiba); + grad_m[is - 1] = gdr_mag; + for (int ir = 0; ir < nrxx; ++ir) + { + + gdr1[ir] += spin_map[ir].jacobian(0, is) * gdr_mag[ir]; + gdr2[ir] += spin_map[ir].jacobian(1, is) * gdr_mag[ir]; + } + } + + double etxc = 0; + double vtxc = 0; + ModuleBase::matrix v(4, nrxx); + + for (int ir = 0; ir < nrxx; ++ir) + { + const double arho = spin_map[ir].absolute_density; + if (arho <= vanishing) + continue; + + double zeta = spin_map[ir].clipped_magnitude / arho; + if (std::abs(zeta) > 1.0) + zeta = (zeta > 0) ? 1.0 : -1.0; + double exc = 0; + double vxc[2] = {0, 0}; + XC_Functional::xc_spin(arho, zeta, exc, vxc[0], vxc[1]); + + for (int channel = 0; channel < 4; ++channel) + { + v(channel, ir) + = e2 * (spin_map[ir].jacobian(0, channel) * vxc[0] + spin_map[ir].jacobian(1, channel) * vxc[1]); + } + + etxc += e2 * exc * arho; + } + + // Step 4: GGA contribution and variational divergence correction. + if (is_gga) + { + double etxcgc = 0; + std::vector vup_gga(nrxx, 0); + std::vector vdw_gga(nrxx, 0); + std::vector> h1(nrxx); + std::vector> h2(nrxx); + for (int ir = 0; ir < nrxx; ++ir) + { + double sx = 0; + double v1xup = 0; + double v1xdw = 0; + double v2xup = 0; + double v2xdw = 0; + double sc = 0; + double v1cup = 0; + double v1cdw = 0; + double v2c = 0; + double grho2a = gdr1[ir] * gdr1[ir]; + double grho2b = gdr2[ir] * gdr2[ir]; + + const double rh = rhotmp1[ir] + rhotmp2[ir]; + + XC_Functional::gcx_spin(rhotmp1[ir], rhotmp2[ir], grho2a, grho2b, sx, v1xup, v1xdw, v2xup, v2xdw); + + if (rh > epsr) + { + const double zeta_input = std::fabs((rhotmp1[ir] - rhotmp2[ir]) / rh); + double zeta = zeta_input; + const double grh2 = (gdr1[ir] + gdr2[ir]) * (gdr1[ir] + gdr2[ir]); + XC_Functional::gcc_spin(rh, zeta, grh2, sc, v1cup, v1cdw, v2c); + if (zeta_input > 1.0 - epsr) + { + // gcc_spin evaluates this branch at a fixed clipped zeta. + // Reverse that actual branch instead of differentiating + // through the discarded input polarization. + const double fixed_zeta_density_derivative = 0.5 * ((1.0 + zeta) * v1cup + (1.0 - zeta) * v1cdw); + v1cup = fixed_zeta_density_derivative; + v1cdw = fixed_zeta_density_derivative; + } + } + + vup_gga[ir] = e2 * (v1xup + v1cup); + vdw_gga[ir] = e2 * (v1xdw + v1cdw); + + const double v2cup = v2c; + const double v2cdw = v2c; + const double v2cud = v2c; + h1[ir] = e2 * ((v2xup + v2cup) * gdr1[ir] + v2cud * gdr2[ir]); + h2[ir] = e2 * ((v2xdw + v2cdw) * gdr2[ir] + v2cud * gdr1[ir]); + + etxcgc += e2 * (sx + sc); + } + + for (int ir = 0; ir < nrxx; ++ir) + { + + for (int channel = 0; channel < 4; ++channel) + { + v(channel, ir) += spin_map[ir].jacobian(0, channel) * vup_gga[ir] + + spin_map[ir].jacobian(1, channel) * vdw_gga[ir]; + } + } + + std::vector dh(nrxx); + std::vector> tmp_h(nrxx); + + // Exact reverse of g_s=sum_A J_sA G_h(x_A): + // v_B = -D_h(sum_s J_sB h_s) + // + sum_s,A dJ_sA/dx_B h_s.G_h(x_A). + for (int channel = 0; channel < 4; ++channel) + { + for (int ir = 0; ir < nrxx; ++ir) + { + tmp_h[ir] = spin_map[ir].jacobian(0, channel) * h1[ir] + spin_map[ir].jacobian(1, channel) * h2[ir]; + } + XC_Functional::grad_dot(tmp_h.data(), dh.data(), rhopw, tpiba); + for (int ir = 0; ir < nrxx; ++ir) + { + v(channel, ir) -= dh[ir]; + if (channel == 0 || spin_map[ir].saturated) + { + continue; + } + const ModuleBase::Vector3 spin_flux = 0.5 * (h1[ir] - h2[ir]); + double local_response = 0.0; + for (int nu = 0; nu < 3; ++nu) + { + local_response += spin_map[ir].radial.jacobian(nu, channel - 1) * (spin_flux * grad_m[nu][ir]); + } + v(channel, ir) += local_response; + } + } + + etxc += etxcgc; + } + + // vtxc uses the same completed four-component potential returned to the + // caller. This unifies the bookkeeping for both modes. + vtxc = 0.0; + for (int ir = 0; ir < nrxx; ++ir) + { + for (int is = 0; is < 4; ++is) + { + vtxc += v(is, ir) * chr->rho[is][ir]; + } + } + +#ifdef __MPI + Parallel_Reduce::reduce_pool(etxc); + Parallel_Reduce::reduce_pool(vtxc); +#endif + etxc *= omega / rhopw->nxyz; + vtxc *= omega / rhopw->nxyz; + + ModuleBase::timer::end("XC_Functional", "v_xc_ncgga_sf_builtin"); + return std::make_tuple(etxc, vtxc, std::move(v)); +} + +} // namespace NCGGA_SF_Builtin +} // namespace ModuleXC diff --git a/source/source_hamilt/module_xc/xc_functional_ncgga_sf.h b/source/source_hamilt/module_xc/xc_functional_ncgga_sf.h new file mode 100644 index 00000000000..c4d71a28ad7 --- /dev/null +++ b/source/source_hamilt/module_xc/xc_functional_ncgga_sf.h @@ -0,0 +1,37 @@ +#ifndef XC_FUNCTIONAL_NCGGA_SF_H +#define XC_FUNCTIONAL_NCGGA_SF_H + +#include "source_base/matrix.h" + +#include +#include + +class Charge; +namespace ModulePW +{ +class PW_Basis; +} + +namespace ModuleXC +{ +namespace NCGGA_SF_Builtin +{ + +// Exact discrete reverse of the regularized projected LCA graph (gga_grad=2). +std::tuple v_xc_ncgga_sf_builtin(const int& nrxx, + const double& omega, + const double tpiba, + const Charge* const chr); + +// Gradient-metric stress of the exact gga_grad=2 projected-LCA graph. The +// returned lower triangle is the unnormalised real-grid sum; Stress_Func +// applies the existing pool reduction and 1/nxyz normalisation. +void gradcorr_ncgga_lca_builtin(const Charge* const chr, + ModulePW::PW_Basis* rhopw, + const double tpiba, + std::vector& stress_gga); + +} // namespace NCGGA_SF_Builtin +} // namespace ModuleXC + +#endif diff --git a/source/source_hamilt/module_xc/xc_grad.cpp b/source/source_hamilt/module_xc/xc_grad.cpp index c7bf9a20136..b93682bfce9 100644 --- a/source/source_hamilt/module_xc/xc_grad.cpp +++ b/source/source_hamilt/module_xc/xc_grad.cpp @@ -14,11 +14,13 @@ // noncolin_rho. #include "xc_functional.h" +#include "xc_functional_ncgga_sf.h" #include "xc_grad_internal.h" #include "source_base/timer.h" #ifdef __LIBXC #include +#include "libxc_abacus.h" #endif void XC_Functional::gradcorr( @@ -33,6 +35,7 @@ void XC_Functional::gradcorr( const int nspin, const bool domag, const bool domag_z, + const int gga_grad, const double hybrid_alpha_in, const double hse_omega_in) { @@ -108,6 +111,7 @@ void XC_Functional::gradcorr( params.igcc_is_lyp = igcc_is_lyp; params.domag = domag; params.domag_z = domag_z; + params.gga_grad = gga_grad; params.hybrid_alpha = hybrid_alpha_in; params.hse_omega = hse_omega_in; params.use_libxc = use_libxc; diff --git a/source/source_hamilt/module_xc/xc_grad_internal.h b/source/source_hamilt/module_xc/xc_grad_internal.h index 8d0e54abedd..76d27832a89 100644 --- a/source/source_hamilt/module_xc/xc_grad_internal.h +++ b/source/source_hamilt/module_xc/xc_grad_internal.h @@ -41,6 +41,7 @@ struct GradCorrParams bool igcc_is_lyp; bool domag; bool domag_z; + int gga_grad; double hybrid_alpha; double hse_omega; bool use_libxc; diff --git a/source/source_hamilt/module_xc/xc_grad_prepare.cpp b/source/source_hamilt/module_xc/xc_grad_prepare.cpp index 756e7c9ff2e..b60d0d45207 100644 --- a/source/source_hamilt/module_xc/xc_grad_prepare.cpp +++ b/source/source_hamilt/module_xc/xc_grad_prepare.cpp @@ -150,7 +150,10 @@ void gradcorr_prepare_rho( } } } - XC_Functional::noncolin_rho(buf.rhotmp1.data(), buf.rhotmp2.data(), buf.neg.data(), chr->rho, rhopw->nrxx, ucell->magnet.ux_, ucell->magnet.lsign_); + // Mode 1 ignores the global quantization axis to remain continuous + // when the magnetic moments tilt away from a collinear state. + const bool use_global_axis = ucell->magnet.lsign_ && params.gga_grad != 1; + XC_Functional::noncolin_rho(buf.rhotmp1.data(), buf.rhotmp2.data(), buf.neg.data(), chr->rho, rhopw->nrxx, ucell->magnet.ux_, use_global_axis); rhopw->real2recip(buf.rhotmp1.data(), buf.rhogsum1.data()); rhopw->real2recip(buf.rhotmp2.data(), buf.rhogsum2.data()); #ifdef _OPENMP diff --git a/source/source_hamilt/module_xc/xc_pot.cpp b/source/source_hamilt/module_xc/xc_pot.cpp index 1f8a3dcd321..f8b54bde2dd 100644 --- a/source/source_hamilt/module_xc/xc_pot.cpp +++ b/source/source_hamilt/module_xc/xc_pot.cpp @@ -6,9 +6,10 @@ #include "source_base/parallel_reduce.h" #include "source_base/timer.h" -#include "source_io/module_parameter/parameter.h" #include "xc_functional.h" +#include "xc_functional_ncgga_sf.h" + #ifdef __LIBXC #include "libxc_abacus.h" #ifdef __EXX @@ -16,6 +17,7 @@ #endif #endif + // [etxc, vtxc, v] = XC_Functional::v_xc(...) std::tuple XC_Functional::v_xc( const int& nrxx, @@ -24,11 +26,13 @@ std::tuple XC_Functional::v_xc( const int nspin, const bool domag, const bool domag_z, + const int gga_grad, const double hybrid_alpha, const double hse_omega) { ModuleBase::TITLE("XC_Functional", "v_xc"); + if (use_libxc) { #ifdef __LIBXC @@ -40,6 +44,7 @@ std::tuple XC_Functional::v_xc( nspin, domag, domag_z, + gga_grad, &(scaling_factor_xc), hybrid_alpha, hse_omega); @@ -48,6 +53,12 @@ std::tuple XC_Functional::v_xc( #endif } + // Evaluate the regularized projected local-collinear graph. + if (nspin == 4 && (domag || domag_z) && gga_grad == 2) + { + return ModuleXC::NCGGA_SF_Builtin::v_xc_ncgga_sf_builtin(nrxx, ucell->omega, ucell->tpiba, chr); + } + ModuleBase::timer::start("XC_Functional", "v_xc"); //Exchange-Correlation potential Vxc(r) from n(r) @@ -184,7 +195,7 @@ std::tuple XC_Functional::v_xc( // the dummy variable dum contains gradient correction to stress // which is not used here std::vector dum; - gradcorr(etxc, vtxc, v, chr, chr->rhopw, ucell, dum, false, nspin, domag, domag_z, hybrid_alpha, hse_omega); + gradcorr(etxc, vtxc, v, chr, chr->rhopw, ucell, dum, false, nspin, domag, domag_z, gga_grad, hybrid_alpha, hse_omega); // parallel code : collect vtxc,etxc // mohan add 2008-06-01 diff --git a/source/source_io/module_hs/write_h_terms.cpp b/source/source_io/module_hs/write_h_terms.cpp index f85aa4b97a7..d3785b806df 100644 --- a/source/source_io/module_hs/write_h_terms.cpp +++ b/source/source_io/module_hs/write_h_terms.cpp @@ -350,7 +350,13 @@ void write_h_vxc(WriteHParams& params) #else const double hse_omega = 0.0; #endif - std::tie(etxc, vtxc, v_xc) = XC_Functional::v_xc(nrxx, chg, &ucell, PARAM.inp.nspin, PARAM.globalv.domag, PARAM.globalv.domag_z, hybrid_alpha, hse_omega); + std::tie(etxc, vtxc, v_xc) = XC_Functional::v_xc(nrxx, chg, &ucell, + PARAM.inp.nspin, + PARAM.globalv.domag, + PARAM.globalv.domag_z, + PARAM.inp.gga_grad, + hybrid_alpha, + hse_omega); for (int ispin = 0; ispin < nspin_out; ispin++) { diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 1637871f4d1..8a3883321ad 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -83,6 +83,7 @@ struct Input_para double nelec_delta = 0.0; ///< change in the number of total electrons double nupdown = 0.0; std::string dft_functional = "default"; ///< input DFT functional. + int gga_grad = 0; ///< Noncollinear GGA: 0 original, 1 local axis, 2 regularized projected LCA. double xc_temperature = 0.0; ///< only relevant if finite temperature functional is used double pseudo_rcut = 15.0; ///< cut-off radius for calculating msh bool pseudo_mesh = false; ///< 0: use msh to normalize radial wave functions; 1: diff --git a/source/source_io/module_parameter/read_inp_estruc.cpp b/source/source_io/module_parameter/read_inp_estruc.cpp index 6f96654199a..49c5fec46b3 100644 --- a/source/source_io/module_parameter/read_inp_estruc.cpp +++ b/source/source_io/module_parameter/read_inp_estruc.cpp @@ -488,6 +488,27 @@ The other way is only available when compiling with LIBXC, and it allows for sup }; this->add_item(item); } + { + Input_Item item("gga_grad"); + item.annotation = "Noncollinear GGA gradient method: 0 original, 1 local axis, 2 regularized projected LCA"; + item.category = "Electronic structure"; + item.type = "Integer"; + item.description = R"(Selects the local spin mapping for LDA/GGA functionals in magnetic nspin=4 calculations. +* 0: preserves the original algorithm (default). +* 1: uses the local magnetization magnitude instead of the global quantization axis in the built-in GGA gradient correction. For LIBXC functionals, 0 and 1 are equivalent. +* 2: uses a C2-regularized magnetization magnitude with eta = 1e-3 in atomic density units. The spin densities are (abs(n + rho_core) +/- min(S_eta(m), abs(n + rho_core)))/2. GGA gradients are the local-map Jacobian applied to the FFT gradients of the four density channels. The potential reverses this same discrete energy graph, including the radial Hessian and density/sigma clipping branches; the GGA stress uses the corresponding metric derivative. +For r = |m| and x = r/eta, S_eta = eta*x^3*(3*x^2 - 8*x + 6) for r < eta, and S_eta = r otherwise. The regularization is part of the functional definition, including its first and second derivatives. +Mode 2 also uses this local map for the LDA contribution. Other spin configurations retain their existing behavior.)"; + item.default_value = "0"; + read_sync_int(input.gga_grad); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.gga_grad < 0 || para.input.gga_grad > 2) + { + ModuleBase::WARNING_QUIT("ReadInput", "gga_grad must be 0, 1, or 2."); + } + }; + this->add_item(item); + } { Input_Item item("smearing_method"); item.annotation = "type of smearing_method: gauss; fd; fixed; mp; mp2; mv"; diff --git a/source/source_io/test/read_input_ptest.cpp b/source/source_io/test/read_input_ptest.cpp index 038246babdd..1d400047fa2 100644 --- a/source/source_io/test/read_input_ptest.cpp +++ b/source/source_io/test/read_input_ptest.cpp @@ -95,6 +95,8 @@ TEST_F(InputParaTest, ParaRead) EXPECT_DOUBLE_EQ(param.inp.min_dist_coef, 0.2); EXPECT_EQ(param.inp.gint_precision, "double"); EXPECT_EQ(param.inp.dft_functional, "hse"); + EXPECT_EQ(Parameter().inp.gga_grad, 0); + EXPECT_EQ(param.inp.gga_grad, 2); EXPECT_DOUBLE_EQ(param.inp.xc_temperature, 0.0); EXPECT_EQ(param.inp.nspin, 1); EXPECT_DOUBLE_EQ(param.inp.nelec, 0.0); @@ -484,6 +486,37 @@ TEST_F(InputParaTest, ParaRead) EXPECT_DOUBLE_EQ(param.inp.rdmft_power_alpha, 0.656); } +TEST_F(InputParaTest, GgaGradAcceptedRange) +{ + ModuleIO::ReadInput readinput(0); + bool found = false; + for (const auto& entry: readinput.get_input_lists()) + { + if (entry.first != "gga_grad") + { + continue; + } + found = true; + ModuleIO::Input_Item item(entry.second); + for (int mode = 0; mode <= 2; ++mode) + { + Parameter param; + item.str_values = {std::to_string(mode)}; + item.read_value(item, param); + EXPECT_EQ(param.inp.gga_grad, mode); + item.check_value(item, param); + } + for (const int mode: {-1, 3}) + { + Parameter param; + item.str_values = {std::to_string(mode)}; + item.read_value(item, param); + EXPECT_EXIT(item.check_value(item, param), testing::ExitedWithCode(1), ""); + } + } + EXPECT_TRUE(found); +} + TEST_F(InputParaTest, TypedTDFieldLists) { ModuleIO::ReadInput readinput(GlobalV::MY_RANK); diff --git a/source/source_io/test/support/INPUT b/source/source_io/test/support/INPUT index df78fb591ee..d72742cd4ee 100644 --- a/source/source_io/test/support/INPUT +++ b/source/source_io/test/support/INPUT @@ -396,3 +396,5 @@ sccut 4 #Maximal step size for lambda in eV/uB #Parameters (23. Time-dependent orbital-free DFT) of_cd 0 #0: no CD potential; 1: add CD potential of_mCD_alpha 1.0 # parameter of modified CD potential + +gga_grad 2 diff --git a/source/source_pw/module_pwdft/force_pw_cc.cpp b/source/source_pw/module_pwdft/force_pw_cc.cpp index 56575ea0925..e556a6f810b 100644 --- a/source/source_pw/module_pwdft/force_pw_cc.cpp +++ b/source/source_pw/module_pwdft/force_pw_cc.cpp @@ -34,6 +34,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, const bool* numeric, UnitCell& ucell_in) { + const Parameter& parameters = PARAM; ModuleBase::TITLE("Forces", "cal_force_cc"); // recalculate the exchange-correlation potential. ModuleBase::timer::start("Forces", "cal_force_cc"); @@ -53,7 +54,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, return; } - ModuleBase::matrix v(PARAM.inp.nspin, rho_basis->nrxx); + ModuleBase::matrix v(parameters.inp.nspin, rho_basis->nrxx); const double hybrid_alpha = XC_Functional::get_hybrid_alpha(); #ifdef __EXX @@ -66,7 +67,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, #ifdef __LIBXC const auto etxc_vtxc_v = XC_Functional_Libxc::v_xc_meta(XC_Functional::get_func_id(), rho_basis->nrxx, ucell_in.omega, ucell_in.tpiba, chr, - PARAM.inp.nspin, hybrid_alpha, hse_omega); + parameters.inp.nspin, hybrid_alpha, hse_omega); // etxc = std::get<0>(etxc_vtxc_v); // vtxc = std::get<1>(etxc_vtxc_v); @@ -77,11 +78,12 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, } else { - unitcell::cal_ux(ucell_in, PARAM.inp.nspin); + unitcell::cal_ux(ucell_in, parameters.inp.nspin); const auto etxc_vtxc_v = XC_Functional::v_xc(rho_basis->nrxx, chr, &ucell_in, - PARAM.inp.nspin, - PARAM.globalv.domag, - PARAM.globalv.domag_z, + parameters.inp.nspin, + parameters.globalv.domag, + parameters.globalv.domag_z, + parameters.inp.gga_grad, hybrid_alpha, hse_omega); @@ -92,7 +94,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, const ModuleBase::matrix vxc = v; std::complex* psiv = new std::complex[rho_basis->nmaxgr]; - if (PARAM.inp.nspin == 1 || PARAM.inp.nspin == 4) + if (parameters.inp.nspin == 1 || parameters.inp.nspin == 4) { #ifdef _OPENMP #pragma omp parallel for schedule(static, 1024) diff --git a/source/source_pw/module_pwdft/stress_cc.cpp b/source/source_pw/module_pwdft/stress_cc.cpp index ffe5dc85e09..a6778447095 100644 --- a/source/source_pw/module_pwdft/stress_cc.cpp +++ b/source/source_pw/module_pwdft/stress_cc.cpp @@ -21,12 +21,13 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, const bool *numeric, const Charge* const chr) { + const Parameter& parameters = PARAM; ModuleBase::TITLE("Stress","stress_cc"); ModuleBase::timer::start("Stress","stress_cc"); FPTYPE fact=1.0; - if(is_pw&&PARAM.globalv.gamma_only_pw) + if(is_pw&¶meters.globalv.gamma_only_pw) { fact = 2.0; //is_pw:PW basis, gamma_only need to FPTYPE. } @@ -62,7 +63,7 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, #ifdef __LIBXC const auto etxc_vtxc_v = XC_Functional_Libxc::v_xc_meta(XC_Functional::get_func_id(), rho_basis->nrxx, ucell.omega, ucell.tpiba, chr, - PARAM.inp.nspin, hybrid_alpha, hse_omega); + parameters.inp.nspin, hybrid_alpha, hse_omega); // etxc = std::get<0>(etxc_vtxc_v); // vtxc = std::get<1>(etxc_vtxc_v); @@ -73,11 +74,12 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, } else { - unitcell::cal_ux(ucell, PARAM.inp.nspin); + unitcell::cal_ux(ucell, parameters.inp.nspin); const auto etxc_vtxc_v = XC_Functional::v_xc(rho_basis->nrxx, chr, &ucell, - PARAM.inp.nspin, - PARAM.globalv.domag, - PARAM.globalv.domag_z, + parameters.inp.nspin, + parameters.globalv.domag, + parameters.globalv.domag_z, + parameters.inp.gga_grad, hybrid_alpha, hse_omega); // etxc = std::get<0>(etxc_vtxc_v); // may delete? @@ -87,7 +89,7 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, std::complex* psic = new std::complex[rho_basis->nmaxgr]; - if(PARAM.inp.nspin==1||PARAM.inp.nspin==4) + if(parameters.inp.nspin==1||parameters.inp.nspin==4) { #ifdef _OPENMP #pragma omp parallel for schedule(static, 1024) diff --git a/source/source_pw/module_pwdft/stress_gga.cpp b/source/source_pw/module_pwdft/stress_gga.cpp index bb175514803..dc9eaf7e9d2 100644 --- a/source/source_pw/module_pwdft/stress_gga.cpp +++ b/source/source_pw/module_pwdft/stress_gga.cpp @@ -10,6 +10,7 @@ void Stress_Func::stress_gga(const UnitCell& ucell, ModulePW::PW_Basis* rho_basis, const Charge* const chr) { + const Parameter& parameters = PARAM; ModuleBase::TITLE("Stress","stress_gga"); ModuleBase::timer::start("Stress","stress_gga"); @@ -31,7 +32,7 @@ void Stress_Func::stress_gga(const UnitCell& ucell, XC_Functional::gradcorr( dum1, dum2, dum3, chr, rho_basis, &ucell, stress_gga, is_stress, - PARAM.inp.nspin, PARAM.globalv.domag, PARAM.globalv.domag_z, + parameters.inp.nspin, parameters.globalv.domag, parameters.globalv.domag_z, parameters.inp.gga_grad, hybrid_alpha, hse_omega); for(int l = 0;l< 3;l++) From 758c8711ac9e5a5d70d2a50b7bdfa8f608ad7ca6 Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Mon, 7 Sep 2026 18:42:23 +0800 Subject: [PATCH 09/14] fix(xc): close gga_grad 2 builtin and LibXC stress derivatives Based-on: https://github.com/deepmodeling/abacus-develop/pull/7758 Co-authored-by: dyzheng --- source/source_hamilt/module_xc/libxc_pot.cpp | 164 ++++++++++++++++++ .../test/test_xc_functional_ncgga_sf.cpp | 130 ++++++++++++++ .../module_xc/xc_functional_ncgga_sf.cpp | 108 ++++++++++++ source/source_hamilt/module_xc/xc_grad.cpp | 26 +++ 4 files changed, 428 insertions(+) diff --git a/source/source_hamilt/module_xc/libxc_pot.cpp b/source/source_hamilt/module_xc/libxc_pot.cpp index 9955d7626b9..b39cb85115b 100644 --- a/source/source_hamilt/module_xc/libxc_pot.cpp +++ b/source/source_hamilt/module_xc/libxc_pot.cpp @@ -16,6 +16,170 @@ #include #include +void XC_Functional_Libxc::gradcorr_ncgga_sf_libxc(const std::vector& func_id, + const std::size_t nrxx, + const double tpiba, + const Charge* const chr, + const std::map* scaling_factor, + const double hybrid_alpha, + const double hse_omega, + std::vector& stress_gga) +{ + constexpr int nspin = 2; + stress_gga.assign(9, 0.0); + + std::vector funcs = XC_Functional_Libxc::init_func(func_id, XC_POLARIZED, hybrid_alpha, hse_omega); + bool has_gga = false; + for (const xc_func_type& func: funcs) + { + has_gga = has_gga || func.info->family == XC_FAMILY_GGA || func.info->family == XC_FAMILY_HYB_GGA; + } + if (!has_gga) + { + XC_Functional_Libxc::finish_func(funcs); + return; + } + + // This is the same forward graph used by v_xc_libxc: the local spin map, + // its projected FFT gradients, and the sigma invariants are constructed + // once and shared by all Libxc components. + const XC_Functional_Libxc::NclSfDiscreteData sf_data + = XC_Functional_Libxc::make_ncl_sf_discrete_data(nrxx, tpiba, chr, true); + const std::vector& rho = sf_data.rho; + const std::vector sigma = XC_Functional_Libxc::convert_sigma(sf_data.spin_gradient); + std::vector aggregate_dsigma(3 * nrxx, 0.0); + + for (xc_func_type& func: funcs) + { + if (func.info->family != XC_FAMILY_GGA && func.info->family != XC_FAMILY_HYB_GGA) + { + continue; + } + + constexpr double rho_threshold = 1.0e-6; + constexpr double grho_threshold = 1.0e-10; + xc_func_set_dens_threshold(&func, rho_threshold); + const std::vector sgn + = XC_Functional_Libxc::cal_sgn(rho_threshold, grho_threshold, func, nspin, nrxx, rho, sigma); + std::vector exc(nrxx); + std::vector vrho(nspin * nrxx); + std::vector vsigma(3 * nrxx); + constexpr int nr_batch_size = 1024; +#ifdef _OPENMP +#pragma omp parallel for schedule(static, nr_batch_size) +#endif + for (int ir_start = 0; ir_start < static_cast(nrxx); ir_start += nr_batch_size) + { + const int ir_end = std::min(ir_start + nr_batch_size, static_cast(nrxx)); + const int nrxx_thread = ir_end - ir_start; + xc_gga_exc_vxc(&func, + nrxx_thread, + rho.data() + ir_start * nspin, + sigma.data() + ir_start * 3, + exc.data() + ir_start, + vrho.data() + ir_start * nspin, + vsigma.data() + ir_start * 3); + } + + double factor = 1.0; + if (scaling_factor != nullptr) + { + const std::map::const_iterator entry = scaling_factor->find(func.info->number); + if (entry != scaling_factor->end()) + { + factor = entry->second; + } + } + const XC_Functional_Libxc::LibxcWeightedDerivatives weighted + = XC_Functional_Libxc::make_libxc_weighted_derivatives(func, + nspin, + nrxx, + sgn, + rho, + sigma, + exc, + vrho, + vsigma); + for (std::size_t index = 0; index < aggregate_dsigma.size(); ++index) + { + aggregate_dsigma[index] += factor * weighted.dsigma[index]; + } + } + +// For g_s=sum_A J_sA G_h x_A, a reciprocal deformation changes G_h but +// not the pointwise map J. Therefore the metric derivative is exactly +// sum_s h_s,l g_s,m, with h_s=dE/dg_s built from the sanitizer-reversed, +// component-scaled aggregate above. +#ifdef _OPENMP +#pragma omp parallel + { + std::vector local_stress(9, 0.0); +#pragma omp for schedule(static, 512) + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + const std::size_t sigma_index = 3 * ir; + const ModuleBase::Vector3& grad_up = sf_data.spin_gradient[0][ir]; + const ModuleBase::Vector3& grad_down = sf_data.spin_gradient[1][ir]; + const ModuleBase::Vector3 h_up + = ModuleBase::e2 + * (2.0 * aggregate_dsigma[sigma_index] * grad_up + aggregate_dsigma[sigma_index + 1] * grad_down); + const ModuleBase::Vector3 h_down + = ModuleBase::e2 + * (2.0 * aggregate_dsigma[sigma_index + 2] * grad_down + aggregate_dsigma[sigma_index + 1] * grad_up); + const double grad_up_component[3] = {grad_up.x, grad_up.y, grad_up.z}; + const double grad_down_component[3] = {grad_down.x, grad_down.y, grad_down.z}; + const double h_up_component[3] = {h_up.x, h_up.y, h_up.z}; + const double h_down_component[3] = {h_down.x, h_down.y, h_down.z}; + for (int l = 0; l < 3; ++l) + { + for (int m = 0; m <= l; ++m) + { + local_stress[l * 3 + m] + += h_up_component[l] * grad_up_component[m] + h_down_component[l] * grad_down_component[m]; + } + } + } +#pragma omp critical(libxc_ncgga_stress_reduce) + { + for (int l = 0; l < 3; ++l) + { + for (int m = 0; m <= l; ++m) + { + stress_gga[l * 3 + m] += local_stress[l * 3 + m]; + } + } + } + } +#else + for (std::size_t ir = 0; ir < nrxx; ++ir) + { + const std::size_t sigma_index = 3 * ir; + const ModuleBase::Vector3& grad_up = sf_data.spin_gradient[0][ir]; + const ModuleBase::Vector3& grad_down = sf_data.spin_gradient[1][ir]; + const ModuleBase::Vector3 h_up + = ModuleBase::e2 + * (2.0 * aggregate_dsigma[sigma_index] * grad_up + aggregate_dsigma[sigma_index + 1] * grad_down); + const ModuleBase::Vector3 h_down + = ModuleBase::e2 + * (2.0 * aggregate_dsigma[sigma_index + 2] * grad_down + aggregate_dsigma[sigma_index + 1] * grad_up); + const double grad_up_component[3] = {grad_up.x, grad_up.y, grad_up.z}; + const double grad_down_component[3] = {grad_down.x, grad_down.y, grad_down.z}; + const double h_up_component[3] = {h_up.x, h_up.y, h_up.z}; + const double h_down_component[3] = {h_down.x, h_down.y, h_down.z}; + for (int l = 0; l < 3; ++l) + { + for (int m = 0; m <= l; ++m) + { + stress_gga[l * 3 + m] + += h_up_component[l] * grad_up_component[m] + h_down_component[l] * grad_down_component[m]; + } + } + } +#endif + + XC_Functional_Libxc::finish_func(funcs); +} + std::tuple XC_Functional_Libxc::v_xc_libxc( // Peize Lin update for nspin==4 at // 2023.01.14 const std::vector& func_id, diff --git a/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp b/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp index 0f5d6b92f47..120dd99ee01 100644 --- a/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp +++ b/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp @@ -1491,13 +1491,92 @@ TEST_F(RealPwNcgga, LibxcGgaGrad2DifferentiatesMixedLdaAndGgaComponents) expect_vtxc_matches_returned_potential("libxc_gga2_mixed_lda_gga", evaluate); } +TEST_F(RealPwNcgga, LibxcGgaGrad2StressProductionDispatchClosesSmoothMetricDerivative) +{ + const BranchMargins margins = report_branch_margins("libxc_stress_smooth"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + EXPECT_GT(margins.min_eta_distance, 0.3); + // The analytic tensor must come through the existing public production + // dispatch. Keeping the new helper out of the test-only patch lets the + // matched parent build and fail on the numerical identity, not at link + // time because the child-only helper does not exist yet. + expect_gradient_stress_metric_derivative( + "libxc_smooth_dispatch", + [this]() { return evaluate_libxc_gga(); }, + [this]() { return evaluate_libxc_pbe_gradient_stress_dispatch(); }, + 3.0e-4, + 1.0e-11); +} +TEST_F(RealPwNcgga, LibxcGgaGrad2StressProductionDispatchClosesLocalMapBranches) +{ + const Evaluator energy = [this]() { return evaluate_libxc_gga(); }; + const StressEvaluator stress = [this]() { return evaluate_libxc_pbe_gradient_stress_dispatch(); }; + set_negative_gga_state(); + const BranchMargins negative = report_branch_margins("libxc_stress_negative_abs"); + EXPECT_GT(negative.min_abs_total_density, 1.3); + EXPECT_GT(negative.min_signed_saturation_gap, 0.8); + expect_gradient_stress_metric_derivative("libxc_negative_abs_dispatch", energy, stress, 3.0e-4, 1.0e-11); + set_saturated_gga_state(); + const BranchMargins saturated = report_branch_margins("libxc_stress_saturated"); + EXPECT_LT(saturated.max_signed_saturation_gap, -0.1); + expect_gradient_stress_metric_derivative("libxc_saturated_dispatch", energy, stress, 3.0e-4, 1.0e-11); + set_inside_eta_state(); + const BranchMargins radial = report_branch_margins("libxc_stress_inside_eta"); + EXPECT_LT(radial.max_magnitude, 6.0e-4); + EXPECT_GT(radial.min_eta_distance, 4.0e-4); + expect_gradient_stress_metric_derivative("libxc_inside_eta_dispatch", energy, stress, 3.0e-4, 1.0e-11); +} +TEST_F(RealPwNcgga, LibxcGgaGrad2StressAggregatesPublicFunctionalScaling) +{ + const std::vector gga = {XC_GGA_X_ITYH, XC_GGA_C_LYPR, XC_GGA_X_B88, XC_GGA_C_LYP}; + const std::map scaling + = {{XC_GGA_X_ITYH, -1.0}, {XC_GGA_C_LYPR, -1.0}, {XC_GGA_X_B88, 1.0}, {XC_GGA_C_LYP, 1.0}}; + const std::vector exchange_short = evaluate_gradient_stress_dispatch("GGA_X_ITYH"); + const std::vector correlation_short = evaluate_gradient_stress_dispatch("GGA_C_LYPR"); + const std::vector exchange_full = evaluate_gradient_stress_dispatch("GGA_X_B88"); + const std::vector correlation_full = evaluate_gradient_stress_dispatch("GGA_C_LYP"); + const std::vector scaled = evaluate_gradient_stress_dispatch("BLYP_LR"); + ASSERT_EQ(exchange_short.size(), 9U); + ASSERT_EQ(correlation_short.size(), 9U); + ASSERT_EQ(exchange_full.size(), 9U); + ASSERT_EQ(correlation_full.size(), 9U); + ASSERT_EQ(scaled.size(), 9U); + for (int row = 0; row < 3; ++row) + { + for (int column = 0; column <= row; ++column) + { + const int index = row * 3 + column; + const double expected + = -exchange_short[index] - correlation_short[index] + exchange_full[index] + correlation_full[index]; + EXPECT_NEAR(scaled[index], expected, 3.0e-12 * std::max(1.0, std::abs(expected))); + } + } + expect_gradient_stress_metric_derivative( + "libxc_blyp_lr_dispatch", + [this, &gga, &scaling]() { return evaluate_libxc(gga, &scaling); }, + [this]() { return evaluate_gradient_stress_dispatch("BLYP_LR"); }, + 3.0e-4, + 1.0e-11); +} +TEST_F(RealPwNcgga, LibxcGgaGrad2StressProductionDispatchClosesFullXcDiagonalWithoutCore) +{ + set_zero_core_density(); + const BranchMargins margins = report_branch_margins("libxc_full_xc_no_core"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + expect_full_xc_diagonal_stress( + "libxc_full_xc_dispatch_no_core", + [this]() { return evaluate_libxc_gga(); }, + [this]() { return evaluate_libxc_pbe_gradient_stress_dispatch(); }); +} #endif TEST_F(RealPwNcgga, BuiltinGgaGrad2VtxcEqualsFinalValencePotentialInnerProduct) @@ -1653,13 +1732,64 @@ TEST_F(RealPwNcgga, BuiltinGgaGrad2IsCovariantUnderGlobalSpinRotation) } } +TEST_F(RealPwNcgga, BuiltinGgaGrad2StressClosesSmoothSixComponentMetricDerivative) +{ + const BranchMargins margins = report_branch_margins("stress_smooth"); + EXPECT_GT(margins.min_abs_total_density, 1.0); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + EXPECT_GT(margins.min_eta_distance, 0.3); + expect_builtin_gradient_stress_metric_derivative("smooth"); + set_zero_core_density(); + const BranchMargins zero_core = report_branch_margins("stress_smooth_zero_core"); + EXPECT_GT(zero_core.min_abs_total_density, 1.0); + EXPECT_GT(zero_core.min_signed_saturation_gap, 0.8); + expect_builtin_full_xc_diagonal_stress("smooth"); +} +TEST_F(RealPwNcgga, BuiltinGgaGrad2StressClosesNegativeAbsSixComponentMetricDerivative) +{ + set_negative_gga_state(); + const BranchMargins margins = report_branch_margins("stress_negative_abs"); + EXPECT_GT(margins.min_abs_total_density, 1.3); + EXPECT_GT(margins.min_signed_saturation_gap, 0.8); + expect_builtin_gradient_stress_metric_derivative("negative_abs"); + set_zero_core_density(); + const BranchMargins zero_core = report_branch_margins("stress_negative_abs_zero_core"); + EXPECT_GT(zero_core.min_abs_total_density, 1.3); + EXPECT_GT(zero_core.min_signed_saturation_gap, 0.8); + expect_builtin_full_xc_diagonal_stress("negative_abs"); +} +TEST_F(RealPwNcgga, BuiltinGgaGrad2StressClosesSaturatedSixComponentMetricDerivative) +{ + set_saturated_gga_state(); + const BranchMargins margins = report_branch_margins("stress_saturated"); + EXPECT_LT(margins.max_signed_saturation_gap, -0.1); + expect_builtin_gradient_stress_metric_derivative("saturated"); + set_zero_core_density(); + const BranchMargins zero_core = report_branch_margins("stress_saturated_zero_core"); + EXPECT_LT(zero_core.max_signed_saturation_gap, -0.1); + expect_builtin_full_xc_diagonal_stress("saturated"); +} +TEST_F(RealPwNcgga, BuiltinGgaGrad2StressClosesRadialEtaSixComponentMetricDerivative) +{ + set_inside_eta_state(); + const BranchMargins margins = report_branch_margins("stress_inside_eta"); + EXPECT_GT(margins.min_abs_total_density, 0.025); + EXPECT_LT(margins.max_magnitude, 6.0e-4); + EXPECT_GT(margins.min_eta_distance, 4.0e-4); + expect_builtin_gradient_stress_metric_derivative("inside_eta"); + set_zero_core_density(); + const BranchMargins zero_core = report_branch_margins("stress_inside_eta_zero_core"); + EXPECT_GT(zero_core.min_abs_total_density, 0.025); + EXPECT_LT(zero_core.max_magnitude, 6.0e-4); + expect_builtin_full_xc_diagonal_stress("inside_eta"); +} } // namespace diff --git a/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp b/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp index cb4e31eba5f..92808791ca9 100644 --- a/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp +++ b/source/source_hamilt/module_xc/xc_functional_ncgga_sf.cpp @@ -235,5 +235,113 @@ std::tuple v_xc_ncgga_sf_builtin(const int& return std::make_tuple(etxc, vtxc, std::move(v)); } +void gradcorr_ncgga_lca_builtin(const Charge* const chr, + ModulePW::PW_Basis* rhopw, + const double tpiba, + std::vector& stress_gga) +{ + stress_gga.assign(9, 0.0); + + const int nrxx = rhopw->nrxx; + const int npw = rhopw->npw; + const double e2 = ModuleBase::e2; + constexpr double epsr = 1.0e-6; + + // Rebuild the same complete local map used by the gga_grad=2 energy: + // rho_s = N_s(n + rho_core, m), + // g_s = sum_A dN_s/dx_A G_h x_A. + // Metric differentiation keeps the real-grid values x_A fixed, so the + // map Jacobian is unchanged and every G_h x_A transforms covariantly. + std::vector spin_map(nrxx); + std::array>, 4> field_gradient; + for (int channel = 0; channel < 4; ++channel) + { + field_gradient[channel].resize(nrxx); + } + std::array>, 2> spin_gradient; + for (int spin = 0; spin < 2; ++spin) + { + spin_gradient[spin].resize(nrxx); + } + + std::vector> reciprocal(npw); + rhopw->real2recip(chr->rho[0], reciprocal.data()); + for (int ig = 0; ig < npw; ++ig) + { + reciprocal[ig] += chr->rhog_core[ig]; + } + XC_Functional::grad_rho(reciprocal.data(), field_gradient[0].data(), rhopw, tpiba); + + for (int channel = 1; channel < 4; ++channel) + { + rhopw->real2recip(chr->rho[channel], reciprocal.data()); + XC_Functional::grad_rho(reciprocal.data(), field_gradient[channel].data(), rhopw, tpiba); + } + + for (int ir = 0; ir < nrxx; ++ir) + { + const std::array magnetization = {{chr->rho[1][ir], chr->rho[2][ir], chr->rho[3][ir]}}; + spin_map[ir] = make_ncgga_spin_map_point(chr->rho[0][ir] + chr->rho_core[ir], + make_ncgga_radial_point(magnetization, ncgga_lca_radial_eta())); + for (int spin = 0; spin < 2; ++spin) + { + for (int channel = 0; channel < 4; ++channel) + { + spin_gradient[spin][ir] += spin_map[ir].jacobian(spin, channel) * field_gradient[channel][ir]; + } + } + } + + for (int ir = 0; ir < nrxx; ++ir) + { + const double rho_up = spin_map[ir].spin_density[0]; + const double rho_down = spin_map[ir].spin_density[1]; + const ModuleBase::Vector3& grad_up = spin_gradient[0][ir]; + const ModuleBase::Vector3& grad_down = spin_gradient[1][ir]; + + double sx = 0.0; + double v1xup = 0.0; + double v1xdw = 0.0; + double v2xup = 0.0; + double v2xdw = 0.0; + XC_Functional::gcx_spin(rho_up, + rho_down, + grad_up * grad_up, + grad_down * grad_down, + sx, + v1xup, + v1xdw, + v2xup, + v2xdw); + + double sc = 0.0; + double v1cup = 0.0; + double v1cdw = 0.0; + double v2c = 0.0; + const double rho = rho_up + rho_down; + if (rho > epsr) + { + double zeta = std::fabs((rho_up - rho_down) / rho); + const ModuleBase::Vector3 grad_rho = grad_up + grad_down; + XC_Functional::gcc_spin(rho, zeta, grad_rho * grad_rho, sc, v1cup, v1cdw, v2c); + } + + const ModuleBase::Vector3 h_up = e2 * ((v2xup + v2c) * grad_up + v2c * grad_down); + const ModuleBase::Vector3 h_down = e2 * ((v2xdw + v2c) * grad_down + v2c * grad_up); + const double grad_up_component[3] = {grad_up.x, grad_up.y, grad_up.z}; + const double grad_down_component[3] = {grad_down.x, grad_down.y, grad_down.z}; + const double h_up_component[3] = {h_up.x, h_up.y, h_up.z}; + const double h_down_component[3] = {h_down.x, h_down.y, h_down.z}; + for (int row = 0; row < 3; ++row) + { + for (int column = 0; column <= row; ++column) + { + stress_gga[row * 3 + column] += h_up_component[row] * grad_up_component[column] + + h_down_component[row] * grad_down_component[column]; + } + } + } +} + } // namespace NCGGA_SF_Builtin } // namespace ModuleXC diff --git a/source/source_hamilt/module_xc/xc_grad.cpp b/source/source_hamilt/module_xc/xc_grad.cpp index b93682bfce9..2df043824a8 100644 --- a/source/source_hamilt/module_xc/xc_grad.cpp +++ b/source/source_hamilt/module_xc/xc_grad.cpp @@ -66,6 +66,32 @@ void XC_Functional::gradcorr( return; } + if (is_stress && !use_libxc && nspin == 4 && (domag || domag_z) && gga_grad == 2) + { + ModuleXC::NCGGA_SF_Builtin::gradcorr_ncgga_lca_builtin( + chr, rhopw, ucell->tpiba, stress_gga); + return; + } + + + +#ifdef __LIBXC + if (is_stress && use_libxc && nspin == 4 && (domag || domag_z) + && gga_grad == 2) + { + XC_Functional_Libxc::gradcorr_ncgga_sf_libxc( + func_id, + rhopw->nrxx, + ucell->tpiba, + chr, + &scaling_factor_xc, + hybrid_alpha_in, + hse_omega_in, + stress_gga); + return; + } +#endif + bool igcc_is_lyp = false; // func_id may hold a single entry (e.g. PBE0 -> {XC_HYB_GGA_XC_PBEH}), so guard the index. if( func_id.size() > 1 && func_id[1] == XC_GGA_C_LYP) From b57a3d3f63071b59afbea0fc6a0fd912cd93818c Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Tue, 8 Sep 2026 15:09:06 +0800 Subject: [PATCH 10/14] fix(exx): average irreducible densities over the little group Project multi-k density matrices before star restoration to preserve the symmetry assumed by reduced EXX contractions. Add complex-density and spin-channel regressions and update the Si HSE reference after independent solver and full-contraction checks. (cherry picked from commit b0441a98e53cc75495b421aed51d262f95e9a37d) --- docs/advanced/input_files/input-main.md | 1 + docs/parameters.yaml | 1 + .../module_exx_symmetry/symm_rotation.cpp | 80 ++++++--- .../module_exx_symmetry/symm_rotation.h | 15 ++ .../module_exx_symmetry/test/CMakeLists.txt | 2 +- .../test/test_symm_rotation.cpp | 161 ++++++++++++++++++ tests/08_EXX/08_KP_HSE_symm/README | 31 +++- tests/08_EXX/08_KP_HSE_symm/result.ref | 6 +- 8 files changed, 266 insertions(+), 31 deletions(-) create mode 100644 source/source_lcao/module_ri/module_exx_symmetry/test/test_symm_rotation.cpp diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index de4e12456e4..111948fff5d 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -3396,6 +3396,7 @@ - **Availability**: *[`symmetry`](#symmetry)==1 and ([`dft_functional`](#dft_functional) in [hse, hf, pbe0, scan0] or ([`basis_type`](#basis_type)==lcao and [`rpa`](#rpa)==true))* - **Description**: - False: only rotate k-space density matrix D(k) from irreducible k-points to accelerate diagonalization - True: rotate both D(k) and Hexx(R) to accelerate both diagonalization and EXX calculation + - For multi-k calculations, D(k) is averaged over the unitary little group of each irreducible k point before star expansion, for either setting. - **Default**: True ### out_ri_cv diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 923d59b6308..b39bafbd5a1 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -4660,6 +4660,7 @@ parameters: description: | * False: only rotate k-space density matrix D(k) from irreducible k-points to accelerate diagonalization * True: rotate both D(k) and Hexx(R) to accelerate both diagonalization and EXX calculation + For multi-k calculations, D(k) is averaged over the unitary little group of each irreducible k point before star expansion, for either setting. default_value: "True" unit: "" availability: "symmetry==1 and (dft_functional in [hse, hf, pbe0, scan0] or (basis_type==lcao and rpa==true))" diff --git a/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.cpp b/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.cpp index 18eb826888c..a1942918f70 100644 --- a/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.cpp +++ b/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.cpp @@ -52,28 +52,37 @@ namespace ModuleSymmetry } this->spin_U_ = spin_U; // keep for restore_HR_nspin4 (real-space EXX H(R) spin mixing) - // 2. calculate the rotation matrix in AO-representation for each ibz_kpoint and symmetry operation: M(k, isym) - auto restrict_kpt = [](const TCdouble& kvec, const double& symm_prec) -> TCdouble - {// in (-0.5, 0.5] - TCdouble kvec_res; - kvec_res.x = fmod(kvec.x + 100.5 - 0.5 * symm_prec, 1) - 0.5 + 0.5 * symm_prec; - kvec_res.y = fmod(kvec.y + 100.5 - 0.5 * symm_prec, 1) - 0.5 + 0.5 * symm_prec; - kvec_res.z = fmod(kvec.z + 100.5 - 0.5 * symm_prec, 1) - 0.5 + 0.5 * symm_prec; - if (std::abs(kvec_res.x) < symm_prec) { kvec_res.x = 0.0; } - if (std::abs(kvec_res.y) < symm_prec) { kvec_res.y = 0.0; } - if (std::abs(kvec_res.z) < symm_prec) { kvec_res.z = 0.0; } - return kvec_res; - }; - int nks_ibz = kv.kstars.size(); // kv.nks = 2 * kv.nks_ibz when nspin=2 - this->Ms_.resize(nks_ibz); - for (int ik_ibz = 0;ik_ibz < nks_ibz;++ik_ibz) + // A k-star contains only one operation per distinct k point. The other + // operations fixing k (modulo a reciprocal lattice vector) must still + // be averaged: a finite-grid SCF density need not respect this little group. + const int nks_ibz = kv.kstars.size(); + this->Ms_.assign(nks_ibz, {}); + this->little_groups_.assign(nks_ibz, {}); + for (int ik_ibz = 0; ik_ibz < nks_ibz; ++ik_ibz) { - // const TCdouble& kvec_d_ibz = restrict_kpt((*kstars[ik_ibz].begin()).second * ucell.symm.kgmatrix[(*kstars[ik_ibz].begin()).first], ucell.symm.epsilon); - for (auto& isym_kvd : kv.kstars[ik_ibz]) { - if (isym_kvd.first < nop_tot) { - this->Ms_[ik_ibz][isym_kvd.first] = this->contruct_2d_rot_mat_ao(ucell.symm, ucell.atoms, ucell.st, kv.kvec_d[ik_ibz], isym_kvd.first, pv, spin_U[isym_kvd.first]); -} -} + std::set needed; + for (const auto& member : kv.kstars[ik_ibz]) + { + const int op = (!this->magnetic_nspin4_ && member.first >= nsym_) + ? member.first - nsym_ : member.first; + needed.insert(op); + } + for (int op = 0; op < nsym_; ++op) + { + const auto delta = kv.kvec_d[ik_ibz] * ucell.symm.kgmatrix[op] - kv.kvec_d[ik_ibz]; + if (std::abs(delta.x - std::round(delta.x)) < this->eps_ + && std::abs(delta.y - std::round(delta.y)) < this->eps_ + && std::abs(delta.z - std::round(delta.z)) < this->eps_) + { + this->little_groups_[ik_ibz].push_back(op); + needed.insert(op); + } + } + for (const int op : needed) + { + this->Ms_[ik_ibz][op] = this->contruct_2d_rot_mat_ao( + ucell.symm, ucell.atoms, ucell.st, kv.kvec_d[ik_ibz], op, pv, spin_U[op]); + } } // output Ms of isym=1 // std::ofstream ofs("Ms_kibz7_sym7.dat"); @@ -109,18 +118,37 @@ namespace ModuleSymmetry { for (int ik_ibz = 0;ik_ibz < nk;++ik_ibz) { + // P_k D = |G_k|^{-1} sum_g M_g^T D M_g^*. This preserves + // Hermiticity and makes restoration independent of the chosen + // star representative; rotating just one arbitrary D does not. + const auto& little_group = this->little_groups_.at(ik_ibz); + assert(!little_group.empty()); + std::vector> projected = dm_k_ibz[ik_ibz + is * nk]; + if (little_group.size() > 1) + { + std::fill(projected.begin(), projected.end(), 0.0); + for (const int op : little_group) + { + const auto rotated = this->rot_matrix_ao( + dm_k_ibz[ik_ibz + is * nk], ik_ibz, little_group.size(), op, pv); + for (size_t i = 0; i < projected.size(); ++i) + { + projected[i] += rotated[i]; + } + } + } for (auto& isym_kvd : kv.kstars[ik_ibz]) { if (isym_kvd.first == 0) { double factor = 1.0 / static_cast(kv.kstars[ik_ibz].size()); std::vector> dm_scaled(pv.get_local_size()); - for (int i = 0;i < pv.get_local_size();++i) { dm_scaled[i] = factor * dm_k_ibz[ik_ibz + is * nk][i]; } + for (int i = 0;i < pv.get_local_size();++i) { dm_scaled[i] = factor * projected[i]; } dm_k_full.push_back(dm_scaled); } else if (isym_kvd.first < nsym_) { //space group operations - dm_k_full.push_back(this->rot_matrix_ao(dm_k_ibz[ik_ibz + is * nk], ik_ibz, kv.kstars[ik_ibz].size(), isym_kvd.first, pv)); + dm_k_full.push_back(this->rot_matrix_ao(projected, ik_ibz, kv.kstars[ik_ibz].size(), isym_kvd.first, pv)); } else { // antiunitary elements: Theta * (spatial operation) @@ -140,12 +168,12 @@ namespace ModuleSymmetry // m=0: gray group: the space-group part of anti-unitary elements are the same of the unitary elements, isym_M < nsym_ // m!=0: Shubnikov group: using different space-group part of anti-unitary elements stored in gmatrix_anti with isym_M >= nsym_ dm_k_full.push_back(this->trs_spin_rotate( - this->rot_matrix_ao(dm_k_ibz[ik_ibz + is * nk], ik_ibz, kv.kstars[ik_ibz].size(), isym_M, pv, false), + this->rot_matrix_ao(projected, ik_ibz, kv.kstars[ik_ibz].size(), isym_M, pv, false), sigma_y, pv, 1.0)); } else { - dm_k_full.push_back(this->rot_matrix_ao(dm_k_ibz[ik_ibz + is * nk], ik_ibz, kv.kstars[ik_ibz].size(), isym_M, pv, true)); + dm_k_full.push_back(this->rot_matrix_ao(projected, ik_ibz, kv.kstars[ik_ibz].size(), isym_M, pv, true)); } } } @@ -492,7 +520,7 @@ namespace ModuleSymmetry const char notrans = 'N'; std::complex alpha(1.0, 0.0); const std::complex beta(0.0, 0.0); - const int nbasis = PARAM.globalv.nlocal; + const int nbasis = pv.get_global_row_size(); const int i1 = 1; if (TRS_conj) { diff --git a/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.h b/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.h index 3ff91c7523e..1d27edfef2f 100644 --- a/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.h +++ b/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation.h @@ -74,6 +74,17 @@ namespace ModuleSymmetry std::vector> trs_spin_rotate(const std::vector>& X, const std::vector>& sigma_y, const Parallel_2D& pv, const double scale) const; + /// Inject synthetic AO rotations for density-restoration regression tests. + void set_density_rotations_for_testing( + const std::vector>>>& rotations, + const std::vector>& little_groups, + const int nrot) + { + this->Ms_ = rotations; + this->little_groups_ = little_groups; + this->nsym_ = nrot; + } + /// calculate Wigner D matrix double wigner_d(const double beta, const int l, const int m1, const int m2) const; std::complex wigner_D(const TCdouble& euler_angle, const int l, const int m1, const int m2, const bool inv) const; @@ -209,6 +220,10 @@ namespace ModuleSymmetry /// size: [nks_ibz][nsym][nbasis*nbasis], only need to calculate once. std::vector>>> Ms_; + /// Unitary operations fixing each IBZ k point modulo reciprocal lattice vectors. + /// Geometry data built with Ms_ in cal_Ms, not an SCF workflow switch. + std::vector> little_groups_; + /// (nspin=4) the SU(2) spin-1/2 rotation U(isym) for each symmetry operation, size [nsym]. /// The spinor AO rotation is T(isym) (x) U(isym); restore_HR_nspin4 uses it to mix the 4 spin /// channels of the real-space EXX H(R). Filled in cal_Ms (identity for nspin<4). diff --git a/source/source_lcao/module_ri/module_exx_symmetry/test/CMakeLists.txt b/source/source_lcao/module_ri/module_exx_symmetry/test/CMakeLists.txt index cf9ad10d1af..ba45e581124 100644 --- a/source/source_lcao/module_ri/module_exx_symmetry/test/CMakeLists.txt +++ b/source/source_lcao/module_ri/module_exx_symmetry/test/CMakeLists.txt @@ -4,7 +4,7 @@ abacus_disable_feature_definitions(__ROCM) AddTest( TARGET MODULE_RI_EXX_SYMMETRY_rotation LIBS base device symmetry neighbor parameter - SOURCES symm_rotation_test.cpp ../symm_rotation.cpp ../symm_rot_out.cpp ../irreducible_sector.cpp ../irred_sec_bvk.cpp + SOURCES symm_rotation_test.cpp test_symm_rotation.cpp ../symm_rotation.cpp ../symm_rot_out.cpp ../irreducible_sector.cpp ../irred_sec_bvk.cpp ../../../../source_basis/module_ao/parallel_orbitals.cpp ) \ No newline at end of file diff --git a/source/source_lcao/module_ri/module_exx_symmetry/test/test_symm_rotation.cpp b/source/source_lcao/module_ri/module_exx_symmetry/test/test_symm_rotation.cpp new file mode 100644 index 00000000000..01ec4938bfb --- /dev/null +++ b/source/source_lcao/module_ri/module_exx_symmetry/test/test_symm_rotation.cpp @@ -0,0 +1,161 @@ +#include "../symm_rotation.h" +#include "source_io/module_parameter/parameter.h" +#include "gtest/gtest.h" + +class TestParameters +{ + public: + TestParameters(Parameter& parameters, const int nspin) + : parameters_(parameters), original_nspin_(parameters.inp.nspin) + { + parameters_.input.nspin = nspin; + } + ~TestParameters() { parameters_.input.nspin = original_nspin_; } + + private: + Parameter& parameters_; + const int original_nspin_; +}; + +// K-point generation is outside this test: use explicit stars, but provide +// the virtual symbols needed by the existing lightweight rotation test target. +void ModuleCell::ReciprocalGrid::renew(const int&) +{ + ADD_FAILURE() << "Unexpected k-point generation"; +} +void K_Vectors::renew(const int&) +{ + ADD_FAILURE() << "Unexpected k-point generation"; +} +void K_Vectors::reduce_by_symmetry(const UnitCell&, const ModuleSymmetry::Symmetry&, + bool, std::string&, bool&, const int, std::ofstream&) +{ + ADD_FAILURE() << "Unexpected k-point reduction"; +} + +namespace +{ +using Complex = std::complex; + +// Independent dense product in the stored (transposed density) convention. +std::vector rotate_reference(const std::vector& density, + const std::vector& rotation, + const int n) +{ + std::vector result(n * n, 0.0); + for (int i = 0; i < n; ++i) + { + for (int j = 0; j < n; ++j) + { + for (int a = 0; a < n; ++a) + { + for (int b = 0; b < n; ++b) + { + result[i + j * n] += rotation[a + i * n] * density[a + b * n] + * std::conj(rotation[b + j * n]); + } + } + } + } + return result; +} + +void check_little_group_restoration(const int nspin) +{ + const TestParameters parameters(PARAM, nspin); + const int channels = nspin == 2 ? 2 : 1; + const int n = 4; + Parallel_2D pv; + pv.init(n, n, 1, MPI_COMM_WORLD); + ModuleSymmetry::Symmetry_rotation rotation; + std::vector identity(n * n, 0.0); + std::vector little(n * n, 0.0); + std::vector representative(n * n, 0.0); + std::vector alternate(n * n, 0.0); + const int sign[n] = {1, 1, -1, -1}; + for (int i = 0; i < n; ++i) + { + identity[i + i * n] = 1.0; + little[i + i * n] = sign[i]; + const int row = (i + 1) % n; + representative[row + i * n] = std::polar(1.0, 0.3 * i); + alternate[row + i * n] = double(sign[row]) * representative[row + i * n]; + } + auto local = [&pv, n](const std::vector& dense) { + std::vector result(pv.get_local_size()); + for (int i = 0; i < n; ++i) + { + for (int j = 0; j < n; ++j) + { + if (pv.in_this_processor(i, j)) + { + result[pv.global2local_row(i) + pv.global2local_col(j) * pv.get_row_size()] + = dense[i + j * n]; + } + } + } + return result; + }; + rotation.set_density_rotations_for_testing( + {{{0, local(identity)}, {1, local(little)}, {2, local(representative)}, {3, local(alternate)}}}, + {{0, 1}}, 4); + K_Vectors kv; + kv.set_nkstot(channels); + kv.set_nkstot_nospin(2); + kv.kstars = {{{0, {0.25, 0.0, 0.0}}, {2, {0.0, 0.25, 0.0}}}}; + std::vector> inputs; + std::vector> expected; + for (int spin = 0; spin < channels; ++spin) + { + std::vector density(n * n); + std::vector projected(n * n); + for (int i = 0; i < n; ++i) + { + for (int j = 0; j < n; ++j) + { + const Complex value((spin + 1) * (2.0 + i + j), 0.2 * (i - j)); + density[i + j * n] = value; + // For this C2 little group, averaging removes exactly the odd blocks. + projected[i + j * n] = sign[i] == sign[j] ? 0.5 * value : Complex(0.0); + } + } + inputs.push_back(local(density)); + expected.push_back(local(projected)); + expected.push_back(local(rotate_reference(projected, representative, n))); + } + const auto restored = rotation.restore_dm(kv, inputs, pv); + ASSERT_EQ(restored.size(), expected.size()); + for (size_t k = 0; k < expected.size(); ++k) + { + for (size_t i = 0; i < expected[k].size(); ++i) + { + EXPECT_NEAR(std::abs(restored[k][i] - expected[k][i]), 0.0, 1e-12); + } + } + // A different representative of the same star must give the same density. + kv.kstars = {{{0, {0.25, 0.0, 0.0}}, {3, {0.0, 0.25, 0.0}}}}; + const auto changed_representative = rotation.restore_dm(kv, inputs, pv); + for (size_t k = 0; k < expected.size(); ++k) + { + for (size_t i = 0; i < expected[k].size(); ++i) + { + EXPECT_NEAR(std::abs(changed_representative[k][i] - restored[k][i]), 0.0, 1e-12); + } + } +} +} // namespace + +TEST(SymmetryDensityRestoration, LittleGroupAndStarWeight) +{ + check_little_group_restoration(1); +} + +TEST(SymmetryDensityRestoration, IndependentSpinChannels) +{ + check_little_group_restoration(2); +} + +TEST(SymmetryDensityRestoration, SpinorDensity) +{ + check_little_group_restoration(4); +} diff --git a/tests/08_EXX/08_KP_HSE_symm/README b/tests/08_EXX/08_KP_HSE_symm/README index 88ac4f7c94d..4ab6d25bed1 100644 --- a/tests/08_EXX/08_KP_HSE_symm/README +++ b/tests/08_EXX/08_KP_HSE_symm/README @@ -1 +1,30 @@ -HSE calculation on Si, multiple k-points, cal force and stress, exx real number, symmetry=1 \ No newline at end of file +HSE calculation on Si, multiple k-points, force and stress, real EXX, symmetry=1. + +The density at each irreducible k point must be averaged over its unitary +little group before expanding its star. A star stores one representative per +unique k point; it does not contain all operations fixing that point. On this +coarse finite grid, omitting that average leaves the EXX density outside the +space-group-invariant subspace, so rotating irreducible H(R) blocks can produce +a non-Hermitian Hamiltonian. + +In ABACUS's transposed density storage, the average is + P_k(D) = sum_{g in G_k} M_g^T D M_g^* / |G_k|, +where g k = k modulo a reciprocal lattice vector. P_k is a group projector: +it preserves Hermiticity and removes dependence on the chosen unitary star +representative. The existing star-size factor is applied only after averaging. +The unit tests in module_exx_symmetry/test/test_symm_rotation.cpp exercise this +contract with complex matrices and separate spin channels. + +The reference energy and stress were regenerated after this correction, with +unchanged INPUT and thresholds. On the same case, corrected ScaLAPACK runs with +1 and 4 MPI ranks and one-stage ELPA runs with 1 and 4 ranks agree within 1e-9 eV; +all report totalstressref=2143.792554 and totalforceref=0.000000. Disabling only +exx_symmetry_realspace (full EXX contraction with the same projected density) +also agrees within 2e-10 eV. The maximum ||H-H^dagger||_F over 57 solves drops +from 6.7e-3 to 1.3e-15. This is a fixed-input regression reference, not a claim of +cutoff or outer-loop convergence. The historical timing reference is retained. + +Verification used GCC, OpenMPI 5.0.10, ELPA 2026.02.001, LibXC 7.0.0 and the +repository-pinned LibRI. The two-stage ELPA backend in that environment failed +before the first EXX update, so its result is not included in the agreement +claim; no solver-stage change is part of this fix. diff --git a/tests/08_EXX/08_KP_HSE_symm/result.ref b/tests/08_EXX/08_KP_HSE_symm/result.ref index 2ce56da4a24..689fb369b55 100644 --- a/tests/08_EXX/08_KP_HSE_symm/result.ref +++ b/tests/08_EXX/08_KP_HSE_symm/result.ref @@ -1,7 +1,7 @@ -etotref -189.4277005988057567 -etotperatomref -94.7138502994 +etotref -189.4406826023468398 +etotperatomref -94.7203413012 totalforceref 0.000000 -totalstressref 2155.899405 +totalstressref 2143.792554 pointgroupref D_3d spacegroupref O_h nksibzref 3 From 651d41ce59d9ba3769c6b06d1fb89b380dbf6f6a Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Tue, 8 Sep 2026 15:14:49 +0800 Subject: [PATCH 11/14] fix(build): link builtin NCGGA sources in Makefile and solvation tests Add the builtin NCGGA implementation and radial helper to the legacy XC object list and both solvation test targets that compile xc_grad.cpp directly. This resolves the undefined NCGGA_SF_Builtin symbols in the CPU/CUDA test links and the Intel Makefile build. Validation: both MODULE_HAMILT_surchem_cal_vcav and MODULE_HAMILT_surchem_cal_vel rebuilt and passed CTest on Sai DSPRHBM; Makefile OBJS_XC expansion includes both objects. Full CUDA and Intel Makefile builds remain covered by CI. --- source/Makefile.Objects | 2 ++ source/source_hamilt/module_surchem/test/CMakeLists.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/source/Makefile.Objects b/source/Makefile.Objects index dd2e861167b..ee424497ffe 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -582,6 +582,8 @@ OBJS_SYMMETRY=symm_other.o\ symmetry.o\ OBJS_XC=xc_functional.o\ + xc_functional_ncgga_sf.o\ + xc_ncgga_radial.o\ xc_functional_op.o\ xc_pot.o\ xc_grad.o\ diff --git a/source/source_hamilt/module_surchem/test/CMakeLists.txt b/source/source_hamilt/module_surchem/test/CMakeLists.txt index 50c14955cee..b843b7cb6e1 100644 --- a/source/source_hamilt/module_surchem/test/CMakeLists.txt +++ b/source/source_hamilt/module_surchem/test/CMakeLists.txt @@ -29,6 +29,7 @@ AddTest( LIBS parameter planewave device base container SOURCES cal_vcav_test.cpp ../cal_vcav.cpp ../surchem.cpp ../../module_xc/xc_grad.cpp ../../module_xc/xc_grad_prepare.cpp ../../module_xc/xc_grad_kernel.cpp ../../module_xc/xc_grad_assemble.cpp ../../module_xc/xc_grad_wfc.cpp ../../module_xc/xc_grad_utils.cpp ../../module_xc/xc_functional.cpp + ../../module_xc/xc_functional_ncgga_sf.cpp ../../module_xc/xc_ncgga_radial.cpp ../../module_xc/xc_lda_wrap.cpp ../../module_xc/xc_gga_wrap.cpp ../../module_xc/libxc_setup.cpp ../../module_xc/libxc_pot.cpp @@ -44,6 +45,7 @@ AddTest( LIBS parameter planewave device base container SOURCES cal_vel_test.cpp ../cal_vel.cpp ../surchem.cpp ../cal_epsilon.cpp ../minimize_cg.cpp ../../module_xc/xc_grad.cpp ../../module_xc/xc_grad_prepare.cpp ../../module_xc/xc_grad_kernel.cpp ../../module_xc/xc_grad_assemble.cpp ../../module_xc/xc_grad_wfc.cpp ../../module_xc/xc_grad_utils.cpp ../../module_xc/xc_functional.cpp + ../../module_xc/xc_functional_ncgga_sf.cpp ../../module_xc/xc_ncgga_radial.cpp ../../module_xc/xc_lda_wrap.cpp ../../module_xc/xc_gga_wrap.cpp ../../module_xc/libxc_setup.cpp ../../module_xc/libxc_pot.cpp From 083077633b3282a5796befa04ba12029bbfdc358 Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Tue, 8 Sep 2026 16:38:03 +0800 Subject: [PATCH 12/14] docs: synchronize EXX symmetry description with parameter source (cherry picked from commit 8d632aa582fc83823385be0be4464caeff9a469b) --- docs/advanced/input_files/input-main.md | 2 +- source/source_io/module_parameter/read_inp_exx_dftu.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 111948fff5d..59823ee8a16 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -3396,7 +3396,7 @@ - **Availability**: *[`symmetry`](#symmetry)==1 and ([`dft_functional`](#dft_functional) in [hse, hf, pbe0, scan0] or ([`basis_type`](#basis_type)==lcao and [`rpa`](#rpa)==true))* - **Description**: - False: only rotate k-space density matrix D(k) from irreducible k-points to accelerate diagonalization - True: rotate both D(k) and Hexx(R) to accelerate both diagonalization and EXX calculation - - For multi-k calculations, D(k) is averaged over the unitary little group of each irreducible k point before star expansion, for either setting. + For multi-k calculations, D(k) is averaged over the unitary little group of each irreducible k point before star expansion, for either setting. - **Default**: True ### out_ri_cv diff --git a/source/source_io/module_parameter/read_inp_exx_dftu.cpp b/source/source_io/module_parameter/read_inp_exx_dftu.cpp index 13515d403ee..cb5a067dbf4 100644 --- a/source/source_io/module_parameter/read_inp_exx_dftu.cpp +++ b/source/source_io/module_parameter/read_inp_exx_dftu.cpp @@ -525,7 +525,8 @@ void ReadInput::item_exx() item.category = "Exact Exchange (LCAO)"; item.type = "Boolean"; item.description = R"(* False: only rotate k-space density matrix D(k) from irreducible k-points to accelerate diagonalization -* True: rotate both D(k) and Hexx(R) to accelerate both diagonalization and EXX calculation)"; +* True: rotate both D(k) and Hexx(R) to accelerate both diagonalization and EXX calculation +For multi-k calculations, D(k) is averaged over the unitary little group of each irreducible k point before star expansion, for either setting.)"; item.default_value = "True"; item.unit = ""; item.set_availability("symmetry==1 and (dft_functional in [hse, hf, pbe0, scan0] or (basis_type==lcao and rpa==true))"); From b004efe136878212459cd5f8a2847c1162027906 Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Tue, 8 Sep 2026 18:21:52 +0800 Subject: [PATCH 13/14] test(xc): use finite range separation in NCGGA scaling checks Use the same finite omega for energy and stress evaluations. LibXC 5.1.7 reproduces NaN derivatives at zero omega; the finite-omega fixture passes all 32 tests in serial and MPI2 with LibXC 5.1.7 and 7.0.0. Keep production behavior and test tolerances unchanged. --- .../module_xc/test/test_xc_functional_ncgga_sf.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp b/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp index 120dd99ee01..e7f32d2fca5 100644 --- a/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp +++ b/source/source_hamilt/module_xc/test/test_xc_functional_ncgga_sf.cpp @@ -75,6 +75,10 @@ namespace { int test_rank = 0; int test_size = 1; +// Use a finite range separation for the BLYP_LR scaling check. At omega=0, +// its short- and full-range terms cancel, and some LibXC versions return +// non-finite short-range derivatives at this boundary. +constexpr double test_hse_omega = 0.11; double pool_sum(const double local) { @@ -276,7 +280,7 @@ class RealPwNcgga : public testing::Test 2, scaling_factor, 0.0, - 0.0); + test_hse_omega); } VxcResult evaluate_libxc_gga(const std::map* scaling_factor = nullptr) @@ -507,7 +511,7 @@ class RealPwNcgga : public testing::Test false, 2, 0.0, - 0.0); + test_hse_omega); EXPECT_EQ(stress.size(), 9U); return stress; } From f0c760a238dcaf282a318fadcf69bbf30d01031a Mon Sep 17 00:00:00 2001 From: Chen Chengbing <1747193328@qq.com> Date: Wed, 9 Sep 2026 14:04:09 +0800 Subject: [PATCH 14/14] refactor(xc): pass spin controls explicitly to force and stress helpers Address review feedback by removing PARAM reads from cal_force_cc, stress_cc and stress_gga and updating PW, LCAO, SDFT and OFDFT callers. Name the legacy gga_grad test input explicitly. Production parameter values and numerical formulas are unchanged; no INPUT documentation changes are required. Validation: Sai DSPRHBM job 1196207 rebuilt abacus_basic_para and four XC test executables. Five CTest entries passed, including serial and MPI2 NCGGA finite differences. CUDA validation remains for CI. --- .../source_hamilt/module_xc/test/test_xc3.cpp | 11 +++---- source/source_lcao/force_stress_lcao.cpp | 16 +++++++--- .../source_pw/module_ofdft/of_stress_pw.cpp | 8 +++-- source/source_pw/module_pwdft/force_pw.cpp | 5 +++- source/source_pw/module_pwdft/force_pw.h | 6 +++- source/source_pw/module_pwdft/force_pw_cc.cpp | 26 +++++++++-------- source/source_pw/module_pwdft/stress_cc.cpp | 29 ++++++++++--------- source/source_pw/module_pwdft/stress_func.h | 13 +++++++-- source/source_pw/module_pwdft/stress_gga.cpp | 10 ++++--- source/source_pw/module_pwdft/stress_pw.cpp | 8 +++-- source/source_pw/module_stodft/sto_forces.cpp | 5 +++- .../source_pw/module_stodft/sto_stress_pw.cpp | 8 +++-- 12 files changed, 96 insertions(+), 49 deletions(-) diff --git a/source/source_hamilt/module_xc/test/test_xc3.cpp b/source/source_hamilt/module_xc/test/test_xc3.cpp index 0988923de36..3016c7d6e4f 100644 --- a/source/source_hamilt/module_xc/test/test_xc3.cpp +++ b/source/source_hamilt/module_xc/test/test_xc3.cpp @@ -90,15 +90,16 @@ class XCTest_GRADCORR : public XCTest XC_Functional::set_xc_type("PBE"); + const int gga_grad = 0; double hybrid_alpha = 0.0; double hse_omega = 0.0; - XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,false,nspin1,domag,domag_z,0, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,true,nspin1,domag,domag_z,0, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,false,nspin1,domag,domag_z,gga_grad, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et1,vt1,v1,&chr,&rhopw,&ucell,stress1,true,nspin1,domag,domag_z,gga_grad, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,false,nspin2,domag,domag_z,0, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,true,nspin2,domag,domag_z,0, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,false,nspin2,domag,domag_z,gga_grad, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et2,vt2,v2,&chr,&rhopw,&ucell,stress2,true,nspin2,domag,domag_z,gga_grad, hybrid_alpha, hse_omega); - XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,false,nspin4,domag_true,domag_z,0, hybrid_alpha, hse_omega); + XC_Functional::gradcorr(et4,vt4,v4,&chr,&rhopw,&ucell,stress4,false,nspin4,domag_true,domag_z,gga_grad, hybrid_alpha, hse_omega); } }; diff --git a/source/source_lcao/force_stress_lcao.cpp b/source/source_lcao/force_stress_lcao.cpp index 165553a918a..6210125c050 100644 --- a/source/source_lcao/force_stress_lcao.cpp +++ b/source/source_lcao/force_stress_lcao.cpp @@ -921,6 +921,8 @@ void Force_Stress_LCAO::calForcePwPart(UnitCell& ucell, const pseudopot_cell_vl& locpp, const Structure_Factor& sf) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::TITLE("Force_Stress_LCAO", "calForcePwPart"); #ifdef __CUDA if(PARAM.inp.device == "gpu") @@ -928,7 +930,8 @@ void Force_Stress_LCAO::calForcePwPart(UnitCell& ucell, Forces f_pw(nat); f_pw.cal_force_loc(ucell, fvl_dvl, rhopw, locpp.vloc, chr); f_pw.cal_force_ew(ucell, fewalds, rhopw, &sf); - f_pw.cal_force_cc(fcc, rhopw, chr, locpp.numeric, ucell); + f_pw.cal_force_cc(fcc, rhopw, chr, locpp.numeric, ucell, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); f_pw.cal_force_scc(fscc, rhopw, vnew, vnew_exist, locpp.numeric, ucell); } else @@ -937,7 +940,8 @@ void Force_Stress_LCAO::calForcePwPart(UnitCell& ucell, Forces f_pw(nat); f_pw.cal_force_loc(ucell, fvl_dvl, rhopw, locpp.vloc, chr); f_pw.cal_force_ew(ucell, fewalds, rhopw, &sf); - f_pw.cal_force_cc(fcc, rhopw, chr, locpp.numeric, ucell); + f_pw.cal_force_cc(fcc, rhopw, chr, locpp.numeric, ucell, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); f_pw.cal_force_scc(fscc, rhopw, vnew, vnew_exist, locpp.numeric, ucell); } @@ -1027,6 +1031,8 @@ void Force_Stress_LCAO::calStressPwPart(UnitCell& ucell, const pseudopot_cell_vl& locpp, const Structure_Factor& sf) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::TITLE("Force_Stress_LCAO", "calStressPwPart"); // local pseudopotential stress: @@ -1039,7 +1045,8 @@ void Force_Stress_LCAO::calStressPwPart(UnitCell& ucell, sc_pw.stress_ewa(ucell, sigmaewa, rhopw, 0); // remain problem // stress due to core correlation. - sc_pw.stress_cc(sigmacc, rhopw, ucell, &sf, 0, locpp.numeric, chr); + sc_pw.stress_cc(sigmacc, rhopw, ucell, &sf, 0, locpp.numeric, chr, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad, xc_spin.gamma_only_pw); // stress due to self-consistent charge. for (int i = 0; i < 3; i++) @@ -1047,7 +1054,8 @@ void Force_Stress_LCAO::calStressPwPart(UnitCell& ucell, sigmaxc(i, i) = -etxc / ucell.omega; } // Exchange-correlation for PBE - sc_pw.stress_gga(ucell, sigmaxc, rhopw, chr); + sc_pw.stress_gga(ucell, sigmaxc, rhopw, chr, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); return; } diff --git a/source/source_pw/module_ofdft/of_stress_pw.cpp b/source/source_pw/module_ofdft/of_stress_pw.cpp index 6b988baf786..65836ddb3a4 100644 --- a/source/source_pw/module_ofdft/of_stress_pw.cpp +++ b/source/source_pw/module_ofdft/of_stress_pw.cpp @@ -16,6 +16,8 @@ void OF_Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, Structure_Factor* p_sf, K_Vectors* p_kv) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::TITLE("OF_Stress_PW", "cal_stress"); ModuleBase::timer::start("OF_Stress_PW", "cal_stress"); @@ -74,13 +76,15 @@ void OF_Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, { sigmaxc(i, i) = -(pelec->f_en.etxc - pelec->f_en.vtxc) / ucell.omega; } - stress_gga(ucell,sigmaxc, this->rhopw, pelec->charge); + stress_gga(ucell,sigmaxc, this->rhopw, pelec->charge, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); // local contribution stress_loc(ucell,sigmaloc, this->rhopw, locpp.vloc, p_sf, true, pelec->charge); // nlcc - stress_cc(sigmaxcc, this->rhopw, ucell, p_sf, true, locpp.numeric, pelec->charge); + stress_cc(sigmaxcc, this->rhopw, ucell, p_sf, true, locpp.numeric, pelec->charge, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad, xc_spin.gamma_only_pw); // vdW term prepared before SCF for this ionic configuration. if (vdw_result != nullptr) diff --git a/source/source_pw/module_pwdft/force_pw.cpp b/source/source_pw/module_pwdft/force_pw.cpp index 787eb528bd1..4c559835e86 100644 --- a/source/source_pw/module_pwdft/force_pw.cpp +++ b/source/source_pw/module_pwdft/force_pw.cpp @@ -41,6 +41,8 @@ void Forces::cal_force(UnitCell& ucell, ModulePW::PW_Basis_K* wfc_basis, const psi::Psi, Device>* psi_in) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::timer::start("Forces", "cal_force"); ModuleBase::TITLE("Forces", "init"); this->device = base_device::get_device_type(this->ctx); @@ -83,7 +85,8 @@ void Forces::cal_force(UnitCell& ucell, } // non-linear core correction - Forces::cal_force_cc(forcecc, rho_basis, chr, locpp->numeric, ucell); + Forces::cal_force_cc(forcecc, rho_basis, chr, locpp->numeric, ucell, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); // force due to core charge this->cal_force_scc(forcescc, rho_basis, elec.vnew, elec.vnew_exist, locpp->numeric, ucell); diff --git a/source/source_pw/module_pwdft/force_pw.h b/source/source_pw/module_pwdft/force_pw.h index 3a2f3c5fa9f..28a99a24171 100644 --- a/source/source_pw/module_pwdft/force_pw.h +++ b/source/source_pw/module_pwdft/force_pw.h @@ -77,7 +77,11 @@ class Forces const ModulePW::PW_Basis* const rho_basis, const Charge* const chr, const bool* numeric, - UnitCell& ucell_in); + UnitCell& ucell_in, + const int nspin, + const bool domag, + const bool domag_z, + const int gga_grad); /** * @brief This routine computes the atomic force of non-local pseudopotential * F^{NL}_i = \sum_{n,k}f_{nk}\sum_I \sum_{lm,l'm'}D_{l,l'}^{I} [ diff --git a/source/source_pw/module_pwdft/force_pw_cc.cpp b/source/source_pw/module_pwdft/force_pw_cc.cpp index e556a6f810b..a0d0b944e4c 100644 --- a/source/source_pw/module_pwdft/force_pw_cc.cpp +++ b/source/source_pw/module_pwdft/force_pw_cc.cpp @@ -1,7 +1,6 @@ #include "force_pw.h" #include "stress_func.h" #include "source_base/parallel_reduce.h" -#include "source_io/module_parameter/parameter.h" // new #include "source_base/complexmatrix.h" #include "source_base/libm/libm.h" @@ -32,9 +31,12 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, const ModulePW::PW_Basis* const rho_basis, const Charge* const chr, const bool* numeric, - UnitCell& ucell_in) + UnitCell& ucell_in, + const int nspin, + const bool domag, + const bool domag_z, + const int gga_grad) { - const Parameter& parameters = PARAM; ModuleBase::TITLE("Forces", "cal_force_cc"); // recalculate the exchange-correlation potential. ModuleBase::timer::start("Forces", "cal_force_cc"); @@ -54,7 +56,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, return; } - ModuleBase::matrix v(parameters.inp.nspin, rho_basis->nrxx); + ModuleBase::matrix v(nspin, rho_basis->nrxx); const double hybrid_alpha = XC_Functional::get_hybrid_alpha(); #ifdef __EXX @@ -67,7 +69,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, #ifdef __LIBXC const auto etxc_vtxc_v = XC_Functional_Libxc::v_xc_meta(XC_Functional::get_func_id(), rho_basis->nrxx, ucell_in.omega, ucell_in.tpiba, chr, - parameters.inp.nspin, hybrid_alpha, hse_omega); + nspin, hybrid_alpha, hse_omega); // etxc = std::get<0>(etxc_vtxc_v); // vtxc = std::get<1>(etxc_vtxc_v); @@ -78,12 +80,12 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, } else { - unitcell::cal_ux(ucell_in, parameters.inp.nspin); + unitcell::cal_ux(ucell_in, nspin); const auto etxc_vtxc_v = XC_Functional::v_xc(rho_basis->nrxx, chr, &ucell_in, - parameters.inp.nspin, - parameters.globalv.domag, - parameters.globalv.domag_z, - parameters.inp.gga_grad, + nspin, + domag, + domag_z, + gga_grad, hybrid_alpha, hse_omega); @@ -94,7 +96,7 @@ void Forces::cal_force_cc(ModuleBase::matrix& forcecc, const ModuleBase::matrix vxc = v; std::complex* psiv = new std::complex[rho_basis->nmaxgr]; - if (parameters.inp.nspin == 1 || parameters.inp.nspin == 4) + if (nspin == 1 || nspin == 4) { #ifdef _OPENMP #pragma omp parallel for schedule(static, 1024) @@ -370,4 +372,4 @@ void Forces::deriv_drhoc template class Forces; #if ((defined __CUDA) || (defined __ROCM)) template class Forces; -#endif \ No newline at end of file +#endif diff --git a/source/source_pw/module_pwdft/stress_cc.cpp b/source/source_pw/module_pwdft/stress_cc.cpp index a6778447095..a0b6cb3acb3 100644 --- a/source/source_pw/module_pwdft/stress_cc.cpp +++ b/source/source_pw/module_pwdft/stress_cc.cpp @@ -1,7 +1,6 @@ #include "stress_func.h" #include "source_base/parallel_reduce.h" #include "source_hamilt/module_xc/xc_functional.h" -#include "source_io/module_parameter/parameter.h" #include "source_base/math_integral.h" #include "source_base/timer.h" #include "source_cell/cal_ux.h" @@ -15,19 +14,23 @@ template void Stress_Func::stress_cc(ModuleBase::matrix& sigma, ModulePW::PW_Basis* rho_basis, - UnitCell& ucell, + UnitCell& ucell, const Structure_Factor* p_sf, const bool is_pw, - const bool *numeric, - const Charge* const chr) + const bool *numeric, + const Charge* const chr, + const int nspin, + const bool domag, + const bool domag_z, + const int gga_grad, + const bool gamma_only_pw) { - const Parameter& parameters = PARAM; ModuleBase::TITLE("Stress","stress_cc"); ModuleBase::timer::start("Stress","stress_cc"); FPTYPE fact=1.0; - if(is_pw&¶meters.globalv.gamma_only_pw) + if(is_pw&&gamma_only_pw) { fact = 2.0; //is_pw:PW basis, gamma_only need to FPTYPE. } @@ -63,7 +66,7 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, #ifdef __LIBXC const auto etxc_vtxc_v = XC_Functional_Libxc::v_xc_meta(XC_Functional::get_func_id(), rho_basis->nrxx, ucell.omega, ucell.tpiba, chr, - parameters.inp.nspin, hybrid_alpha, hse_omega); + nspin, hybrid_alpha, hse_omega); // etxc = std::get<0>(etxc_vtxc_v); // vtxc = std::get<1>(etxc_vtxc_v); @@ -74,12 +77,12 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, } else { - unitcell::cal_ux(ucell, parameters.inp.nspin); + unitcell::cal_ux(ucell, nspin); const auto etxc_vtxc_v = XC_Functional::v_xc(rho_basis->nrxx, chr, &ucell, - parameters.inp.nspin, - parameters.globalv.domag, - parameters.globalv.domag_z, - parameters.inp.gga_grad, + nspin, + domag, + domag_z, + gga_grad, hybrid_alpha, hse_omega); // etxc = std::get<0>(etxc_vtxc_v); // may delete? @@ -89,7 +92,7 @@ void Stress_Func::stress_cc(ModuleBase::matrix& sigma, std::complex* psic = new std::complex[rho_basis->nmaxgr]; - if(parameters.inp.nspin==1||parameters.inp.nspin==4) + if(nspin==1||nspin==4) { #ifdef _OPENMP #pragma omp parallel for schedule(static, 1024) diff --git a/source/source_pw/module_pwdft/stress_func.h b/source/source_pw/module_pwdft/stress_func.h index 9ac596cb63c..09b668d2210 100644 --- a/source/source_pw/module_pwdft/stress_func.h +++ b/source/source_pw/module_pwdft/stress_func.h @@ -117,7 +117,12 @@ class Stress_Func const Structure_Factor* p_sf, const bool is_pw, const bool *numeric, - const Charge* const chr); // nonlinear core correction stress in PW or LCAO basis + const Charge* const chr, + const int nspin, + const bool domag, + const bool domag_z, + const int gga_grad, + const bool gamma_only_pw); // nonlinear core correction stress in PW or LCAO basis void deriv_drhoc(const bool& numeric, const double& omega, @@ -134,7 +139,11 @@ class Stress_Func void stress_gga(const UnitCell& ucell, ModuleBase::matrix& sigma, ModulePW::PW_Basis* rho_basis, - const Charge* const chr); // gga part in both PW and LCAO basis + const Charge* const chr, + const int nspin, + const bool domag, + const bool domag_z, + const int gga_grad); // gga part in both PW and LCAO basis void stress_mgga(const UnitCell& ucell, ModuleBase::matrix& sigma, const ModuleBase::matrix& wg, diff --git a/source/source_pw/module_pwdft/stress_gga.cpp b/source/source_pw/module_pwdft/stress_gga.cpp index dc9eaf7e9d2..1716bd093db 100644 --- a/source/source_pw/module_pwdft/stress_gga.cpp +++ b/source/source_pw/module_pwdft/stress_gga.cpp @@ -1,16 +1,18 @@ #include "stress_func.h" #include "source_base/parallel_reduce.h" #include "source_hamilt/module_xc/xc_functional.h" -#include "source_io/module_parameter/parameter.h" //calculate the GGA stress correction in PW and LCAO template void Stress_Func::stress_gga(const UnitCell& ucell, ModuleBase::matrix& sigma, ModulePW::PW_Basis* rho_basis, - const Charge* const chr) + const Charge* const chr, + const int nspin, + const bool domag, + const bool domag_z, + const int gga_grad) { - const Parameter& parameters = PARAM; ModuleBase::TITLE("Stress","stress_gga"); ModuleBase::timer::start("Stress","stress_gga"); @@ -32,7 +34,7 @@ void Stress_Func::stress_gga(const UnitCell& ucell, XC_Functional::gradcorr( dum1, dum2, dum3, chr, rho_basis, &ucell, stress_gga, is_stress, - parameters.inp.nspin, parameters.globalv.domag, parameters.globalv.domag_z, parameters.inp.gga_grad, + nspin, domag, domag_z, gga_grad, hybrid_alpha, hse_omega); for(int l = 0;l< 3;l++) diff --git a/source/source_pw/module_pwdft/stress_pw.cpp b/source/source_pw/module_pwdft/stress_pw.cpp index bcf6aff8db2..3bf3be8e3a9 100644 --- a/source/source_pw/module_pwdft/stress_pw.cpp +++ b/source/source_pw/module_pwdft/stress_pw.cpp @@ -23,6 +23,8 @@ void Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, const General_Exx_Info& exx_info, const psi::Psi , Device>* d_psi_in) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::TITLE("Stress_PW", "cal_stress"); ModuleBase::timer::start("Stress_PW", "cal_stress"); @@ -91,7 +93,8 @@ void Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, { sigmaxc(i, i) = -(pelec->f_en.etxc - pelec->f_en.vtxc) / ucell.omega; } - this->stress_gga(ucell, sigmaxc, rho_basis, pelec->charge); + this->stress_gga(ucell, sigmaxc, rho_basis, pelec->charge, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); if (XC_Functional::get_ked_flag()) { this->stress_mgga(ucell, @@ -108,7 +111,8 @@ void Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, this->stress_loc(ucell, sigmaloc, rho_basis, locpp.vloc, p_sf, 1, pelec->charge); // nlcc - this->stress_cc(sigmaxcc, rho_basis, ucell, p_sf, 1, locpp.numeric, pelec->charge); + this->stress_cc(sigmaxcc, rho_basis, ucell, p_sf, 1, locpp.numeric, pelec->charge, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad, xc_spin.gamma_only_pw); // nonlocal this->stress_nl(sigmanl, this->pelec->wg, this->pelec->ekb, p_sf, p_kv, p_symm, wfc_basis, d_psi_in, nlpp, ucell); diff --git a/source/source_pw/module_stodft/sto_forces.cpp b/source/source_pw/module_stodft/sto_forces.cpp index e092b8f9327..d2400de3430 100644 --- a/source/source_pw/module_stodft/sto_forces.cpp +++ b/source/source_pw/module_stodft/sto_forces.cpp @@ -29,6 +29,8 @@ void Sto_Forces::cal_stoforce(ModuleBase::matrix& force, const psi::Psi, Device>& psi, const Stochastic_WF, Device>& stowf) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::timer::start("Sto_Forces", "cal_force"); ModuleBase::TITLE("Sto_Forces", "init"); this->device = base_device::get_device_type(this->ctx); @@ -44,7 +46,8 @@ void Sto_Forces::cal_stoforce(ModuleBase::matrix& force, this->cal_force_loc(ucell, forcelc, rho_basis, locpp.vloc, chr); this->cal_force_ew(ucell,forceion, rho_basis, p_sf); this->cal_sto_force_nl(forcenl, wg, pkv, wfc_basis, p_sf, nlpp, ucell, psi, stowf); - this->cal_force_cc(forcecc, rho_basis, chr, locpp.numeric, ucell); + this->cal_force_cc(forcecc, rho_basis, chr, locpp.numeric, ucell, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); this->cal_force_scc(forcescc, rho_basis, elec.vnew, elec.vnew_exist, locpp.numeric, ucell); // impose total force = 0 diff --git a/source/source_pw/module_stodft/sto_stress_pw.cpp b/source/source_pw/module_stodft/sto_stress_pw.cpp index b85ab58b503..e286a1cfd42 100644 --- a/source/source_pw/module_stodft/sto_stress_pw.cpp +++ b/source/source_pw/module_stodft/sto_stress_pw.cpp @@ -23,6 +23,8 @@ void Sto_Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, const pseudopot_cell_vnl* nlpp, UnitCell& ucell_in) { + const auto& xc_input = PARAM.inp; + const auto& xc_spin = PARAM.globalv; ModuleBase::TITLE("Sto_Stress_PW", "cal_stress"); ModuleBase::timer::start("Sto_Stress_PW", "cal_stress"); const ModuleBase::matrix& wg = elec.wg; @@ -50,13 +52,15 @@ void Sto_Stress_PW::cal_stress(ModuleBase::matrix& sigmatot, { sigmaxc(i, i) = -(elec.f_en.etxc - elec.f_en.vtxc) / this->ucell->omega; } - this->stress_gga(ucell_in, sigmaxc, rho_basis, chr); + this->stress_gga(ucell_in, sigmaxc, rho_basis, chr, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad); // local contribution this->stress_loc(ucell_in, sigmaloc, rho_basis, locpp->vloc, p_sf, true, chr); // nlcc - this->stress_cc(sigmaxcc, rho_basis, ucell_in, p_sf, true, locpp->numeric, chr); + this->stress_cc(sigmaxcc, rho_basis, ucell_in, p_sf, true, locpp->numeric, chr, + xc_input.nspin, xc_spin.domag, xc_spin.domag_z, xc_input.gga_grad, xc_spin.gamma_only_pw); // nonlocal this->sto_stress_nl(sigmanl, wg, p_sf, p_symm, p_kv, wfc_basis, *nlpp, ucell_in, psi_in, stowf);