diff --git a/source/source_base/element_basis_index.cpp b/source/source_base/element_basis_index.cpp index 46fe192c97f..6ce78c0c86b 100644 --- a/source/source_base/element_basis_index.cpp +++ b/source/source_base/element_basis_index.cpp @@ -5,6 +5,9 @@ #include "element_basis_index.h" +#include +#include + namespace ModuleBase { @@ -37,4 +40,63 @@ Element_Basis_Index::construct_index( const Range &range ) return index; } -} \ No newline at end of file +Element_Basis_Index::IndexLNM +Element_Basis_Index::construct_index( const Range &range, const IndexPermutation &old_to_new ) +{ + if( old_to_new.empty() ) + { + return construct_index(range); + } + + IndexLNM index; + index.resize( range.size() ); + for( std::size_t T=0; T!=range.size(); ++T ) + { + std::size_t count_size = 0; + for( std::size_t L=0; L!=range[T].size(); ++L ) + { + count_size += range[T][L].N * range[T][L].M; + } + + const bool use_permutation = (T < old_to_new.size() && !old_to_new[T].empty()); + if( use_permutation ) + { + if( old_to_new[T].size() != count_size ) + { + throw std::invalid_argument("Element_Basis_Index::construct_index permutation size mismatch."); + } + + std::vector seen(count_size, false); + for( const std::size_t new_index : old_to_new[T] ) + { + if( new_index >= count_size || seen[new_index] ) + { + throw std::invalid_argument("Element_Basis_Index::construct_index permutation is not bijective."); + } + seen[new_index] = true; + } + } + + std::size_t count = 0; + index[T].resize( range[T].size() ); + for( std::size_t L=0; L!=range[T].size(); ++L ) + { + index[T][L].resize( range[T][L].N ); + for( std::size_t N=0; N!=range[T][L].N; ++N ) + { + index[T][L][N].resize( range[T][L].M ); + for( std::size_t M=0; M!=range[T][L].M; ++M ) + { + index[T][L][N][M] = use_permutation ? old_to_new[T][count] : count; + ++count; + } + } + index[T][L].N = range[T][L].N; + index[T][L].M = range[T][L].M; + } + index[T].count_size = count_size; + } + return index; +} + +} diff --git a/source/source_base/element_basis_index.h b/source/source_base/element_basis_index.h index 22bfd2c7f0a..cb0f9589f90 100644 --- a/source/source_base/element_basis_index.h +++ b/source/source_base/element_basis_index.h @@ -16,34 +16,36 @@ namespace Element_Basis_Index { //private: - struct NM - { - public: - std::size_t N; - std::size_t M; - }; - - class Index_TL: public std::vector> - { - public: - std::size_t N; - std::size_t M; - }; - - class Index_T: public std::vector - { - public: - std::size_t count_size; - }; + struct NM + { + public: + std::size_t N; + std::size_t M; + }; + + class Index_TL: public std::vector> + { + public: + std::size_t N; + std::size_t M; + }; + + class Index_T: public std::vector + { + public: + std::size_t count_size; + }; //public: - typedef std::vector> Range; // range[T][L] - typedef std::vector IndexLNM; // index[T][L][N][M] + typedef std::vector> Range; // range[T][L] + typedef std::vector IndexLNM; // index[T][L][N][M] + typedef std::vector> IndexPermutation; // old_to_new[T][old_index] - extern IndexLNM construct_index( const Range &range ); + extern IndexLNM construct_index( const Range &range ); + extern IndexLNM construct_index( const Range &range, const IndexPermutation &old_to_new ); } } -#endif \ No newline at end of file +#endif diff --git a/source/source_cell/module_symmetry/irred_sec.h b/source/source_cell/module_symmetry/irred_sec.h index 0f68e88103d..97f0dbaa313 100644 --- a/source/source_cell/module_symmetry/irred_sec.h +++ b/source/source_cell/module_symmetry/irred_sec.h @@ -11,6 +11,8 @@ namespace ModuleSymmetry { + class SymmetryRotationTestAccess; + using Tap = std::pair; using TC = std::array; using TapR = std::pair; @@ -149,6 +151,7 @@ namespace ModuleSymmetry friend class Symmetry_rotation; friend class Symmetry_rotation_k; + friend class SymmetryRotationTestAccess; }; } diff --git a/source/source_cell/module_symmetry/symm_rotation_k.cpp b/source/source_cell/module_symmetry/symm_rotation_k.cpp index ac145a67e16..e9af5262f8b 100644 --- a/source/source_cell/module_symmetry/symm_rotation_k.cpp +++ b/source/source_cell/module_symmetry/symm_rotation_k.cpp @@ -235,8 +235,9 @@ namespace ModuleSymmetry // calculate Wigner D matrix double Symmetry_rotation_k::wigner_d(const double beta, const int l, const int m1, const int m2) const { - auto factorial = [](int n) -> int { - int result = 1; + // The four-factorial product overflows int for l >= 5, so evaluate in double. + auto factorial = [](int n) -> double { + double result = 1; for (int i = 1;i <= n;++i) { result *= i; } return result; diff --git a/source/source_hamilt/module_xc/exx_info.cpp b/source/source_hamilt/module_xc/exx_info.cpp index 7098c1dd7d5..4887d7fb366 100644 --- a/source/source_hamilt/module_xc/exx_info.cpp +++ b/source/source_hamilt/module_xc/exx_info.cpp @@ -221,12 +221,24 @@ bool init_exx_info(Exx_Info& exx_info, const Input_para& inp) exx_info.info_ri.pca_threshold = inp.exx_pca_threshold; exx_info.info_ri.C_threshold = inp.exx_c_threshold; exx_info.info_ri.V_threshold = inp.exx_v_threshold; + exx_info.info_ri.V_threshold_long = inp.exx_v_threshold_long; + exx_info.info_ri.V_cd_threshold = inp.exx_vcd_threshold; + exx_info.info_ri.V_cd_stats_only = inp.exx_vcd_stats_only; + exx_info.info_ri.V_cd_short_only = inp.exx_vcd_short_only; exx_info.info_ri.dm_threshold = inp.exx_dm_threshold; exx_info.info_ri.C_grad_threshold = inp.exx_c_grad_threshold; exx_info.info_ri.V_grad_threshold = inp.exx_v_grad_threshold; exx_info.info_ri.C_grad_R_threshold = inp.exx_c_grad_r_threshold; exx_info.info_ri.V_grad_R_threshold = inp.exx_v_grad_r_threshold; exx_info.info_ri.ccp_rmesh_times = std::stod(inp.exx_ccp_rmesh_times); + exx_info.info_ri.ewald_lambda = inp.exx_ewald_lambda; + exx_info.info_ri.ewald_tail_check = inp.exx_ewald_tail_check; + exx_info.info_ri.ewald_tail_abs_tol = inp.exx_ewald_tail_abs_tol; + exx_info.info_ri.ewald_tail_rel_tol = inp.exx_ewald_tail_rel_tol; + exx_info.info_ri.ewald_tail_sum_rel_tol = inp.exx_ewald_tail_sum_rel_tol; + exx_info.info_ri.ewald_tail_guard_cells = inp.exx_ewald_tail_guard_cells; + exx_info.info_ri.ewald_tail_max_expansions = inp.exx_ewald_tail_max_expansions; + exx_info.info_ri.ewald_dimension = inp.exx_ewald_dimension; exx_info.info_ri.exx_symmetry_realspace = inp.exx_symmetry_realspace; exx_info.info_ri.Cs_inv_thr = inp.exx_cs_inv_thr; exx_info.info_ri.shrink_abfs_pca_thr = inp.shrink_abfs_pca_thr; diff --git a/source/source_hamilt/module_xc/exx_info_ri.h b/source/source_hamilt/module_xc/exx_info_ri.h index 6e8d9b0367e..f970e39b2a0 100644 --- a/source/source_hamilt/module_xc/exx_info_ri.h +++ b/source/source_hamilt/module_xc/exx_info_ri.h @@ -20,12 +20,24 @@ struct Exx_Info_RI std::vector files_shrink_abfs; double C_threshold = 0; double V_threshold = 0; + double V_threshold_long = 0; + double V_cd_threshold = -1; + bool V_cd_stats_only = false; + bool V_cd_short_only = true; double dm_threshold = 0; double C_grad_threshold = 0; double V_grad_threshold = 0; double C_grad_R_threshold = 0; double V_grad_R_threshold = 0; double ccp_rmesh_times = 10; + double ewald_lambda = 1.0; + std::string ewald_tail_check = "warn"; + double ewald_tail_abs_tol = 1e-12; + double ewald_tail_rel_tol = 1e-10; + double ewald_tail_sum_rel_tol = 1e-8; + int ewald_tail_guard_cells = 1; + int ewald_tail_max_expansions = 3; + int ewald_dimension = 3; bool exx_symmetry_realspace = true; double kmesh_times = 4; double Cs_inv_thr = -1; diff --git a/source/source_hamilt/module_xc/xc_functional.cpp b/source/source_hamilt/module_xc/xc_functional.cpp index afb4aff039c..3be8966caa7 100644 --- a/source/source_hamilt/module_xc/xc_functional.cpp +++ b/source/source_hamilt/module_xc/xc_functional.cpp @@ -2,8 +2,12 @@ #include "source_base/global_function.h" #include "source_base/tool_title.h" #include "source_base/constants.h" +#include "source_io/module_parameter/parameter.h" #include +#include +#include + #ifdef __LIBXC #include "libxc_abacus.h" #endif @@ -77,6 +81,23 @@ void XC_Functional::set_runtime_parameters(const XCFunctionalParameters& paramet runtime_parameters = parameters; } +std::string XC_Functional::resolve_runtime_xc_type(const std::string& input_xc_func, + const std::string& pseudo_xc_func) +{ + std::string input_xc = input_xc_func; + std::transform(input_xc.begin(), input_xc.end(), input_xc.begin(), (::toupper)); + if (!input_xc.empty() && input_xc != "DEFAULT" && input_xc != "NONE") + { + return input_xc_func; + } + return pseudo_xc_func; +} + +std::string XC_Functional::resolve_runtime_xc_type(const UnitCell& ucell) +{ + return resolve_runtime_xc_type(PARAM.inp.dft_functional, ucell.atoms[0].ncpp.xc_func); +} + void XC_Functional::set_xc_first_loop(const UnitCell& ucell) { ModuleBase::TITLE("XC_Functional", "set_xc_first_loop"); diff --git a/source/source_hamilt/module_xc/xc_functional.h b/source/source_hamilt/module_xc/xc_functional.h index 3465a46794c..ca700f8e4d6 100644 --- a/source/source_hamilt/module_xc/xc_functional.h +++ b/source/source_hamilt/module_xc/xc_functional.h @@ -103,6 +103,9 @@ class XC_Functional }; static void set_xc_type(const std::string xc_func_in); + static std::string resolve_runtime_xc_type(const std::string& input_xc_func, + const std::string& pseudo_xc_func); + static std::string resolve_runtime_xc_type(const UnitCell& ucell); // Temporary adapter for legacy Potential -> PotXC -> XC evaluation globals. // Restores the complete selection on scope exit, including exception unwinding. diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index b7737f89109..d8c23b046e1 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -630,6 +630,7 @@ struct Input_para bool exx_separate_loop = true; ///< if 1, a two-step method is employed, else it will start ///< with a GGA-Loop, and then Hybrid-Loop std::string exx_singularity_correction = "default"; ///< set the scheme of Coulomb singularity correction + int exx_ewald_dimension = 3; ///< dimensionality used by Ewald Coulomb singularity correction int exx_hybrid_step = 100; ///< the maximal electronic iteration number in ///< the evaluation of Fock exchange double exx_mixing_beta = 1.0; ///< mixing_beta for outer-loop when exx_separate_loop=1 @@ -637,6 +638,10 @@ struct Input_para double exx_pca_threshold = 0.0001; ///< threshold to screen on-site ABFs in exx double exx_c_threshold = 0.0001; ///< threshold to screen C matrix in exx double exx_v_threshold = 0.1; ///< threshold to screen C matrix in exx + double exx_v_threshold_long = 0.0; ///< threshold to screen long-range V matrix in exx + double exx_vcd_threshold = -1.0; ///< dynamic C/D-weighted threshold for merged-short EXX paths + bool exx_vcd_stats_only = false; ///< collect weighted short-screen stats without skipping paths + bool exx_vcd_short_only = true; ///< apply weighted short-screening only to the split short channel double exx_dm_threshold = 0.0001; ///< threshold to screen density matrix in exx double exx_c_grad_threshold = 0.0001; ///< threshold to screen nabla C matrix in exx double exx_v_grad_threshold = 0.1; ///< threshold to screen nabla V matrix in exx @@ -644,6 +649,13 @@ struct Input_para double exx_v_grad_r_threshold = 0.1; ///< threshold to screen nabla V * R matrix in exx std::string exx_ccp_rmesh_times = "default"; ///< how many times larger the radial mesh required for ///< calculating Columb potential is to that of atomic orbitals + double exx_ewald_lambda = 1.0; ///< Gaussian decay coefficient for Ewald full Coulomb in RI-EXX + std::string exx_ewald_tail_check = "warn"; ///< off, warn, enlarge, or strict short-range tail handling + double exx_ewald_tail_abs_tol = 1e-12; ///< absolute Frobenius-norm tolerance for one guard-shell block + double exx_ewald_tail_rel_tol = 1e-10; ///< relative Frobenius-norm tolerance for one guard-shell block + double exx_ewald_tail_sum_rel_tol = 1e-8; ///< relative sum-of-block-norm tolerance for the guard shell + int exx_ewald_tail_guard_cells = 1; ///< lattice-cell layers added in every periodic direction + int exx_ewald_tail_max_expansions = 3; ///< maximum adaptive real-space range expansions int exx_opt_orb_lmax = 0; ///< the maximum l of the spherical Bessel functions for opt ABFs double exx_opt_orb_ecut = 0.0; ///< the cut-off of plane wave expansion for opt ABFs double exx_opt_orb_tolerence = 0.0; ///< the threshold when solving for the zeros of spherical Bessel @@ -651,12 +663,21 @@ struct Input_para bool exx_symmetry_realspace = true; ///< whether to reduce the real-space sector in when using symmetry=1 in EXX calculation double rpa_ccp_rmesh_times = 10.0; ///< how many times larger the radial mesh required for ///< calculating Columb potential is to that of atomic orbitals + std::string rpa_abfs_preorth = "none"; ///< on-site auxiliary Coulomb preorthogonalization for RPA output + double rpa_abfs_preorth_threshold = 1.0e-2; ///< residual Coulomb-norm threshold for RPA ABFS preorthogonalization double exx_cs_inv_thr = -1; ///< threshold to inverse Vq in abfs for generating Cs double shrink_abfs_pca_thr = -1; ///< threshold to shrink auxiliary basis for GW/RPA double shrink_LU_inv_thr = 1e-6; ///< threshold to get inverse of overlap matrix by LU decomposition in auxiliary basis representation bool out_ri_cv = false; ///< Whether to output the coefficient tensor C and ABFs-representation Coulomb matrix V bool out_unshrinked_v = false; ///< whether to output the large Vq matrix in unshrinked auxiliary basis int out_librpa_ver = 0; ///< 0: legacy ABACUS LibRPA text files; 1: LibRPA reader-v1 binary files + std::string out_librpa_2d_coulomb_method = "ewald"; ///< ewald or direct_mixed_fourier for reader-v1 full V + double out_librpa_2d_direct_ecut = 0.0; ///< reciprocal cutoff in Ry for direct mixed-Fourier 2D Coulomb + int out_librpa_2d_direct_kz_order = 0; ///< positive kz quadrature order for direct mixed-Fourier 2D Coulomb + int out_librpa_2d_direct_gamma_order = 8; ///< positive even Gamma-plane quadrature order + std::string out_librpa_3d_coulomb_method = "ewald"; ///< ewald or direct_reciprocal for reader-v1 full V + double out_librpa_3d_direct_ecut = 0.0; ///< reciprocal cutoff in Ry for direct 3D Coulomb + bool out_librpa_abf_overlap = false; ///< output raw active-ABF overlap for LibRPA v1 PSD diagnostics bool exx_coul_moment = false; ///< whether to use moment method for Coulomb calculation bool exx_rotate_abfs = false; ///< whether to rotate auxiliary basis for Coulomb calculation double exx_multip_moments_threshold = 1e-10; ///< threshold to screen multipole moments in Coulomb calculation 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 d67fdfb509d..e1bc76bd029 100644 --- a/source/source_io/module_parameter/read_inp_exx_dftu.cpp +++ b/source/source_io/module_parameter/read_inp_exx_dftu.cpp @@ -1,3 +1,5 @@ +#include + #include "source_base/constants.h" #include "source_base/tool_quit.h" #include "read_input.h" @@ -281,6 +283,54 @@ void ReadInput::item_exx() read_sync_double(input.exx_v_threshold); this->add_item(item); } + { + Input_Item item("exx_v_threshold_long"); + item.annotation = "threshold to screen long-range V matrix in exx"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.description + = "Threshold used only for the long-range Coulomb channel in the rotated-ABFS moment workflow. Smaller values of the long-range V matrix can be truncated to accelerate calculation. The default is 0, i.e. no truncation, which preserves the current behavior."; + item.default_value = "0"; + item.unit = ""; + read_sync_double(input.exx_v_threshold_long); + this->add_item(item); + } + { + Input_Item item("exx_vcd_threshold"); + item.annotation = "dynamic C/D-weighted threshold for merged-short EXX"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.description + = "Optional second-stage screen applied during EXX short-channel contractions. A positive value enables dynamic path screening using current-step C/D/V block magnitudes. Non-positive values disable this feature and preserve the current behavior."; + item.default_value = "-1"; + item.unit = ""; + read_sync_double(input.exx_vcd_threshold); + this->add_item(item); + } + { + Input_Item item("exx_vcd_stats_only"); + item.annotation = "collect dynamic merged-short screening stats only"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Boolean"; + item.description + = "When true, collect C/D-weighted short-screening statistics without skipping any EXX contraction paths. Useful for calibrating the dynamic threshold before enabling it."; + item.default_value = "0"; + item.unit = ""; + read_sync_bool(input.exx_vcd_stats_only); + this->add_item(item); + } + { + Input_Item item("exx_vcd_short_only"); + item.annotation = "apply dynamic screening only to merged-short EXX"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Boolean"; + item.description + = "When true, the C/D-weighted dynamic screen is restricted to the split merged-short EXX channel and does not affect full or long-range channels."; + item.default_value = "1"; + item.unit = ""; + read_sync_bool(input.exx_vcd_short_only); + this->add_item(item); + } { Input_Item item("exx_dm_threshold"); item.annotation = "threshold to screen density matrix in exx"; @@ -463,6 +513,176 @@ void ReadInput::item_exx() }; this->add_item(item); } + { + Input_Item item("exx_ewald_dimension"); + item.annotation = "dimensionality used by the Ewald Coulomb singularity correction"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Integer"; + item.description = "Use 3 for periodic three-dimensional Coulomb and 2 for isolated-layer Coulomb."; + item.default_value = "3"; + item.unit = ""; + read_sync_int(input.exx_ewald_dimension); + item.check_value = [](const Input_Item&, const Parameter& para) { + if (para.input.exx_ewald_dimension != 2 && para.input.exx_ewald_dimension != 3) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_dimension must be 2 or 3"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_lambda"); + item.annotation = "Gaussian decay coefficient for Ewald full Coulomb in RI-EXX"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.description = "This parameter controls the Gaussian auxiliary functions used in the Ewald split of the full Coulomb matrix in RI-EXX. The real-space Gaussian cutoff is proportional to sqrt(35/exx_ewald_lambda)."; + item.default_value = "1.0"; + item.unit = ""; + item.set_availability(""); + read_sync_double(input.exx_ewald_lambda); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_ewald_lambda <= 0) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_lambda must > 0"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_tail_check"); + item.annotation = "Ewald short-range Gaussian-tail handling mode"; + item.category = "Exact Exchange (LCAO)"; + item.type = "String"; + item.description = "Check the outer real-space shell of Vbare-Vgauss. off disables the probe, warn reports without changing the production range, enlarge expands until converged, and strict also aborts on incomplete or unconverged coverage."; + item.default_value = "warn"; + read_sync_string(input.exx_ewald_tail_check); + item.check_value = [](const Input_Item& item, const Parameter& para) { + const std::string& mode = para.input.exx_ewald_tail_check; + if (mode != "off" && mode != "warn" && mode != "enlarge" && mode != "strict") + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_tail_check must be off, warn, enlarge, or strict"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_tail_abs_tol"); + item.annotation = "absolute Ewald short-range guard-shell tolerance"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.default_value = "1e-12"; + read_sync_double(input.exx_ewald_tail_abs_tol); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_ewald_tail_abs_tol < 0.0) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_tail_abs_tol must be non-negative"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_tail_rel_tol"); + item.annotation = "relative Ewald short-range guard-shell maximum tolerance"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.default_value = "1e-10"; + read_sync_double(input.exx_ewald_tail_rel_tol); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_ewald_tail_rel_tol < 0.0) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_tail_rel_tol must be non-negative"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_tail_sum_rel_tol"); + item.annotation = "relative Ewald short-range guard-shell summed tolerance"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.default_value = "1e-8"; + read_sync_double(input.exx_ewald_tail_sum_rel_tol); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_ewald_tail_sum_rel_tol < 0.0) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_tail_sum_rel_tol must be non-negative"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_tail_guard_cells"); + item.annotation = "number of lattice-cell layers in the Ewald tail probe"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Integer"; + item.default_value = "1"; + read_sync_int(input.exx_ewald_tail_guard_cells); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_ewald_tail_guard_cells < 1) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_tail_guard_cells must be at least one"); + } + }; + this->add_item(item); + } + { + Input_Item item("exx_ewald_tail_max_expansions"); + item.annotation = "maximum adaptive Ewald real-space range expansions"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Integer"; + item.default_value = "3"; + read_sync_int(input.exx_ewald_tail_max_expansions); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_ewald_tail_max_expansions < 0) + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_ewald_tail_max_expansions must be non-negative"); + } + }; + this->add_item(item); + } + { + Input_Item item("rpa_abfs_preorth"); + item.annotation = "on-site Coulomb-metric preorthogonalization for RPA auxiliary bases"; + item.category = "Exact Exchange (LCAO)"; + item.type = "String"; + item.description = "Select none for the legacy RPA auxiliary basis or onsite_coulomb to Coulomb-orthonormalize each atom-type and angular-momentum radial channel before RPA producer output."; + item.default_value = "none"; + item.unit = ""; + item.set_availability("basis_type==lcao"); + read_sync_string(input.rpa_abfs_preorth); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.rpa_abfs_preorth != "none" + && para.input.rpa_abfs_preorth != "onsite_coulomb") + { + ModuleBase::WARNING_QUIT( + "ReadInput", + "rpa_abfs_preorth must be none or onsite_coulomb"); + } + }; + this->add_item(item); + } + { + Input_Item item("rpa_abfs_preorth_threshold"); + item.annotation = "residual Coulomb-norm threshold for RPA auxiliary preorthogonalization"; + item.category = "Exact Exchange (LCAO)"; + item.type = "Real"; + item.description = "Reject a radial auxiliary candidate when its squared residual on-site Coulomb norm is no larger than this value."; + item.default_value = "1e-2"; + item.unit = ""; + item.set_availability("basis_type==lcao and rpa_abfs_preorth==onsite_coulomb"); + read_sync_double(input.rpa_abfs_preorth_threshold); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (!std::isfinite(para.input.rpa_abfs_preorth_threshold) + || para.input.rpa_abfs_preorth_threshold <= 0.0 + || para.input.rpa_abfs_preorth_threshold >= 1.0) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + "rpa_abfs_preorth_threshold must be finite and strictly between 0 and 1"); + } + }; + this->add_item(item); + } { Input_Item item("exx_singularity_correction"); item.annotation = "set the scheme of Coulomb singularity correction"; diff --git a/source/source_io/module_parameter/read_inp_out.cpp b/source/source_io/module_parameter/read_inp_out.cpp index 80a55319466..dd453580f43 100644 --- a/source/source_io/module_parameter/read_inp_out.cpp +++ b/source/source_io/module_parameter/read_inp_out.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include "source_base/formatter.h" #include "source_base/global_function.h" #include "source_base/tool_quit.h" @@ -1447,9 +1451,8 @@ If EXX(exact exchange) is calculated (i.e. dft_fuctional==hse/hf/pbe0/scan0 or r item.category = "Output information"; item.type = "Boolean"; item.description = "Generate output files used in rpa calculations." - "\n\n[NOTE] If symmetry is set to 1, additional files containing the necessary information for " - "exploiting symmetry in the subsequent rpa calculation will be output: " - "irreducible_sector.txt, symrot_k.txt and symrot_R.txt."; + "\n\n[NOTE] If symmetry is set to 1, stru_out.txt also contains the spatial and, for nspin=4, " + "magnetic symmetry operations consumed by the subsequent LibRPA calculation."; item.default_value = "False"; item.unit = ""; item.set_availability("basis_type==lcao"); @@ -1502,6 +1505,178 @@ If EXX(exact exchange) is calculated (i.e. dft_fuctional==hse/hf/pbe0/scan0 or r }; this->add_item(item); } + { + Input_Item item("out_librpa_2d_coulomb_method"); + item.annotation = "strict-2D full Coulomb construction used by LibRPA reader-v1 output"; + item.category = "Output information"; + item.type = "String"; + item.description = "Use ewald for the established split-Ewald output, or direct_mixed_fourier " + "to construct the strict-2D full auxiliary Coulomb matrix as a positive Gram matrix. " + "The direct method is default-off and has no automatic fallback."; + item.default_value = "ewald"; + item.unit = ""; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1 and exx_ewald_dimension==2"); + read_sync_string(input.out_librpa_2d_coulomb_method); + item.check_value = [](const Input_Item& item, const Parameter& para) { + const auto& method = para.input.out_librpa_2d_coulomb_method; + if (method != "ewald" && method != "direct_mixed_fourier") + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " supports only ewald or direct_mixed_fourier."); + } + if (method == "direct_mixed_fourier") + { + if (!para.input.rpa || para.input.out_librpa_ver != 1 + || para.input.exx_ewald_dimension != 2) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + "=direct_mixed_fourier requires rpa=1, " + "out_librpa_ver=1, and exx_ewald_dimension=2."); + } + if (!(para.input.out_librpa_2d_direct_ecut > 0.0) + || para.input.out_librpa_2d_direct_kz_order <= 0 + || para.input.out_librpa_2d_direct_gamma_order <= 0 + || para.input.out_librpa_2d_direct_gamma_order % 2 != 0) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + "=direct_mixed_fourier requires positive out_librpa_2d_direct_ecut, " + "positive out_librpa_2d_direct_kz_order, and a positive even " + "out_librpa_2d_direct_gamma_order."); + } + } + }; + this->add_item(item); + } + { + } + { + Input_Item item("out_librpa_2d_direct_ecut"); + item.annotation = "reciprocal cutoff for direct mixed-Fourier strict-2D Coulomb output"; + item.category = "Output information"; + item.type = "Real"; + item.description = "Reciprocal cutoff in Ry for out_librpa_2d_coulomb_method=direct_mixed_fourier. " + "This value must be converged for each auxiliary basis."; + item.default_value = "0"; + item.unit = "Ry"; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1 and exx_ewald_dimension==2 and out_librpa_2d_coulomb_method==direct_mixed_fourier"); + read_sync_double(input.out_librpa_2d_direct_ecut); + this->add_item(item); + } + { + } + { + Input_Item item("out_librpa_2d_direct_kz_order"); + item.annotation = "kz quadrature order for direct mixed-Fourier strict-2D Coulomb output"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Positive transformed-kz quadrature order for the direct strict-2D Coulomb matrix. " + "It must be converged independently of the reciprocal cutoff."; + item.default_value = "0"; + item.unit = ""; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1 and exx_ewald_dimension==2 and out_librpa_2d_coulomb_method==direct_mixed_fourier"); + read_sync_int(input.out_librpa_2d_direct_kz_order); + this->add_item(item); + } + { + } + { + Input_Item item("out_librpa_2d_direct_gamma_order"); + item.annotation = "Gamma-plane quadrature order for direct mixed-Fourier strict-2D Coulomb output"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Positive even in-plane quadrature order for the Gamma-cell Coulomb average."; + item.default_value = "8"; + item.unit = ""; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1 and exx_ewald_dimension==2 and out_librpa_2d_coulomb_method==direct_mixed_fourier"); + read_sync_int(input.out_librpa_2d_direct_gamma_order); + this->add_item(item); + } + { + } + { + Input_Item item("out_librpa_3d_coulomb_method"); + item.annotation = "3D full Coulomb construction used by LibRPA reader-v1 output"; + item.category = "Output information"; + item.type = "String"; + item.description = "Use ewald for the established split-Ewald output, or direct_reciprocal " + "to construct the 3D full auxiliary Coulomb matrix as a positive Gram matrix. " + "The direct method is default-off and has no automatic fallback."; + item.default_value = "ewald"; + item.unit = ""; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1 and exx_ewald_dimension==3"); + read_sync_string(input.out_librpa_3d_coulomb_method); + item.check_value = [](const Input_Item& item, const Parameter& para) { + const auto& method = para.input.out_librpa_3d_coulomb_method; + if (method != "ewald" && method != "direct_reciprocal") + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " supports only ewald or direct_reciprocal."); + } + if (method == "direct_reciprocal") + { + if (!para.input.rpa || para.input.out_librpa_ver != 1 + || para.input.exx_ewald_dimension != 3) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + "=direct_reciprocal requires rpa=1, " + "out_librpa_ver=1, and exx_ewald_dimension=3."); + } + if (!(para.input.out_librpa_3d_direct_ecut > 0.0)) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + "=direct_reciprocal requires positive out_librpa_3d_direct_ecut."); + } + } + }; + this->add_item(item); + } + { + } + { + Input_Item item("out_librpa_3d_direct_ecut"); + item.annotation = "reciprocal cutoff for direct 3D Coulomb output"; + item.category = "Output information"; + item.type = "Real"; + item.description = "Reciprocal cutoff in Ry for " + "out_librpa_3d_coulomb_method=direct_reciprocal. " + "This value must be converged for each auxiliary basis."; + item.default_value = "0"; + item.unit = "Ry"; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1 and exx_ewald_dimension==3 and out_librpa_3d_coulomb_method==direct_reciprocal"); + read_sync_double(input.out_librpa_3d_direct_ecut); + this->add_item(item); + } + { + } + { + Input_Item item("out_librpa_abf_overlap"); + item.annotation = "output raw active-ABF overlap for LibRPA v1 PSD diagnostics"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "Write raw active-ABF q-space overlap matrices as " + "v1_abf_overlap_active_iq_.dat. This diagnostic requires " + "rpa=true, out_librpa_ver=1, and a shrink/active ABF " + "lifecycle; full-unshrunk overlap output is not provided."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao and rpa==true and out_librpa_ver==1"); + read_sync_bool(input.out_librpa_abf_overlap); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.out_librpa_abf_overlap + && (!para.input.rpa || para.input.out_librpa_ver != 1 + || para.input.shrink_abfs_pca_thr < 0.0)) + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " requires rpa=true, " + "out_librpa_ver=1, and shrink ABFs."); + } + }; + this->add_item(item); + } { Input_Item item("out_pchg"); item.annotation = "specify the bands to be calculated for the partial (band-decomposed) charge densities"; diff --git a/source/source_io/module_restart/restart_exx_csr.cpp b/source/source_io/module_restart/restart_exx_csr.cpp index 2c7db4a85cd..fd92aa92622 100644 --- a/source/source_io/module_restart/restart_exx_csr.cpp +++ b/source/source_io/module_restart/restart_exx_csr.cpp @@ -3,6 +3,8 @@ #include "source_base/global_function.h" #include "source_cell/unitcell.h" #include "source_base/module_out/csr_reader.h" +#include "source_base/module_out/sparse_matrix.h" +#include "source_cell/ucell_io.h" #include "source_io/module_hs/hs_sparse_io.h" #include "source_lcao/module_ri/serialization_cereal.h" #include @@ -120,8 +122,7 @@ namespace ModuleIO const std::vector>>>& Hexxs) { ModuleBase::TITLE("ModuleIO", "write_Hexxs_csr"); - std::set> all_R_coor; - double sparse_threshold = 1e-10; + const double sparse_threshold = 1e-10; int matrix_dimension = 0; for (int it = 0; it < ucell.ntype; ++it) { @@ -132,32 +133,58 @@ namespace ModuleIO ModuleBase::WARNING_QUIT("ModuleIO::write_Hexxs_csr", "LCAO matrix dimension must be positive."); } - Parallel_Orbitals pv; - pv.set_serial(matrix_dimension, matrix_dimension); + // The Hexx restart files must round-trip through read_Hexxs_csr, whose + // csrFileReader parses the sparse-file layout shared with the DM(R) + // restart files: header, lattice block, then CSR blocks per R. The + // bare save_sparse header cannot be read back. for (int is = 0;is < Hexxs.size();++is) { + std::set> all_R_coor; for (const auto& HexxA : Hexxs[is]) { - const int iat0 = HexxA.first; for (const auto& HexxB : HexxA.second) { - const int iat1 = HexxB.first.first; - const Abfs::Vector3_Order R = RI_Util::array3_to_Vector3(HexxB.first.second); - all_R_coor.insert(R); + all_R_coor.insert(RI_Util::array3_to_Vector3(HexxB.first.second)); } } - ModuleIO::SparseWriteOptions options; - options.filename = file_name + "_" + std::to_string(is) + ".csr"; - options.label = "Hexxs_" + std::to_string(is); - options.threshold = sparse_threshold; - options.binary = false; - options.istep = -1; - options.reduce = false; - ModuleIO::save_sparse( - calculate_RI_Tensor_sparse(sparse_threshold, Hexxs[is], ucell), - all_R_coor, - pv, - options); + std::ofstream ofs(file_name + "_" + std::to_string(is) + ".csr"); + if (!ofs.is_open()) + { + ModuleBase::WARNING_QUIT("ModuleIO::write_Hexxs_csr", + "cannot open " + file_name + "_" + std::to_string(is) + ".csr"); + } + ofs << " --- Ionic Step 1 ---" << std::endl; + ofs << " # print exact exchange matrix in real space Hexxs_" << is << "(R)" << std::endl; + ofs << " " << Hexxs.size() << " # number of spin directions" << std::endl; + ofs << " " << is + 1 << " # spin index" << std::endl; + ofs << " " << matrix_dimension << " # number of localized basis" << std::endl; + ofs << " " << all_R_coor.size() << " # number of Bravais lattice vector R" << std::endl; + ofs << std::endl; + ModuleIO::UcellIO::write_ucell(ofs, &ucell); + ofs << std::endl; + + const std::map, std::map>> sparse_by_R + = calculate_RI_Tensor_sparse(sparse_threshold, Hexxs[is], ucell); + for (const auto& R : all_R_coor) + { + ModuleIO::SparseMatrix sparse_matrix(matrix_dimension, matrix_dimension); + sparse_matrix.setSparseThreshold(sparse_threshold); + const auto itR = sparse_by_R.find(R); + if (itR != sparse_by_R.end()) + { + for (const auto& row_cols : itR->second) + { + for (const auto& col_val : row_cols.second) + { + sparse_matrix.insert(row_cols.first, col_val.first, col_val.second); + } + } + } + ofs << " " << R.x << " " << R.y << " " << R.z << " " + << sparse_matrix.getNNZ() << std::endl; + sparse_matrix.printToCSR(ofs, 8); + } + ofs.close(); } } diff --git a/source/source_io/test/CMakeLists.txt b/source/source_io/test/CMakeLists.txt index 6cfbecf5089..d32283bd7e6 100644 --- a/source/source_io/test/CMakeLists.txt +++ b/source/source_io/test/CMakeLists.txt @@ -257,6 +257,7 @@ AddTest( ../module_restart/restart_exx_csr.cpp ../module_hs/hs_sparse_io.cpp ../module_hs/lat_r_csr.cpp + ../../source_cell/ucell_io.cpp tmp_mocks.cpp ) endif() diff --git a/source/source_io/test/read_input_ptest.cpp b/source/source_io/test/read_input_ptest.cpp index ab407175979..d24ec5cfcf9 100644 --- a/source/source_io/test/read_input_ptest.cpp +++ b/source/source_io/test/read_input_ptest.cpp @@ -306,9 +306,19 @@ TEST_F(InputParaTest, ParaRead) EXPECT_DOUBLE_EQ(param.inp.exx_v_grad_threshold, 0); EXPECT_DOUBLE_EQ(param.inp.exx_c_grad_r_threshold, 0); EXPECT_DOUBLE_EQ(param.inp.exx_v_grad_r_threshold, 0); + EXPECT_DOUBLE_EQ(param.inp.exx_v_threshold_long, 0.123); EXPECT_EQ(param.inp.exx_ccp_rmesh_times, "1.5"); + EXPECT_DOUBLE_EQ(param.inp.exx_ewald_lambda, 0.25); + EXPECT_EQ(param.inp.exx_ewald_tail_check, "enlarge"); + EXPECT_DOUBLE_EQ(param.inp.exx_ewald_tail_abs_tol, 2e-12); + EXPECT_DOUBLE_EQ(param.inp.exx_ewald_tail_rel_tol, 3e-10); + EXPECT_DOUBLE_EQ(param.inp.exx_ewald_tail_sum_rel_tol, 4e-8); + EXPECT_EQ(param.inp.exx_ewald_tail_guard_cells, 2); + EXPECT_EQ(param.inp.exx_ewald_tail_max_expansions, 4); EXPECT_DOUBLE_EQ(param.inp.rpa_ccp_rmesh_times, 10.0); EXPECT_EQ(param.inp.out_librpa_ver, 1); + EXPECT_EQ(param.inp.out_librpa_3d_coulomb_method, "ewald"); + EXPECT_DOUBLE_EQ(param.inp.out_librpa_3d_direct_ecut, 110.0); EXPECT_EQ(param.inp.exx_opt_orb_lmax, 0); EXPECT_DOUBLE_EQ(param.inp.exx_opt_orb_ecut, 0.0); EXPECT_DOUBLE_EQ(param.inp.exx_opt_orb_tolerence, 0.0); diff --git a/source/source_io/test/support/INPUT b/source/source_io/test/support/INPUT index 8e8d3902c9c..a3c050cdce7 100644 --- a/source/source_io/test/support/INPUT +++ b/source/source_io/test/support/INPUT @@ -288,8 +288,18 @@ exx_c_grad_threshold 0 # exx_v_grad_threshold 0 # exx_c_grad_r_threshold 0 # exx_v_grad_r_threshold 0 # +exx_v_threshold_long 0.123 # exx_ccp_rmesh_times default # out_librpa_ver 1 # +exx_ewald_lambda 0.25 # +exx_ewald_tail_check enlarge # +exx_ewald_tail_abs_tol 2e-12 # +exx_ewald_tail_rel_tol 3e-10 # +exx_ewald_tail_sum_rel_tol 4e-8 # +exx_ewald_tail_guard_cells 2 # +exx_ewald_tail_max_expansions 4 # +out_librpa_3d_coulomb_method ewald # +out_librpa_3d_direct_ecut 110.0 # Ry exx_opt_orb_lmax 0 # exx_opt_orb_ecut 0 # exx_opt_orb_tolerence 0 # diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index b0131e215e9..53a8b55b4f0 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -1889,6 +1889,109 @@ TEST_F(InputTest, Item_test2) output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, testing::HasSubstr("NOTICE")); } + { // rpa_abfs_preorth + EXPECT_EQ(TestParameters::input(param).rpa_abfs_preorth, "none"); + auto it = find_label("rpa_abfs_preorth", readinput.input_lists); + ASSERT_NE(it, readinput.input_lists.end()); + it->second.str_values = {"onsite_coulomb"}; + it->second.read_value(it->second, param); + EXPECT_EQ(TestParameters::input(param).rpa_abfs_preorth, "onsite_coulomb"); + + TestParameters::input(param).rpa_abfs_preorth = "invalid"; + testing::internal::CaptureStdout(); + EXPECT_EXIT(it->second.check_value(it->second, param), ::testing::ExitedWithCode(1), ""); + output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, testing::HasSubstr("rpa_abfs_preorth")); + TestParameters::input(param).rpa_abfs_preorth = "none"; + } + { // rpa_abfs_preorth_threshold + EXPECT_DOUBLE_EQ(TestParameters::input(param).rpa_abfs_preorth_threshold, 1.0e-2); + auto it = find_label("rpa_abfs_preorth_threshold", readinput.input_lists); + ASSERT_NE(it, readinput.input_lists.end()); + for (const double invalid : {0.0, 1.0, -1.0, std::numeric_limits::infinity()}) + { + TestParameters::input(param).rpa_abfs_preorth_threshold = invalid; + testing::internal::CaptureStdout(); + EXPECT_EXIT(it->second.check_value(it->second, param), ::testing::ExitedWithCode(1), ""); + output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, testing::HasSubstr("rpa_abfs_preorth_threshold")); + } + TestParameters::input(param).rpa_abfs_preorth_threshold = 1.0e-2; + } + { // out_librpa_2d_coulomb_method and direct quadrature controls + auto method = find_label("out_librpa_2d_coulomb_method", readinput.input_lists); + auto ecut = find_label("out_librpa_2d_direct_ecut", readinput.input_lists); + auto kz_order = find_label("out_librpa_2d_direct_kz_order", readinput.input_lists); + auto gamma_order = find_label("out_librpa_2d_direct_gamma_order", readinput.input_lists); + ASSERT_NE(method, readinput.input_lists.end()); + ASSERT_NE(ecut, readinput.input_lists.end()); + ASSERT_NE(kz_order, readinput.input_lists.end()); + ASSERT_NE(gamma_order, readinput.input_lists.end()); + EXPECT_EQ(TestParameters::input(param).out_librpa_2d_coulomb_method, "ewald"); + EXPECT_DOUBLE_EQ(TestParameters::input(param).out_librpa_2d_direct_ecut, 0.0); + EXPECT_EQ(TestParameters::input(param).out_librpa_2d_direct_kz_order, 0); + EXPECT_EQ(TestParameters::input(param).out_librpa_2d_direct_gamma_order, 8); + + method->second.str_values = {"direct_mixed_fourier"}; + method->second.read_value(method->second, param); + ecut->second.str_values = {"110"}; + ecut->second.read_value(ecut->second, param); + kz_order->second.str_values = {"64"}; + kz_order->second.read_value(kz_order->second, param); + gamma_order->second.str_values = {"8"}; + gamma_order->second.read_value(gamma_order->second, param); + TestParameters::input(param).rpa = true; + TestParameters::input(param).out_librpa_ver = 1; + TestParameters::input(param).exx_ewald_dimension = 2; + method->second.check_value(method->second, param); + EXPECT_EQ(TestParameters::input(param).out_librpa_2d_coulomb_method, "direct_mixed_fourier"); + EXPECT_DOUBLE_EQ(TestParameters::input(param).out_librpa_2d_direct_ecut, 110.0); + EXPECT_EQ(TestParameters::input(param).out_librpa_2d_direct_kz_order, 64); + EXPECT_EQ(TestParameters::input(param).out_librpa_2d_direct_gamma_order, 8); + + const auto expect_direct_failure = [&]() { + testing::internal::CaptureStdout(); + EXPECT_EXIT(method->second.check_value(method->second, param), ::testing::ExitedWithCode(1), ""); + output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, testing::HasSubstr("NOTICE")); + }; + TestParameters::input(param).out_librpa_2d_coulomb_method = "unknown"; + expect_direct_failure(); + TestParameters::input(param).out_librpa_2d_coulomb_method = "direct_mixed_fourier"; + TestParameters::input(param).out_librpa_2d_direct_ecut = 0.0; + expect_direct_failure(); + TestParameters::input(param).out_librpa_2d_direct_ecut = 110.0; + TestParameters::input(param).out_librpa_2d_direct_kz_order = 0; + expect_direct_failure(); + TestParameters::input(param).out_librpa_2d_direct_kz_order = 64; + TestParameters::input(param).out_librpa_2d_direct_gamma_order = 7; + expect_direct_failure(); + TestParameters::input(param).out_librpa_2d_direct_gamma_order = 8; + TestParameters::input(param).exx_ewald_dimension = 3; + expect_direct_failure(); + TestParameters::input(param).exx_ewald_dimension = 2; + TestParameters::input(param).out_librpa_ver = 0; + expect_direct_failure(); + TestParameters::input(param).out_librpa_ver = 1; + TestParameters::input(param).rpa = false; + expect_direct_failure(); + } + { // out_librpa_abf_overlap + auto it = find_label("out_librpa_abf_overlap", readinput.input_lists); + ASSERT_NE(it, readinput.input_lists.end()); + EXPECT_FALSE(TestParameters::input(param).out_librpa_abf_overlap); + it->second.str_values = {"true"}; + it->second.read_value(it->second, param); + EXPECT_TRUE(TestParameters::input(param).out_librpa_abf_overlap); + testing::internal::CaptureStdout(); + EXPECT_EXIT(it->second.check_value(it->second, param), ::testing::ExitedWithCode(1), ""); + output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, testing::HasSubstr("requires rpa=true")); + TestParameters::input(param).rpa = true; + TestParameters::input(param).out_librpa_ver = 1; + TestParameters::input(param).shrink_abfs_pca_thr = 0.0; + EXPECT_NO_THROW(it->second.check_value(it->second, param)); + } { // berry_phase auto it = find_label("berry_phase", readinput.input_lists); TestParameters::input(param).berry_phase = true; diff --git a/source/source_lcao/center2orb.cpp b/source/source_lcao/center2orb.cpp index 1e5a60a337e..862d1e86258 100644 --- a/source/source_lcao/center2orb.cpp +++ b/source/source_lcao/center2orb.cpp @@ -133,11 +133,7 @@ void Center2_Orb::cal_ST_Phi12_R(const int& job, assert(jl.size()>=rmesh); assert(jlp1.size()>=rmesh); -#ifdef _OPENMP -#pragma omp parallel for schedule(static) -#endif - for (int ir = 0; ir < rmesh; ir++) - { + auto accumulate_radial = [&](const int ir) { std::vector integrated_func(kmesh); const std::vector& jl_r = jl[ir]; assert(jl_r.size()>=kmesh); @@ -174,6 +170,14 @@ void Center2_Orb::cal_ST_Phi12_R(const int& job, ModuleBase::Integral::Simpson_Integral(kmesh, integrated_func.data(), dk, temp); drs[ir] = -ModuleBase::FOUR_PI * (l + 1) / (2.0 * l + 1) * temp; + }; + +#ifdef _OPENMP +#pragma omp parallel for schedule(static) +#endif + for (int ir = 0; ir < rmesh; ir++) + { + accumulate_radial(ir); } // liaochen modify on 2010/4/22 diff --git a/source/source_lcao/module_operator_lcao/ekinetic.cpp b/source/source_lcao/module_operator_lcao/ekinetic.cpp index cd7ae91083f..000f08f7721 100644 --- a/source/source_lcao/module_operator_lcao/ekinetic.cpp +++ b/source/source_lcao/module_operator_lcao/ekinetic.cpp @@ -22,6 +22,12 @@ hamilt::EKinetic>::EKinetic( this->ucell = ucell_in; #ifdef __DEBUG assert(this->ucell != nullptr); + assert(this->intor_ != nullptr); + // Force/stress code paths construct EKinetic without k-space matrices. + if (hR_in != nullptr) + { + assert(this->hR != nullptr); + } #endif // initialize HR to allocate sparse Ekinetic matrix memory // Only initialize if hR_in is not nullptr (for force calculation, hR_in can be nullptr) diff --git a/source/source_lcao/module_operator_lcao/op_exx_lcao.cpp b/source/source_lcao/module_operator_lcao/op_exx_lcao.cpp index 5a6c6769830..192cbfa4910 100644 --- a/source/source_lcao/module_operator_lcao/op_exx_lcao.cpp +++ b/source/source_lcao/module_operator_lcao/op_exx_lcao.cpp @@ -175,6 +175,12 @@ OperatorEXX>::OperatorEXX(HS_Matrix_K* hsk_in, this->restart = restart_in; ModuleBase::TITLE("OperatorEXX", "OperatorEXX"); const Parallel_Orbitals* const pv = hR_in->get_paraV(); + const bool zero_koffset + = (ModuleBase::Vector3(std::fmod(this->kv.get_koffset(0), 1.0), + std::fmod(this->kv.get_koffset(1), 1.0), + std::fmod(this->kv.get_koffset(2), 1.0)) + .norm() + < 1e-10); if (PARAM.inp.calculation == "nscf" && exx_info_ptr->info_global.cal_exx) { // for nscf, calculate HexxR from the read-in DM, or read HexxR in @@ -209,6 +215,38 @@ OperatorEXX>::OperatorEXX(HS_Matrix_K* hsk_in, } return true; }; + auto maybe_remap_wigner_seitz = [&](auto& Hexxs_loaded) { + std::array Rs_period = {this->kv.nmp[0], this->kv.nmp[1], this->kv.nmp[2]}; + const bool period_from_kmesh = Rs_period[0] > 0 && Rs_period[1] > 0 && Rs_period[2] > 0; + if (!can_remap_wigner_seitz_for_nscf(this->add_hexx_type, period_from_kmesh, zero_koffset)) + { + return; + } + if (!period_from_kmesh) + { + const auto inferred = infer_complete_Rs_period_from_Hexxs(Hexxs_loaded); + if (!inferred.first) + { + if (GlobalV::MY_RANK == 0) + { + ModuleBase::WARNING( + "OperatorEXX", + "Cannot infer a complete BvK period; HexxR Wigner-Seitz remapping is disabled"); + } + return; + } + Rs_period = inferred.second; + } + + const WignerSeitzRemapStats stats = remap_Hexxs_wigner_seitz(ucell, Rs_period, Hexxs_loaded); + if (GlobalV::MY_RANK == 0) + { + std::cout << " NSCF EXX Wigner-Seitz remapping enabled with R period: " << Rs_period[0] << " " + << Rs_period[1] << " " << Rs_period[2] << "; input blocks=" << stats.input_blocks + << "; output blocks=" << stats.output_blocks << "; remapped blocks=" << stats.remapped_blocks + << "; split blocks=" << stats.split_blocks << "; max images=" << stats.max_images << std::endl; + } + }; if (PARAM.inp.init_chg == "dm" || PARAM.inp.init_chg == "dm_no_renormalize") { @@ -275,10 +313,12 @@ OperatorEXX>::OperatorEXX(HS_Matrix_K* hsk_in, if (exx_info_ptr->info_ri.real_number) { ModuleIO::read_Hexxs_csr(file_name_exx_csr, ucell, PARAM.inp.nspin, PARAM.globalv.nlocal, *Hexxd); + maybe_remap_wigner_seitz(*Hexxd); } else { ModuleIO::read_Hexxs_csr(file_name_exx_csr, ucell, PARAM.inp.nspin, PARAM.globalv.nlocal, *Hexxc); + maybe_remap_wigner_seitz(*Hexxc); } } else if (check_exist(file_name_list_cereal())) @@ -294,10 +334,12 @@ OperatorEXX>::OperatorEXX(HS_Matrix_K* hsk_in, if (exx_info_ptr->info_ri.real_number) { ModuleIO::read_Hexxs_cereal(file_name_exx_cereal, *Hexxd); + maybe_remap_wigner_seitz(*Hexxd); } else { ModuleIO::read_Hexxs_cereal(file_name_exx_cereal, *Hexxc); + maybe_remap_wigner_seitz(*Hexxc); } } else diff --git a/source/source_lcao/module_operator_lcao/op_exx_lcao.h b/source/source_lcao/module_operator_lcao/op_exx_lcao.h index c1e792aa277..a39955211a9 100644 --- a/source/source_lcao/module_operator_lcao/op_exx_lcao.h +++ b/source/source_lcao/module_operator_lcao/op_exx_lcao.h @@ -10,6 +10,15 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include + // Forward declaration to avoid circular include (exx_lri_interface.hpp includes op_exx_lcao.h) template class Exx_LRI_Interface; @@ -107,6 +116,187 @@ class OperatorEXX> : public OperatorLCAO using TAC = std::pair>; +template +inline std::pair> infer_complete_Rs_period_from_Hexxs( + const std::vector>>>& Hexxs) +{ + std::array Rs_period = {0, 0, 0}; + if (Hexxs.empty()) + { + return {false, Rs_period}; + } + + std::set> unique_Rs; + std::array min_R = { + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + std::array max_R = { + std::numeric_limits::min(), + std::numeric_limits::min(), + std::numeric_limits::min()}; + + for (const auto& Htmp1 : Hexxs[0]) + { + for (const auto& Htmp2 : Htmp1.second) + { + const std::array& cell = Htmp2.first.second; + if (unique_Rs.insert(cell).second) + { + for (int idim = 0; idim < 3; ++idim) + { + min_R[idim] = std::min(min_R[idim], cell[idim]); + max_R[idim] = std::max(max_R[idim], cell[idim]); + } + } + } + } + + if (unique_Rs.empty()) + { + return {false, Rs_period}; + } + + std::size_t expected_nR = 1; + for (int idim = 0; idim < 3; ++idim) + { + Rs_period[idim] = max_R[idim] - min_R[idim] + 1; + if (Rs_period[idim] <= 0) + { + return {false, {0, 0, 0}}; + } + expected_nR *= static_cast(Rs_period[idim]); + } + + if (expected_nR != unique_Rs.size()) + { + return {false, {0, 0, 0}}; + } + + return {true, Rs_period}; +} + +inline bool can_remap_wigner_seitz_for_nscf(const Add_Hexx_Type add_hexx_type, + const bool period_from_kmesh, + const bool zero_koffset) +{ + return add_hexx_type == Add_Hexx_Type::R && (!period_from_kmesh || zero_koffset); +} + +struct WignerSeitzRemapStats +{ + std::size_t input_blocks = 0; + std::size_t output_blocks = 0; + std::size_t remapped_blocks = 0; + std::size_t split_blocks = 0; + std::size_t max_images = 0; +}; + +inline std::vector> find_nearest_bvk_cells(const UnitCell& ucell, + const int iat0, + const int iat1, + const std::array& R, + const std::array& period) +{ + // Boundary-equivalent BvK images must all carry the same matrix weight. + double min_distance_sq = std::numeric_limits::max(); + std::vector> nearest_cells; + + for (int ix = -1; ix <= 1; ++ix) + { + for (int iy = -1; iy <= 1; ++iy) + { + for (int iz = -1; iz <= 1; ++iz) + { + const std::array candidate = { + R[0] + ix * period[0], R[1] + iy * period[1], R[2] + iz * period[2]}; + const ModuleBase::Vector3 candidate_vector(candidate[0], candidate[1], candidate[2]); + const double distance_sq = ucell.cal_dtau(iat0, iat1, candidate_vector).norm2(); + const double tolerance = 1.0e-10 + * std::max(1.0, + std::max(std::abs(distance_sq), std::abs(min_distance_sq))); + + if (distance_sq + tolerance < min_distance_sq) + { + min_distance_sq = distance_sq; + nearest_cells.clear(); + nearest_cells.push_back(candidate); + } + else if (std::abs(distance_sq - min_distance_sq) <= tolerance) + { + nearest_cells.push_back(candidate); + } + } + } + } + return nearest_cells; +} + +template +inline WignerSeitzRemapStats remap_Hexxs_wigner_seitz( + const UnitCell& ucell, + const std::array& period, + std::vector>>>& Hexxs) +{ + WignerSeitzRemapStats stats; + std::vector>>> remapped(Hexxs.size()); + + for (std::size_t is = 0; is < Hexxs.size(); ++is) + { + for (const auto& Htmp1 : Hexxs[is]) + { + const int iat0 = Htmp1.first; + for (const auto& Htmp2 : Htmp1.second) + { + ++stats.input_blocks; + const int iat1 = Htmp2.first.first; + const std::array& R = Htmp2.first.second; + const auto nearest_cells = find_nearest_bvk_cells(ucell, iat0, iat1, R, period); + if (nearest_cells.empty()) + { + throw std::runtime_error("No nearest BvK image found for HexxR block"); + } + + stats.max_images = std::max(stats.max_images, nearest_cells.size()); + if (nearest_cells.size() != 1 || nearest_cells.front() != R) + { + ++stats.remapped_blocks; + } + if (nearest_cells.size() > 1) + { + ++stats.split_blocks; + } + + const Tdata weight = static_cast(1.0 / static_cast(nearest_cells.size())); + auto& target = remapped[is][iat0]; + for (const auto& R_bvk : nearest_cells) + { + const TAC key = {iat1, R_bvk}; + auto it = target.find(key); + if (it == target.end()) + { + target.emplace(key, weight * Htmp2.second); + } + else + { + it->second += weight * Htmp2.second; + } + } + } + } + } + + Hexxs.swap(remapped); + for (const auto& Hspin : Hexxs) + { + for (const auto& Htmp1 : Hspin) + { + stats.output_blocks += Htmp1.second.size(); + } + } + return stats; +} + RI::Cell_Nearest init_cell_nearest(const UnitCell& ucell, const std::array& Rs_period); // allocate according to the read-in HexxR, used in nscf @@ -124,4 +314,4 @@ void reallocate_hcontainer(const int nat, } // namespace hamilt #endif // __EXX -#endif // OPEXXLCAO_H \ No newline at end of file +#endif // OPEXXLCAO_H diff --git a/source/source_lcao/module_operator_lcao/operator_lcao.cpp b/source/source_lcao/module_operator_lcao/operator_lcao.cpp index f2e0e24ad67..ea70a6b3134 100644 --- a/source/source_lcao/module_operator_lcao/operator_lcao.cpp +++ b/source/source_lcao/module_operator_lcao/operator_lcao.cpp @@ -14,6 +14,9 @@ #include "source_lcao/module_rt/td_info.h" #include "source_lcao/module_rt/td_folding.h" +#include +#include + namespace hamilt { template @@ -30,6 +33,21 @@ void OperatorLCAO::bind_td_info(TD_info* info) } } +namespace +{ +bool operator_lcao_debug_dump_exx_ao_enabled() +{ + const char* env = std::getenv("ABACUS_DUMP_EXX_AO"); + if (env == nullptr) + { + return false; + } + const std::string value(env); + return !(value.empty() || value == "0" || value == "f" || value == "F" + || value == "false" || value == "FALSE"); +} +} + template <> void OperatorLCAO::get_hs_pointers() { ModuleBase::timer::start("OperatorLCAO", "get_hs_pointers"); @@ -211,6 +229,12 @@ void OperatorLCAO::init(const int ik_in) { { this->contributeHR(); } + if(PARAM.inp.esolver_type == "tddft" + || (PARAM.inp.calculation == "nscf" + && operator_lcao_debug_dump_exx_ao_enabled())) + { + this->contributeHk(ik_in); + } break; } diff --git a/source/source_lcao/module_operator_lcao/overlap.cpp b/source/source_lcao/module_operator_lcao/overlap.cpp index 417e69b9e51..8a9f9c36339 100644 --- a/source/source_lcao/module_operator_lcao/overlap.cpp +++ b/source/source_lcao/module_operator_lcao/overlap.cpp @@ -110,6 +110,12 @@ Overlap>::Overlap(HS_Matrix_K* hsk_in, this->SR = SR_in; #ifdef __DEBUG assert(this->ucell != nullptr); + assert(this->intor_ != nullptr); + // Force/stress code paths construct Overlap without a stored S(R) container. + if (SR_in != nullptr) + { + assert(this->SR != nullptr); + } #endif // Initialize SR to allocate sparse overlap matrix memory. // Only initialize if SR_in is not nullptr (for force calculation, SR_in can be nullptr). diff --git a/source/source_lcao/module_operator_lcao/test/CMakeLists.txt b/source/source_lcao/module_operator_lcao/test/CMakeLists.txt index dbd321da8f7..204949183ad 100644 --- a/source/source_lcao/module_operator_lcao/test/CMakeLists.txt +++ b/source/source_lcao/module_operator_lcao/test/CMakeLists.txt @@ -93,6 +93,34 @@ AddTest( tmp_mocks.cpp ../../../source_hamilt/operator.cpp ) +if(ENABLE_LIBRI) +AddTest( + TARGET MODULE_LCAO_operator_exx_nscf_cell_nearest_test + LIBS parameter psi base device container cereal::cereal ri + SOURCES test_op_exx_nscf_cell_nearest.cpp ../op_exx_lcao.cpp ../../../source_hamilt/module_hcontainer/func_folding.cpp + ../../../source_hamilt/module_hcontainer/base_matrix.cpp ../../../source_hamilt/module_hcontainer/hcontainer.cpp ../../../source_hamilt/module_hcontainer/atom_pair.cpp + ../../../source_hamilt/module_hcontainer/read_hcontainer.cpp + ../../../source_basis/module_ao/parallel_orbitals.cpp + ../../../source_basis/module_ao/orb_atomic_lm.cpp + ../../../source_basis/module_ao/orb_gaunt_table.cpp + ../../lcao_set_mat2d.cpp + ../../center2orb.cpp + ../../center2orb_orb11.cpp + ../../center2orb_orb21.cpp + ../../center2orb_orb22.cpp + ../../../source_basis/module_ao/orb_read.cpp + ../../../source_basis/module_ao/elem_basis_idx_orb.cpp + ../../../source_io/module_hs/hs_sparse_io.cpp + ../../../source_io/module_hs/lat_r_csr.cpp + ../../../source_hamilt/module_hcontainer/func_transfer.cpp + ../../../source_hamilt/module_hcontainer/transfer.cpp + ../../../source_io/module_restart/restart.cpp + ../../../source_io/module_restart/restart_exx_csr.cpp + ../../../source_cell/ucell_io.cpp + tmp_mocks.cpp ../../../source_hamilt/operator.cpp +) +endif() + install(FILES parallel_operator_tests.sh DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) find_program(BASH bash) add_test(NAME MODULE_LCAO_operators_para_test diff --git a/source/source_lcao/module_operator_lcao/test/test_op_exx_nscf_cell_nearest.cpp b/source/source_lcao/module_operator_lcao/test/test_op_exx_nscf_cell_nearest.cpp new file mode 100644 index 00000000000..8e45bb0ec4f --- /dev/null +++ b/source/source_lcao/module_operator_lcao/test/test_op_exx_nscf_cell_nearest.cpp @@ -0,0 +1,253 @@ +#include "../op_exx_lcao.h" +#include "source_io/module_restart/restart_exx_csr.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include + +namespace +{ + +std::string make_test_dir() +{ + const std::string dir = "./op_exx_nscf_cell_nearest_tmp"; + std::system(("rm -rf " + dir).c_str()); + std::system(("mkdir -p " + dir).c_str()); + return dir; +} + +// The csrFileReader on develop expects the full sparse-file header (title, +// spin lines, dimension and R counts, lattice block) before the CSR blocks, +// in the shape produced by the SR.csr fixture of source_base tests. +void write_csr_header(std::ofstream& ofs) +{ + ofs << " --- Ionic Step 0 ---\n"; + ofs << " # minimal HexxR fixture for the cell-nearest operator test\n"; + ofs << " 1 # number of spin directions\n"; + ofs << " 1 # spin index\n"; + ofs << " 2 # number of localized basis\n"; + ofs << " 2 # number of Bravais lattice vector R\n"; + ofs << "\n"; + ofs << " user_defined_lattice\n"; + ofs << " 1\n"; + ofs << " 1 0 0\n"; + ofs << " 0 1 0\n"; + ofs << " 0 0 1\n"; + ofs << " X\n"; + ofs << " 2\n"; + ofs << " Direct\n"; + ofs << " 0 0 0\n"; + ofs << " 0.9 0 0\n"; +} + +void write_csr_r_block(std::ofstream& ofs, const int rx, const int ry, const int rz, const double value) +{ + ofs << " " << rx << " " << ry << " " << rz << " 1\n"; + ofs << " # CSR values\n " << value << "\n"; + ofs << " # CSR column indices\n 1\n"; + ofs << " # CSR row pointers\n 0 1 1\n"; + ofs << "\n"; +} + +void write_minimal_hexx_csr(const std::string& dir) +{ + std::system(("mkdir -p " + dir).c_str()); + std::ofstream ofs(dir + "/HexxR0_0.csr"); + write_csr_header(ofs); + write_csr_r_block(ofs, 0, 0, 0, 1.0); + write_csr_r_block(ofs, 1, 0, 0, 2.0); +} + +void write_centered_hexx_csr(const std::string& dir) +{ + std::system(("mkdir -p " + dir).c_str()); + std::ofstream ofs(dir + "/HexxR0_0.csr"); + write_csr_header(ofs); + write_csr_r_block(ofs, -1, 0, 0, 1.0); + write_csr_r_block(ofs, 0, 0, 0, 2.0); +} + +} // namespace + +class OperatorExxNscfCellNearestTest : public ::testing::Test +{ + protected: + void SetUp() override + { +#ifdef __MPI + MPI_Comm_size(MPI_COMM_WORLD, &world_size_); + MPI_Comm_rank(MPI_COMM_WORLD, &world_rank_); +#endif + + dir_ = make_test_dir(); + write_minimal_hexx_csr(dir_); + + ucell_.ntype = 1; + ucell_.nat = 2; + ucell_.atoms = new Atom[ucell_.ntype]; + ucell_.iat2it.resize(ucell_.nat); + ucell_.iat2ia.resize(ucell_.nat); + ucell_.itia2iat.create(ucell_.ntype, ucell_.nat); + ucell_.atoms[0].na = 2; + ucell_.atoms[0].nw = 1; + ucell_.atoms[0].tau.resize(ucell_.nat); + ucell_.atoms[0].tau[0] = ModuleBase::Vector3(0.0, 0.0, 0.0); + ucell_.atoms[0].tau[1] = ModuleBase::Vector3(0.9, 0.0, 0.0); + ucell_.a1 = ModuleBase::Vector3(1.0, 0.0, 0.0); + ucell_.a2 = ModuleBase::Vector3(0.0, 1.0, 0.0); + ucell_.a3 = ModuleBase::Vector3(0.0, 0.0, 1.0); + for (int iat = 0; iat < ucell_.nat; ++iat) + { + ucell_.iat2it[iat] = 0; + ucell_.iat2ia[iat] = iat; + ucell_.itia2iat(0, iat) = iat; + } + ucell_.set_iat2iwt(1); + ucell_.iwt2iat = {0, 1}; + ucell_.iwt2iw = {0, 0}; + + paraV_ = new Parallel_Orbitals(); +#ifdef __MPI + paraV_->init(2, 2, 1, MPI_COMM_WORLD); + paraV_->set_atomic_trace(ucell_.get_iat2iwt(), ucell_.nat, 2); +#endif + + hR_ = new hamilt::HContainer(paraV_); + + } + + void TearDown() override + { + // tmp_mocks have partial UnitCell ownership; process cleanup avoids double free. + } + + int world_size_ = 1; + int world_rank_ = 0; + std::string dir_; + UnitCell ucell_; + Parallel_Orbitals* paraV_ = nullptr; + hamilt::HContainer* hR_ = nullptr; +}; + +TEST_F(OperatorExxNscfCellNearestTest, ReallocatesHexxRWithNearestImageWhenPeriodIsInferredFromFile) +{ + std::vector>>> Hexxd; + ModuleIO::read_Hexxs_csr(dir_ + "/HexxR0", ucell_, 1, 2, Hexxd); + + const auto inferred = hamilt::infer_complete_Rs_period_from_Hexxs(Hexxd); + const std::array expected_period = {2, 1, 1}; + ASSERT_TRUE(inferred.first); + EXPECT_EQ(expected_period, inferred.second); + + const std::array raw_R = {1, 0, 0}; + const auto cell_nearest = hamilt::init_cell_nearest(ucell_, inferred.second); + const auto nearest = cell_nearest.get_cell_nearest_discrete(0, 1, raw_R); + ASSERT_TRUE(nearest != raw_R); + hamilt::reallocate_hcontainer(Hexxd, hR_, &cell_nearest); + + EXPECT_NE(nullptr, hR_->find_matrix(0, 1, nearest[0], nearest[1], nearest[2])); + EXPECT_EQ(nullptr, hR_->find_matrix(0, 1, 1, 0, 0)); +} + +TEST_F(OperatorExxNscfCellNearestTest, InfersPeriodFromCenteredCompleteRsSet) +{ + const std::string centered_dir = dir_ + "/centered"; + write_centered_hexx_csr(centered_dir); + + std::vector>>> Hexxd; + ModuleIO::read_Hexxs_csr(centered_dir + "/HexxR0", ucell_, 1, 2, Hexxd); + + const auto inferred = hamilt::infer_complete_Rs_period_from_Hexxs(Hexxd); + const std::array expected_period = {2, 1, 1}; + ASSERT_TRUE(inferred.first); + EXPECT_EQ(expected_period, inferred.second); +} + +TEST_F(OperatorExxNscfCellNearestTest, WriteHexxsCsrRoundTripsThroughReader) +{ + // read_Hexxs_csr and write_Hexxs_csr must agree on the file layout; the + // reader parses the sparse-file format with the lattice block, so the + // writer has to emit that layout, not the bare save_sparse header. + std::vector>>> Hexxd; + ModuleIO::read_Hexxs_csr(dir_ + "/HexxR0", ucell_, 1, 2, Hexxd); + + // fields only touched by the writer's lattice block + ucell_.latName = "user_defined_lattice"; + ucell_.lat0 = 1.0; + ucell_.latvec = ModuleBase::Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + ucell_.atoms[0].label = "X"; + ucell_.atoms[0].stapos_wf = 0; + ucell_.atoms[0].taud.resize(ucell_.nat); + ucell_.atoms[0].taud[0] = ModuleBase::Vector3(0.0, 0.0, 0.0); + ucell_.atoms[0].taud[1] = ModuleBase::Vector3(0.9, 0.0, 0.0); + + const std::string out_dir = dir_ + "/roundtrip"; + std::system(("mkdir -p " + out_dir).c_str()); + ModuleIO::write_Hexxs_csr(out_dir + "/HexxR0", ucell_, Hexxd); + + std::vector>>> Hexxd2; + ModuleIO::read_Hexxs_csr(out_dir + "/HexxR0", ucell_, 1, 2, Hexxd2); + + ASSERT_EQ(1u, Hexxd2.size()); + ASSERT_EQ(1u, Hexxd2[0].count(0)); + EXPECT_DOUBLE_EQ(1.0, Hexxd2[0].at(0).at({1, {0, 0, 0}})(0, 0)); + EXPECT_DOUBLE_EQ(2.0, Hexxd2[0].at(0).at({1, {1, 0, 0}})(0, 0)); +} + +TEST(OperatorExxNscfCellNearestPolicyTest, UsesInferredPeriodIndependentlyOfPathOffset) +{ + EXPECT_TRUE(hamilt::can_remap_wigner_seitz_for_nscf(hamilt::Add_Hexx_Type::R, false, false)); + EXPECT_TRUE(hamilt::can_remap_wigner_seitz_for_nscf(hamilt::Add_Hexx_Type::R, true, true)); + EXPECT_FALSE(hamilt::can_remap_wigner_seitz_for_nscf(hamilt::Add_Hexx_Type::R, true, false)); + EXPECT_FALSE(hamilt::can_remap_wigner_seitz_for_nscf(hamilt::Add_Hexx_Type::k, false, true)); +} + +TEST_F(OperatorExxNscfCellNearestTest, FindsAllEqualDistanceImagesInZnOPrimitiveCell) +{ + ucell_.a1 = ModuleBase::Vector3(2.292, 2.292, 0.0); + ucell_.a2 = ModuleBase::Vector3(2.292, 0.0, 2.292); + ucell_.a3 = ModuleBase::Vector3(0.0, 2.292, 2.292); + + const auto images = hamilt::find_nearest_bvk_cells(ucell_, 0, 0, {4, 0, 0}, {8, 8, 8}); + + ASSERT_EQ(2, images.size()); + EXPECT_NE(images.end(), std::find(images.begin(), images.end(), std::array{-4, 0, 0})); + EXPECT_NE(images.end(), std::find(images.begin(), images.end(), std::array{4, 0, 0})); +} + +TEST_F(OperatorExxNscfCellNearestTest, SplitsHexxEquallyAcrossWignerSeitzBoundaryImages) +{ + ucell_.a1 = ModuleBase::Vector3(2.292, 2.292, 0.0); + ucell_.a2 = ModuleBase::Vector3(2.292, 0.0, 2.292); + ucell_.a3 = ModuleBase::Vector3(0.0, 2.292, 2.292); + + std::vector>>> Hexxd(1); + RI::Tensor block({1, 1}); + block(0, 0) = 2.0; + Hexxd[0][0][{0, {4, 0, 0}}] = block; + + const auto stats = hamilt::remap_Hexxs_wigner_seitz(ucell_, {8, 8, 8}, Hexxd); + + EXPECT_EQ(1, stats.remapped_blocks); + EXPECT_EQ(1, stats.split_blocks); + EXPECT_EQ(2, stats.max_images); + ASSERT_EQ(2, Hexxd[0][0].size()); + EXPECT_DOUBLE_EQ(1.0, Hexxd[0][0].at({0, {-4, 0, 0}})(0, 0)); + EXPECT_DOUBLE_EQ(1.0, Hexxd[0][0].at({0, {4, 0, 0}})(0, 0)); +} + +int main(int argc, char** argv) +{ +#ifdef __MPI + MPI_Init(&argc, &argv); +#endif + ::testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); +#ifdef __MPI + MPI_Finalize(); +#endif + return result; +} diff --git a/source/source_lcao/module_operator_lcao/test/tmp_mocks.cpp b/source/source_lcao/module_operator_lcao/test/tmp_mocks.cpp index f6ec362f733..7a0f1f83ec0 100644 --- a/source/source_lcao/module_operator_lcao/test/tmp_mocks.cpp +++ b/source/source_lcao/module_operator_lcao/test/tmp_mocks.cpp @@ -193,15 +193,8 @@ void TwoCenterIntegrator::snap(const int itype1, } } -#include "source_basis/module_ao/orb_read.h" -LCAO_Orbitals::LCAO_Orbitals() -{ - this->Phi = new Numerical_Orbital[1]; -} -LCAO_Orbitals::~LCAO_Orbitals() -{ - delete[] Phi; -} +// LCAO_Orbitals ctor/dtor/cutoffs come from the real orb_read.cpp, which is +// in this target's source list; do not stub them here. #include "source_cell/module_neighbor/sltk_grid_driver.h" // mock find_atom() function @@ -294,3 +287,17 @@ int elecstate::TDFieldManager::gauge() const { return gauge_; } + +// Restart (including the GlobalC::restart instance and the out-of-line +// read_file2) comes from the real module_io restart.cpp, which is in this +// target's source list; do not stub it here. + +// op_exx_lcao.cpp reads the global XC functional type inside OperatorEXX +// member functions that the cell-nearest test never enters; satisfy the +// linker without module_xc's PW-dependent objects. +#include "source_hamilt/module_xc/xc_functional.h" +int XC_Functional::func_type = 0; +void XC_Functional::set_xc_type(const std::string xc_func_in) +{ + (void)xc_func_in; +} diff --git a/source/source_lcao/module_rdmft/rdmft.cpp b/source/source_lcao/module_rdmft/rdmft.cpp index 5567140ebc2..523e78e361b 100644 --- a/source/source_lcao/module_rdmft/rdmft.cpp +++ b/source/source_lcao/module_rdmft/rdmft.cpp @@ -168,12 +168,12 @@ void RDMFT::init(Parallel_Orbitals& ParaV_in, if (this->exx_info_->info_ri.real_number) { - Vxc_fromRI_d = new Exx_LRI(this->exx_info_->info_ri); + Vxc_fromRI_d = new Exx_LRI(this->exx_info_->info_ri, make_exx_lri_runtime()); Vxc_fromRI_d->init(MPI_COMM_WORLD, ucell_in,*kv, *orb); } else { - Vxc_fromRI_c = new Exx_LRI>(this->exx_info_->info_ri); + Vxc_fromRI_c = new Exx_LRI>(this->exx_info_->info_ri, make_exx_lri_runtime()); Vxc_fromRI_c->init(MPI_COMM_WORLD, ucell_in,*kv, *orb); } } @@ -434,4 +434,3 @@ template class RDMFT, std::complex>; } - diff --git a/source/source_lcao/module_ri/CMakeLists.txt b/source/source_lcao/module_ri/CMakeLists.txt index 8efd6fea7e9..c77c98e3a0b 100644 --- a/source/source_lcao/module_ri/CMakeLists.txt +++ b/source/source_lcao/module_ri/CMakeLists.txt @@ -8,6 +8,8 @@ if (ENABLE_LIBRI) matrix_orbs22.cpp ri_2d_comm.cpp mix_dmk_2d.cpp + mix_matrix.cpp + direct_2d_coulomb.cpp ) if(ENABLE_LCAO) diff --git a/source/source_lcao/module_ri/abfs_construct_pca.cpp b/source/source_lcao/module_ri/abfs_construct_pca.cpp index 751f9405fa0..262eea81aac 100644 --- a/source/source_lcao/module_ri/abfs_construct_pca.cpp +++ b/source/source_lcao/module_ri/abfs_construct_pca.cpp @@ -1,11 +1,11 @@ #include "abfs_construct_pca.h" -#include "source_base/module_external/lapack_connector.h" -#include "source_base/global_function.h" -#include "source_basis/module_ao/elem_basis_idx_orb.h" -#include "source_base/matrix.h" -#include "source_lcao/module_ri/matrix_orbs11.h" -#include "source_lcao/module_ri/matrix_orbs21.h" +#include "../../source_base/module_external/lapack_connector.h" +#include "../../source_base/global_function.h" +#include "../../source_basis/module_ao/elem_basis_idx_orb.h" +#include "../../source_base/matrix.h" +#include "../../source_lcao/module_ri/matrix_orbs11.h" +#include "../../source_lcao/module_ri/matrix_orbs21.h" #include #include diff --git a/source/source_lcao/module_ri/direct_2d_coulomb.cpp b/source/source_lcao/module_ri/direct_2d_coulomb.cpp new file mode 100644 index 00000000000..952fb95b699 --- /dev/null +++ b/source/source_lcao/module_ri/direct_2d_coulomb.cpp @@ -0,0 +1,1076 @@ +#include "direct_2d_coulomb.h" + +#include "source_base/constants.h" +#include "source_base/module_external/lapack_connector.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Direct mixed-Fourier construction of the strict-2D full Coulomb matrix. +// +// The established Ewald route builds V as (real space) - (Gaussian +// compensation) + (reciprocal) + (tail), several large terms whose difference +// is the small physical matrix; imperfect cancellation can then leave a +// strongly indefinite result for diffuse or high-l auxiliary functions. This +// file instead evaluates one positive representation: for every in-plane +// momentum line q+G_parallel the continuous kz integral becomes a quadrature +// +// V_PQ(q) = sum_n w_n * alpha * 4*pi * Lz/(2*pi) / k_n^2 +// * rho_P(k_n)^* rho_Q(k_n), k_n = (q+G_parallel, kz_n), +// +// whose weights w_n are non-negative, so V is a Hermitian positive- +// semidefinite Gram matrix by construction (accumulated through ZHERK). The +// kz nodes come from a tan-mapped Gauss-Legendre rule; the singular Gamma +// line is replaced by an in-plane cell quadrature constrained to the Massidda +// 1/q cell average. The route is opt-in +// (out_librpa_2d_coulomb_method = direct_mixed_fourier) and converges in the +// reciprocal cutoff plus two quadrature orders rather than cutoff alone. +namespace Direct2dCoulomb +{ +namespace +{ +double column_norm(const ModuleBase::Matrix3& matrix, const int column) +{ + if (column == 0) + { + return std::sqrt(matrix.e11 * matrix.e11 + matrix.e21 * matrix.e21 + matrix.e31 * matrix.e31); + } + if (column == 1) + { + return std::sqrt(matrix.e12 * matrix.e12 + matrix.e22 * matrix.e22 + matrix.e32 * matrix.e32); + } + return std::sqrt(matrix.e13 * matrix.e13 + matrix.e23 * matrix.e23 + matrix.e33 * matrix.e33); +} + +std::string json_escape(const std::string& input) +{ + std::string output; + output.reserve(input.size()); + for (const char c: input) + { + if (c == '\\' || c == '"') + { + output.push_back('\\'); + } + output.push_back(c); + } + return output; +} + +std::complex minus_i_to_power(const int l) +{ + switch ((l % 4 + 4) % 4) + { + case 0: + return {1.0, 0.0}; + case 1: + return {0.0, -1.0}; + case 2: + return {-1.0, 0.0}; + default: + return {0.0, 1.0}; + } +} + +// Four-point cubic interpolation of the auxiliary radial table on its +// uniform dk grid. Requests outside the tabulated range are rejected +// (fail-closed) because the direct route must not silently truncate the +// basis transform at the cutoff sphere boundary. +double interpolate_radial(const RadialTable& radial, const double k_bohr) +{ + if (radial.l < 0 || radial.dk <= 0.0 || radial.values == nullptr || radial.size < 4) + { + throw std::invalid_argument("Direct G-sum Coulomb received an invalid auxiliary radial table."); + } + const double last_supported = radial.dk * static_cast(radial.size - 4); + if (k_bohr < -1.0e-14 || k_bohr > last_supported + 1.0e-12) + { + throw std::out_of_range("Direct G-sum cutoff exceeds the auxiliary reciprocal radial table."); + } + const double position = std::max(0.0, k_bohr) / radial.dk; + const std::size_t iq = static_cast(position); + if (iq > radial.size - 4) + { + return 0.0; + } + const double x0 = position - static_cast(iq); + const double x1 = 1.0 - x0; + const double x2 = 2.0 - x0; + const double x3 = 3.0 - x0; + return x1 * x2 * (radial.values[iq] * x3 + radial.values[iq + 3] * x0) / 6.0 + + x0 * x3 * (radial.values[iq + 1] * x2 - radial.values[iq + 2] * x1) / 2.0; +} + +double associated_legendre(const int l, const int m, const double x) +{ + double pmm = 1.0; + if (m > 0) + { + const double root = std::sqrt(std::max(0.0, 1.0 - x * x)); + double factor = 1.0; + for (int i = 1; i <= m; ++i) + { + pmm *= -factor * root; + factor += 2.0; + } + } + if (l == m) + { + return pmm; + } + double pmmp1 = x * static_cast(2 * m + 1) * pmm; + if (l == m + 1) + { + return pmmp1; + } + double pll = 0.0; + for (int ll = m + 2; ll <= l; ++ll) + { + pll = (static_cast(2 * ll - 1) * x * pmmp1 + - static_cast(ll + m - 1) * pmm) + / static_cast(ll - m); + pmm = pmmp1; + pmmp1 = pll; + } + return pll; +} + +// Real spherical harmonics Y_lm(k_hat) in the same convention as the RI +// auxiliary basis (m>0 pairs stored as cos/sin at l^2+2m-1 / l^2+2m). The +// normalization uses lgamma ratios so high l does not overflow factorials. +std::vector real_spherical_harmonics(const int lmax, + const ModuleBase::Vector3& vector) +{ + if (lmax < 0 || lmax > 30) + { + throw std::invalid_argument("Direct G-sum Coulomb supports auxiliary angular momentum 0 <= l <= 30."); + } + const double radius = vector.norm(); + if (radius <= 1.0e-14) + { + throw std::invalid_argument("Real spherical harmonics require non-zero momentum."); + } + const double cos_theta = std::max(-1.0, std::min(1.0, vector.z / radius)); + const double phi = std::atan2(vector.y, vector.x); + std::vector result(static_cast((lmax + 1) * (lmax + 1)), 0.0); + for (int l = 0; l <= lmax; ++l) + { + const double norm0 = std::sqrt(static_cast(2 * l + 1) / ModuleBase::FOUR_PI); + result[static_cast(l * l)] = norm0 * associated_legendre(l, 0, cos_theta); + for (int m = 1; m <= l; ++m) + { + const double log_ratio = std::lgamma(static_cast(l - m + 1)) + - std::lgamma(static_cast(l + m + 1)); + const double norm = std::sqrt(2.0 * static_cast(2 * l + 1) / ModuleBase::FOUR_PI + * std::exp(log_ratio)); + const double radial = associated_legendre(l, m, cos_theta); + result[static_cast(l * l + 2 * m - 1)] = norm * radial * std::cos(m * phi); + result[static_cast(l * l + 2 * m)] = norm * radial * std::sin(m * phi); + } + } + return result; +} +} // namespace + +ModuleBase::Vector3 cartesian_to_direct( + const ModuleBase::Matrix3& reciprocal, + const ModuleBase::Vector3& cartesian) +{ + if (std::abs(reciprocal.Det()) <= 1.0e-14) + { + throw std::invalid_argument("Direct G-sum Coulomb received a singular reciprocal lattice."); + } + return cartesian * reciprocal.Inverse(); +} + +std::vector enumerate_reciprocal_points( + const ModuleBase::Matrix3& reciprocal, + const ModuleBase::Vector3& q_direct, + const double tpiba, + const double ecut_ry) +{ + if (!std::isfinite(tpiba) || tpiba <= 0.0) + { + throw std::invalid_argument("Direct G-sum Coulomb requires positive tpiba."); + } + if (!std::isfinite(ecut_ry) || ecut_ry <= 0.0) + { + throw std::invalid_argument("Direct G-sum Coulomb requires a positive reciprocal cutoff."); + } + if (std::abs(reciprocal.Det()) <= std::numeric_limits::epsilon()) + { + throw std::invalid_argument("Direct G-sum Coulomb received a singular reciprocal lattice."); + } + + const double cart_radius = std::sqrt(ecut_ry) / tpiba; + const ModuleBase::Matrix3 inverse = reciprocal.Inverse(); + std::array lower{}; + std::array upper{}; + for (int axis = 0; axis < 3; ++axis) + { + const double direct_bound = cart_radius * column_norm(inverse, axis); + lower[static_cast(axis)] + = static_cast(std::ceil(-direct_bound - q_direct[axis] - 1.0e-12)); + upper[static_cast(axis)] + = static_cast(std::floor(direct_bound - q_direct[axis] + 1.0e-12)); + } + + std::vector result; + for (int i = lower[0]; i <= upper[0]; ++i) + { + for (int j = lower[1]; j <= upper[1]; ++j) + { + for (int k = lower[2]; k <= upper[2]; ++k) + { + const ModuleBase::Vector3 qg_direct(q_direct.x + i, + q_direct.y + j, + q_direct.z + k); + const ModuleBase::Vector3 qg_cart = qg_direct * reciprocal; + const double energy_ry = qg_cart.norm2() * tpiba * tpiba; + if (energy_ry <= ecut_ry + 1.0e-12) + { + result.push_back({{i, j, k}, qg_cart, std::sqrt(std::max(0.0, energy_ry))}); + } + } + } + } + std::sort(result.begin(), result.end(), [](const ReciprocalPoint& lhs, const ReciprocalPoint& rhs) { + const double lhs_norm = lhs.q_plus_g_cart.norm2(); + const double rhs_norm = rhs.q_plus_g_cart.norm2(); + if (std::abs(lhs_norm - rhs_norm) > 1.0e-14) + { + return lhs_norm < rhs_norm; + } + return lhs.g_direct < rhs.g_direct; + }); + return result; +} + +std::vector gauss_legendre_interval(const int order, + const double lower_bohr, + const double upper_bohr) +{ + if (order <= 0 || !std::isfinite(lower_bohr) || !std::isfinite(upper_bohr) + || !(lower_bohr < upper_bohr)) + { + throw std::invalid_argument("Direct mixed-2D Coulomb received an invalid quadrature interval."); + } + const double midpoint = 0.5 * (lower_bohr + upper_bohr); + const double half_width = 0.5 * (upper_bohr - lower_bohr); + std::vector result(static_cast(order)); + const int half_order = (order + 1) / 2; + for (int i = 0; i < half_order; ++i) + { + double root = std::cos(ModuleBase::PI * (static_cast(i) + 0.75) + / (static_cast(order) + 0.5)); + double derivative = 0.0; + for (int iteration = 0; iteration < 100; ++iteration) + { + double pnm2 = 1.0; + double pnm1 = root; + for (int n = 2; n <= order; ++n) + { + const double pn = ((2.0 * n - 1.0) * root * pnm1 - (n - 1.0) * pnm2) + / static_cast(n); + pnm2 = pnm1; + pnm1 = pn; + } + const double pn = order == 1 ? root : pnm1; + const double pnm1_final = order == 1 ? 1.0 : pnm2; + derivative = static_cast(order) * (root * pn - pnm1_final) + / (root * root - 1.0); + const double update = pn / derivative; + root -= update; + if (std::abs(update) < 4.0 * std::numeric_limits::epsilon()) + { + break; + } + } + const double weight = 2.0 / ((1.0 - root * root) * derivative * derivative); + const int left = i; + const int right = order - 1 - i; + result[static_cast(left)] = {midpoint - half_width * root, half_width * weight}; + result[static_cast(right)] = {midpoint + half_width * root, half_width * weight}; + } + return result; +} + +std::vector enumerate_reciprocal_lines( + const ModuleBase::Matrix3& reciprocal, + const ModuleBase::Vector3& q_direct, + const double tpiba, + const double ecut_ry) +{ + if (!std::isfinite(tpiba) || tpiba <= 0.0 || !std::isfinite(ecut_ry) || ecut_ry <= 0.0) + { + throw std::invalid_argument("Direct mixed-2D Coulomb requires positive tpiba and cutoff."); + } + if (std::abs(q_direct.z) > 1.0e-12) + { + throw std::invalid_argument("Direct mixed-2D Coulomb requires qz=0."); + } + if (std::abs(reciprocal.Det()) <= std::numeric_limits::epsilon()) + { + throw std::invalid_argument("Direct mixed-2D Coulomb received a singular reciprocal lattice."); + } + + const double cart_radius = std::sqrt(ecut_ry) / tpiba; + const ModuleBase::Matrix3 inverse = reciprocal.Inverse(); + std::array lower{}; + std::array upper{}; + for (int axis = 0; axis < 2; ++axis) + { + const double direct_bound = cart_radius * column_norm(inverse, axis); + lower[static_cast(axis)] + = static_cast(std::ceil(-direct_bound - q_direct[axis] - 1.0e-12)); + upper[static_cast(axis)] + = static_cast(std::floor(direct_bound - q_direct[axis] + 1.0e-12)); + } + + std::vector result; + for (int i = lower[0]; i <= upper[0]; ++i) + { + for (int j = lower[1]; j <= upper[1]; ++j) + { + const ModuleBase::Vector3 qg_direct(q_direct.x + i, q_direct.y + j, 0.0); + ModuleBase::Vector3 qg_cart = qg_direct * reciprocal; + if (std::abs(qg_cart.z) > 1.0e-10) + { + throw std::invalid_argument( + "Direct mixed-2D Coulomb currently requires the reciprocal slab plane to be xy."); + } + qg_cart.z = 0.0; + const double k_parallel_sq = qg_cart.norm2() * tpiba * tpiba; + if (k_parallel_sq < ecut_ry - 1.0e-14) + { + result.push_back({{i, j}, + qg_cart, + std::sqrt(std::max(0.0, k_parallel_sq)), + std::sqrt(std::max(0.0, ecut_ry - k_parallel_sq))}); + } + } + } + std::sort(result.begin(), result.end(), [](const ReciprocalLine2D& lhs, const ReciprocalLine2D& rhs) { + if (std::abs(lhs.k_parallel_bohr - rhs.k_parallel_bohr) > 1.0e-14) + { + return lhs.k_parallel_bohr < rhs.k_parallel_bohr; + } + return lhs.g_direct < rhs.g_direct; + }); + return result; +} + +std::vector expand_mixed_fourier_points( + const std::vector& lines, + const double tpiba, + const int kz_order) +{ + if (!std::isfinite(tpiba) || tpiba <= 0.0 || kz_order <= 0) + { + throw std::invalid_argument("Direct mixed-2D Coulomb received invalid kz quadrature settings."); + } + std::vector result; + result.reserve(lines.size() * static_cast(kz_order)); + for (const auto& line: lines) + { + if (line.k_parallel_bohr <= 1.0e-14) + { + throw std::invalid_argument( + "Direct mixed-2D regular quadrature requires separate treatment of the Gamma G_parallel=0 line."); + } + // Map the truncated kz integral (-kz_max, +kz_max) onto a compact + // angle interval through kz = k_parallel * tan(theta): the Coulomb + // kernel 1/(k_parallel^2 + kz^2) is then smooth in theta, and the + // Jacobian k_parallel*(1+tan^2) is folded into the Gauss-Legendre + // weights. This keeps every quadrature weight positive. + const double theta_max = std::atan(line.kz_max_bohr / line.k_parallel_bohr); + const auto quadrature = gauss_legendre_interval(kz_order, -theta_max, theta_max); + for (const auto& node: quadrature) + { + const double tangent = std::tan(node.coordinate_bohr); + const double kz_bohr = line.k_parallel_bohr * tangent; + const double jacobian = line.k_parallel_bohr * (1.0 + tangent * tangent); + ModuleBase::Vector3 k_cart = line.q_plus_g_parallel_cart; + k_cart.z = kz_bohr / tpiba; + const double k_bohr = line.k_parallel_bohr * std::sqrt(1.0 + tangent * tangent); + result.push_back({{{line.g_direct[0], line.g_direct[1], 0}, k_cart, k_bohr}, + node.weight_bohr * jacobian}); + } + } + return result; +} + +std::vector gamma_plane_quadrature( + const ModuleBase::Matrix3& reciprocal, + const double tpiba, + const std::array& nmp, + const int plane_order, + const double massidda_chi) +{ + if (!std::isfinite(tpiba) || tpiba <= 0.0 || nmp[0] <= 0 || nmp[1] <= 0 + || nmp[2] != 1 || plane_order <= 0 || plane_order % 2 != 0 + || !std::isfinite(massidda_chi) || massidda_chi <= 0.0) + { + throw std::invalid_argument( + "Direct mixed-2D Gamma quadrature requires a positive even plane order, " + "a 2D mesh, and positive Massidda chi."); + } + if (std::abs(reciprocal.Det()) <= std::numeric_limits::epsilon()) + { + throw std::invalid_argument("Direct mixed-2D Gamma quadrature received a singular lattice."); + } + + const double half_u = 0.5 / static_cast(nmp[0]); + const double half_v = 0.5 / static_cast(nmp[1]); + const auto u_nodes = gauss_legendre_interval(plane_order, -half_u, half_u); + const auto v_nodes = gauss_legendre_interval(plane_order, -half_v, half_v); + std::vector result; + result.reserve(static_cast(plane_order * plane_order)); + double inverse_q_average = 0.0; + for (const auto& u: u_nodes) + { + for (const auto& v: v_nodes) + { + ModuleBase::Vector3 q_cart + = ModuleBase::Vector3(u.coordinate_bohr, v.coordinate_bohr, 0.0) + * reciprocal; + if (std::abs(q_cart.z) > 1.0e-10) + { + throw std::invalid_argument( + "Direct mixed-2D Gamma quadrature requires the reciprocal slab plane to be xy."); + } + q_cart.z = 0.0; + const double q_bohr = q_cart.norm() * tpiba; + if (q_bohr <= 1.0e-14) + { + throw std::invalid_argument( + "Direct mixed-2D Gamma quadrature contains q=0; use an even plane order."); + } + const double weight = u.weight_bohr * v.weight_bohr + * static_cast(nmp[0] * nmp[1]); + result.push_back({q_cart, q_bohr, weight}); + inverse_q_average += weight / q_bohr; + } + } + + // Uniformly rescale the mesh-cell quadrature so that its leading 1/q + // average is exactly the same Massidda value used by the Ewald route. + // The weights stay positive, hence the resulting Gamma contribution can + // still be accumulated as a positive Gram matrix. + const double scale = inverse_q_average / massidda_chi; + if (!std::isfinite(scale) || scale <= 0.0) + { + throw std::runtime_error("Direct mixed-2D Gamma quadrature produced an invalid scale."); + } + for (auto& point: result) + { + point.q_parallel_cart = point.q_parallel_cart * scale; + point.q_bohr *= scale; + } + return result; +} + +std::vector expand_gamma_zero_line( + const std::vector& plane_points, + const double tpiba, + const double ecut_ry, + const int kz_order) +{ + if (plane_points.empty() || !std::isfinite(tpiba) || tpiba <= 0.0 + || !std::isfinite(ecut_ry) || ecut_ry <= 0.0 || kz_order <= 0) + { + throw std::invalid_argument("Direct mixed-2D Gamma expansion received invalid settings."); + } + std::vector result; + result.reserve(plane_points.size() * static_cast(kz_order)); + for (const auto& plane: plane_points) + { + if (!std::isfinite(plane.q_bohr) || plane.q_bohr <= 1.0e-14 + || !std::isfinite(plane.weight) || plane.weight <= 0.0 + || plane.q_bohr * plane.q_bohr >= ecut_ry) + { + throw std::invalid_argument( + "Direct mixed-2D Gamma plane point is invalid or outside the reciprocal cutoff."); + } + const double kz_max_bohr = std::sqrt(ecut_ry - plane.q_bohr * plane.q_bohr); + const double theta_max = std::atan(kz_max_bohr / plane.q_bohr); + const auto quadrature = gauss_legendre_interval(kz_order, -theta_max, theta_max); + for (const auto& node: quadrature) + { + const double tangent = std::tan(node.coordinate_bohr); + const double kz_bohr = plane.q_bohr * tangent; + const double jacobian = plane.q_bohr * (1.0 + tangent * tangent); + ModuleBase::Vector3 k_cart = plane.q_parallel_cart; + k_cart.z = kz_bohr / tpiba; + const double k_bohr = plane.q_bohr * std::sqrt(1.0 + tangent * tangent); + result.push_back({{{0, 0, 0}, k_cart, k_bohr}, + plane.weight * node.weight_bohr * jacobian}); + } + } + return result; +} + +// Fourier transform of one auxiliary basis function at momentum k, +// rho_mu(k) = sqrt((2*pi)^3/omega) * (-i)^l * Y_lm(k_hat) +// * R_l(k) * exp(-2*pi*i k.tau), +// from the tabulated radial part (cubic interpolation) and the real +// spherical harmonic of the direction. At k=0 only l=0 survives with +// Y_00 = 1/sqrt(4*pi). +std::vector> fourier_coefficients( + const std::vector& basis, + const ReciprocalPoint& point, + const double omega) +{ + if (!std::isfinite(omega) || omega <= 0.0) + { + throw std::invalid_argument("Direct G-sum Coulomb requires a positive cell volume."); + } + int lmax = 0; + for (const BasisFunction& function: basis) + { + if (function.radial.l < 0 || function.m < 0 || function.m > 2 * function.radial.l) + { + throw std::invalid_argument("Direct G-sum Coulomb received an invalid auxiliary angular index."); + } + lmax = std::max(lmax, function.radial.l); + } + + std::vector harmonics(static_cast((lmax + 1) * (lmax + 1)), 0.0); + const bool zero_momentum = point.k_bohr <= 1.0e-14; + if (zero_momentum) + { + harmonics[0] = ModuleBase::SQRT_INVERSE_FOUR_PI; + } + else + { + harmonics = real_spherical_harmonics(lmax, point.q_plus_g_cart); + } + + const double normalization = std::sqrt(std::pow(ModuleBase::TWO_PI, 3) / omega); + std::vector> coefficients(basis.size(), {0.0, 0.0}); + for (std::size_t i = 0; i < basis.size(); ++i) + { + const BasisFunction& function = basis[i]; + if (zero_momentum && function.radial.l != 0) + { + continue; + } + const double radial_value = interpolate_radial(function.radial, point.k_bohr); + const std::size_t lm = static_cast(function.radial.l * function.radial.l + function.m); + const double phase_arg = -ModuleBase::TWO_PI * (point.q_plus_g_cart * function.tau); + const std::complex center_phase(std::cos(phase_arg), std::sin(phase_arg)); + coefficients[i] = normalization * minus_i_to_power(function.radial.l) * harmonics[lm] + * radial_value * center_phase; + } + return coefficients; +} + +// 3D direct G-sum route: V_munu = alpha * sum_G 4*pi/k^2 * rho_mu(G)^* +// rho_nu(G), accumulated as a Hermitian rank-ng positive Gram matrix via +// ZHERK on rows sqrt(alpha*4*pi/k^2)*rho^*. The Gamma point adds the rank-one +// Massidda cell-average term alpha*4*pi*chi*rho_0^* rho_0 instead of the +// singular 1/k^2 value. +Result build_coulomb_matrix(const std::vector& basis, + const std::vector& points, + const double omega, + const double alpha, + const double chi, + const bool is_gamma) +{ + const auto start = std::chrono::steady_clock::now(); + if (basis.empty()) + { + throw std::invalid_argument("Direct G-sum Coulomb requires a non-empty auxiliary basis."); + } + if (!std::isfinite(alpha) || alpha < 0.0) + { + throw std::invalid_argument("Direct G-sum Coulomb requires a non-negative Fock alpha."); + } + if (!std::isfinite(chi) || chi < 0.0) + { + throw std::invalid_argument( + "Direct G-sum Coulomb requires a non-negative finite singular correction."); + } + + const std::size_t dimension = basis.size(); + std::vector index_seen(dimension, false); + for (const BasisFunction& function: basis) + { + if (function.global_index >= dimension || index_seen[function.global_index]) + { + throw std::invalid_argument("Direct G-sum auxiliary indices must be a contiguous permutation."); + } + index_seen[function.global_index] = true; + } + + std::vector regular_points; + regular_points.reserve(points.size()); + bool zero_found = false; + for (const ReciprocalPoint& point: points) + { + const bool gamma_integer_zero = is_gamma + && point.g_direct == std::array{0, 0, 0}; + if (gamma_integer_zero || point.k_bohr <= 1.0e-14) + { + zero_found = true; + } + else + { + regular_points.push_back(&point); + } + } + if (zero_found && !is_gamma) + { + throw std::invalid_argument("A non-Gamma direct G-sum set contains zero momentum."); + } + + Result result; + result.matrix.dimension = dimension; + result.matrix.values.assign(dimension * dimension, {0.0, 0.0}); + result.regular_g_count = regular_points.size(); + + if (!regular_points.empty() && alpha > 0.0) + { + const std::size_t ng = regular_points.size(); + if (dimension > static_cast(std::numeric_limits::max()) + || ng > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Direct G-sum matrix dimensions exceed the BLAS integer range."); + } + std::vector> gram_rows(dimension * ng, {0.0, 0.0}); +#ifdef _OPENMP +#pragma omp parallel for schedule(static) +#endif + for (std::size_t ig = 0; ig < ng; ++ig) + { + const ReciprocalPoint& point = *regular_points[ig]; + const auto rho = fourier_coefficients(basis, point, omega); + const double scale = std::sqrt(alpha * ModuleBase::FOUR_PI + / (point.k_bohr * point.k_bohr)); + for (std::size_t ib = 0; ib < basis.size(); ++ib) + { + gram_rows[basis[ib].global_index * ng + ig] = scale * std::conj(rho[ib]); + } + } + LapackConnector::herk('U', + 'N', + static_cast(dimension), + static_cast(ng), + 1.0, + gram_rows.data(), + static_cast(ng), + 0.0, + result.matrix.values.data(), + static_cast(dimension)); + for (std::size_t row = 0; row < dimension; ++row) + { + result.matrix(row, row) = {result.matrix(row, row).real(), 0.0}; + for (std::size_t column = row + 1; column < dimension; ++column) + { + result.matrix(column, row) = std::conj(result.matrix(row, column)); + } + } + } + + if (is_gamma && chi != 0.0 && alpha != 0.0) + { + // Singular_Value returns chi before the Coulomb 4*pi prefactor. + const ReciprocalPoint zero{{0, 0, 0}, ModuleBase::Vector3(), 0.0}; + const auto rho0 = fourier_coefficients(basis, zero, omega); + for (std::size_t mu = 0; mu < basis.size(); ++mu) + { + for (std::size_t nu = 0; nu < basis.size(); ++nu) + { + result.matrix(basis[mu].global_index, basis[nu].global_index) + += alpha * ModuleBase::FOUR_PI * chi * std::conj(rho0[mu]) * rho0[nu]; + } + } + } + + for (std::size_t row = 0; row < dimension; ++row) + { + for (std::size_t column = 0; column < dimension; ++column) + { + result.hermitian_residual + = std::max(result.hermitian_residual, + std::abs(result.matrix(row, column) - std::conj(result.matrix(column, row)))); + } + } + result.elapsed_seconds + = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + return result; +} + +// 2D mixed-Fourier route (see the file header): each quadrature node carries +// the continuum measure w_n*Lz/(2*pi), and the Gram rows are scaled by +// sqrt(alpha*4*pi*w_n*Lz/(2*pi)/k_n^2) so that the ZHERK outer-product sum +// reproduces V_PQ(q) with strictly non-negative weights. The residual +// against the exactly conjugate-symmetric result is recorded for the +// reader-v1 metadata. +Result build_gram_matrix(const std::vector& basis, + const std::vector& points, + const double omega, + const double lz_bohr, + const double alpha) +{ + const auto start = std::chrono::steady_clock::now(); + if (basis.empty() || points.empty()) + { + throw std::invalid_argument("Direct mixed-2D Coulomb requires basis functions and quadrature points."); + } + if (!std::isfinite(omega) || omega <= 0.0 || !std::isfinite(lz_bohr) || lz_bohr <= 0.0 + || !std::isfinite(alpha) || alpha <= 0.0) + { + throw std::invalid_argument("Direct mixed-2D Coulomb received invalid normalization parameters."); + } + + const std::size_t dimension = basis.size(); + std::vector index_seen(dimension, false); + for (const BasisFunction& function: basis) + { + if (function.global_index >= dimension || index_seen[function.global_index]) + { + throw std::invalid_argument("Direct mixed-2D auxiliary indices must be a contiguous permutation."); + } + index_seen[function.global_index] = true; + } + if (dimension > static_cast(std::numeric_limits::max()) + || points.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Direct mixed-2D matrix dimensions exceed the BLAS integer range."); + } + + Result result; + result.matrix.dimension = dimension; + result.matrix.values.assign(dimension * dimension, {0.0, 0.0}); + result.regular_g_count = points.size(); + const std::size_t npoints = points.size(); + std::vector> gram_rows(dimension * npoints, {0.0, 0.0}); + for (std::size_t ipoint = 0; ipoint < npoints; ++ipoint) + { + const auto& mixed_point = points[ipoint]; + if (mixed_point.point.k_bohr <= 1.0e-14 || mixed_point.kz_weight_bohr <= 0.0) + { + throw std::invalid_argument( + "Direct mixed-2D regular quadrature cannot contain the Gamma singular point."); + } + const auto rho = fourier_coefficients(basis, mixed_point.point, omega); + const double continuum_weight = mixed_point.kz_weight_bohr * lz_bohr / ModuleBase::TWO_PI; + const double scale = std::sqrt(alpha * ModuleBase::FOUR_PI * continuum_weight + / (mixed_point.point.k_bohr * mixed_point.point.k_bohr)); + for (std::size_t ib = 0; ib < basis.size(); ++ib) + { + gram_rows[basis[ib].global_index * npoints + ipoint] = scale * std::conj(rho[ib]); + } + } + LapackConnector::herk('U', + 'N', + static_cast(dimension), + static_cast(npoints), + 1.0, + gram_rows.data(), + static_cast(npoints), + 0.0, + result.matrix.values.data(), + static_cast(dimension)); + for (std::size_t row = 0; row < dimension; ++row) + { + result.matrix(row, row) = {result.matrix(row, row).real(), 0.0}; + for (std::size_t column = row + 1; column < dimension; ++column) + { + result.matrix(column, row) = std::conj(result.matrix(row, column)); + } + } + for (std::size_t row = 0; row < dimension; ++row) + { + for (std::size_t column = 0; column < dimension; ++column) + { + result.hermitian_residual + = std::max(result.hermitian_residual, + std::abs(result.matrix(row, column) - std::conj(result.matrix(column, row)))); + } + } + result.elapsed_seconds + = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + return result; +} + +std::vector atom_global_indices( + const std::vector& atom_types, + const std::vector>& type_traversal_to_local) +{ + std::vector result; + std::size_t offset = 0; + for (const int type: atom_types) + { + if (type < 0 || static_cast(type) >= type_traversal_to_local.size()) + { + throw std::invalid_argument("Direct G-sum Coulomb received an invalid atom type."); + } + const auto& permutation = type_traversal_to_local[static_cast(type)]; + std::vector seen(permutation.size(), false); + for (const std::size_t local: permutation) + { + if (local >= permutation.size() || seen[local]) + { + throw std::invalid_argument( + "Direct G-sum Coulomb type-local auxiliary ordering is not bijective."); + } + seen[local] = true; + result.push_back(offset + local); + } + offset += permutation.size(); + } + return result; +} + +void write_matrix_market(const DenseMatrix& matrix, const std::string& path) +{ + if (matrix.dimension == 0 || matrix.values.size() != matrix.dimension * matrix.dimension) + { + throw std::invalid_argument("Direct G-sum diagnostic received an invalid dense matrix."); + } + std::ofstream stream(path, std::ios::out | std::ios::trunc); + if (!stream.good()) + { + throw std::runtime_error("Failed to open direct G-sum MatrixMarket output: " + path); + } + stream << "%%MatrixMarket matrix coordinate complex general\n"; + stream << matrix.dimension << ' ' << matrix.dimension << ' ' + << matrix.dimension * matrix.dimension << '\n'; + stream << std::scientific << std::setprecision(17); + for (std::size_t row = 0; row < matrix.dimension; ++row) + { + for (std::size_t column = 0; column < matrix.dimension; ++column) + { + const auto value = matrix(row, column); + stream << row + 1 << ' ' << column + 1 << ' ' << value.real() << ' ' << value.imag() << '\n'; + } + } + if (!stream.good()) + { + throw std::runtime_error("Failed while writing direct G-sum MatrixMarket output: " + path); + } +} + +void write_reader_v1(const DenseMatrix& matrix, + const std::vector& atom_naux, + const int iq_one_based, + const int mpi_rank, + const std::string& prefix) +{ + constexpr std::int32_t marker = -20129433; + constexpr std::int32_t complex_flag = 1; + static_assert(sizeof(std::complex) == 2 * sizeof(double), + "LibRPA v1 Coulomb output expects complex as two doubles."); + if (matrix.dimension == 0 || matrix.values.size() != matrix.dimension * matrix.dimension) + { + throw std::invalid_argument("Direct G-sum reader-v1 output received an invalid dense matrix."); + } + if (atom_naux.empty() || iq_one_based <= 0 || mpi_rank < 0 || prefix.empty()) + { + throw std::invalid_argument("Direct G-sum reader-v1 output received invalid metadata."); + } + std::size_t naux = 0; + for (const int count: atom_naux) + { + if (count <= 0) + { + throw std::invalid_argument("Direct G-sum reader-v1 output requires positive per-atom sizes."); + } + naux += static_cast(count); + } + if (naux != matrix.dimension + || naux > static_cast(std::numeric_limits::max()) + || atom_naux.size() > static_cast(std::numeric_limits::max())) + { + throw std::invalid_argument("Direct G-sum reader-v1 auxiliary dimensions are inconsistent."); + } + + struct Block + { + std::int32_t pair_index = 0; + std::int64_t offset = 0; + std::size_t row_begin = 0; + std::size_t column_begin = 0; + std::size_t nrows = 0; + std::size_t ncolumns = 0; + }; + std::vector blocks; + std::size_t row_begin = 0; + for (std::size_t atom_i = 0; atom_i < atom_naux.size(); ++atom_i) + { + std::size_t column_begin = row_begin; + for (std::size_t atom_j = atom_i; atom_j < atom_naux.size(); ++atom_j) + { + const std::size_t pair_index + = atom_i * atom_naux.size() - atom_i * (atom_i - 1) / 2 + (atom_j - atom_i); + if (pair_index > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Direct G-sum reader-v1 atom-pair index exceeds int32 range."); + } + blocks.push_back({static_cast(pair_index), + 0, + row_begin, + column_begin, + static_cast(atom_naux[atom_i]), + static_cast(atom_naux[atom_j])}); + column_begin += static_cast(atom_naux[atom_j]); + } + row_begin += static_cast(atom_naux[atom_i]); + } + if (blocks.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Direct G-sum reader-v1 block count exceeds int32 range."); + } + + const auto checked_add = [](const std::int64_t lhs, const std::size_t values) { + if (values > static_cast(std::numeric_limits::max()) + / sizeof(std::complex)) + { + throw std::overflow_error("Direct G-sum reader-v1 payload exceeds int64 range."); + } + const std::int64_t bytes + = static_cast(values * sizeof(std::complex)); + if (lhs > std::numeric_limits::max() - bytes) + { + throw std::overflow_error("Direct G-sum reader-v1 file offset exceeds int64 range."); + } + return lhs + bytes; + }; + std::int64_t offset = static_cast(6 * sizeof(std::int32_t)) + + static_cast(atom_naux.size() * sizeof(std::int32_t)) + + static_cast(blocks.size()) + * static_cast(sizeof(std::int32_t) + sizeof(std::int64_t)); + for (auto& block: blocks) + { + block.offset = offset; + offset = checked_add(offset, block.nrows * block.ncolumns); + } + + std::ostringstream name; + name << prefix << iq_one_based << "_rank" << mpi_rank << ".dat"; + const std::string path = name.str(); + std::ofstream stream(path, std::ios::out | std::ios::binary | std::ios::trunc); + if (!stream.good()) + { + throw std::runtime_error("Failed to open direct G-sum reader-v1 output: " + path); + } + const auto write = [&stream, &path](const void* data, const std::size_t bytes) { + stream.write(reinterpret_cast(data), static_cast(bytes)); + if (!stream.good()) + { + throw std::runtime_error("Failed while writing direct G-sum reader-v1 output: " + path); + } + }; + const std::int32_t iq = iq_one_based; + const std::int32_t naux_i32 = static_cast(naux); + const std::int32_t natoms = static_cast(atom_naux.size()); + const std::int32_t nblocks = static_cast(blocks.size()); + for (const auto value: {marker, iq, naux_i32, complex_flag, natoms, nblocks}) + { + write(&value, sizeof(value)); + } + for (const int count: atom_naux) + { + const std::int32_t count_i32 = count; + write(&count_i32, sizeof(count_i32)); + } + for (const auto& block: blocks) + { + write(&block.pair_index, sizeof(block.pair_index)); + write(&block.offset, sizeof(block.offset)); + } + for (const auto& block: blocks) + { + for (std::size_t row = 0; row < block.nrows; ++row) + { + const auto* values = matrix.values.data() + + (block.row_begin + row) * matrix.dimension + + block.column_begin; + write(values, block.ncolumns * sizeof(std::complex)); + } + } +} + +void write_metadata(const Result& result, const Metadata& metadata, const std::string& path) +{ + std::ofstream stream(path, std::ios::out | std::ios::trunc); + if (!stream.good()) + { + throw std::runtime_error("Failed to open direct G-sum metadata output: " + path); + } + stream << std::scientific << std::setprecision(17); + stream << "{\n" + << " \"iq\": " << metadata.iq << ",\n" + << " \"q_direct\": [" << metadata.q_direct.x << ", " << metadata.q_direct.y << ", " + << metadata.q_direct.z << "],\n" + << " \"q_cart\": [" << metadata.q_cart.x << ", " << metadata.q_cart.y << ", " + << metadata.q_cart.z << "],\n" + << " \"ecut_ry\": " << metadata.ecut_ry << ",\n" + << " \"alpha\": " << metadata.alpha << ",\n" + << " \"chi\": " << metadata.chi << ",\n" + << " \"dimension\": " << result.matrix.dimension << ",\n" + << " \"regular_g_count\": " << result.regular_g_count << ",\n" + << " \"hermitian_residual\": " << result.hermitian_residual << ",\n" + << " \"elapsed_seconds\": " << result.elapsed_seconds << ",\n" + << " \"mpi_owner\": " << metadata.mpi_owner << ",\n" + << " \"mpi_size\": " << metadata.mpi_size << ",\n" + << " \"build_version\": \"" << json_escape(metadata.build_version) << "\"\n" + << "}\n"; + if (!stream.good()) + { + throw std::runtime_error("Failed while writing direct G-sum metadata output: " + path); + } +} +std::string format_method_metadata(const MethodMetadata& metadata) +{ + if (metadata.method.empty() || !std::isfinite(metadata.ecut_ry) + || metadata.ecut_ry <= 0.0 || metadata.kz_order <= 0 + || metadata.gamma_order <= 0 || metadata.gamma_order % 2 != 0 + || metadata.nq <= 0 || metadata.naux == 0 || metadata.source_revision.empty()) + { + throw std::invalid_argument("invalid direct strict-2D Coulomb method metadata"); + } + std::ostringstream output; + output << "# ABACUS reader-v1 strict 2D Coulomb method\n" + << "version = 1\n" + << "method = " << metadata.method << '\n' + << std::setprecision(17) + << "ecut_ry = " << metadata.ecut_ry << '\n' + << "kz_order = " << metadata.kz_order << '\n' + << "gamma_order = " << metadata.gamma_order << '\n' + << "nq = " << metadata.nq << '\n' + << "naux = " << metadata.naux << '\n' + << "source_revision = " << metadata.source_revision << '\n'; + return output.str(); +} + +std::string format_3d_method_metadata(const MethodMetadata3D& metadata) +{ + if (metadata.method.empty() || !std::isfinite(metadata.ecut_ry) + || metadata.ecut_ry <= 0.0 || metadata.nq <= 0 + || metadata.naux == 0 || metadata.source_revision.empty()) + { + throw std::invalid_argument("invalid direct 3D Coulomb method metadata"); + } + std::ostringstream output; + output << "# ABACUS reader-v1 3D Coulomb method\n" + << "version = 1\n" + << "method = " << metadata.method << '\n' + << std::setprecision(17) + << "ecut_ry = " << metadata.ecut_ry << '\n' + << "nq = " << metadata.nq << '\n' + << "naux = " << metadata.naux << '\n' + << "source_revision = " << metadata.source_revision << '\n'; + return output.str(); +} + +} // namespace Direct2dCoulomb diff --git a/source/source_lcao/module_ri/direct_2d_coulomb.h b/source/source_lcao/module_ri/direct_2d_coulomb.h new file mode 100644 index 00000000000..b6759b8da97 --- /dev/null +++ b/source/source_lcao/module_ri/direct_2d_coulomb.h @@ -0,0 +1,206 @@ +#ifndef DIRECT_2D_COULOMB_H +#define DIRECT_2D_COULOMB_H + +#include "source_base/matrix3.h" +#include "source_base/vector3.h" + +#include +#include +#include +#include +#include + +namespace Direct2dCoulomb +{ +// One reciprocal sample k = q + G in Cartesian bohr^-1 units. +struct ReciprocalPoint +{ + std::array g_direct{}; + ModuleBase::Vector3 q_plus_g_cart; + double k_bohr = 0.0; +}; + +// A Gauss-Legendre node in a generic abscissa/weight pair (units of the +// mapped variable, not necessarily bohr). +struct QuadratureNode +{ + double coordinate_bohr = 0.0; + double weight_bohr = 0.0; +}; + +// One in-plane momentum line q+G_parallel of the mixed-Fourier 2D route; the +// continuous kz integral on this line is truncated at +/- kz_max_bohr. +struct ReciprocalLine2D +{ + std::array g_direct{}; + ModuleBase::Vector3 q_plus_g_parallel_cart; + double k_parallel_bohr = 0.0; + double kz_max_bohr = 0.0; +}; + +// A kz quadrature node on one line: the 3D momentum and the node weight that +// already includes the tan-mapping Jacobian. +struct MixedFourierPoint +{ + ReciprocalPoint point; + double kz_weight_bohr = 0.0; +}; + +// A node of the Gamma-line in-plane cell quadrature (the replacement for the +// singular q=0 line, rescaled to the Massidda 1/q cell average). +struct InPlaneQuadraturePoint +{ + ModuleBase::Vector3 q_parallel_cart; + double q_bohr = 0.0; + double weight = 0.0; +}; + +// The tabulated l-resolved radial part R_l(k) of one auxiliary function on a +// uniform dk grid; the table is borrowed, not owned. +struct RadialTable +{ + int l = 0; + double dk = 0.0; + const double* values = nullptr; + std::size_t size = 0; +}; + +// One auxiliary basis function: radial table, magnetic number, global matrix +// index, and the atom center tau for the Fourier phase. +struct BasisFunction +{ + RadialTable radial; + int m = 0; + std::size_t global_index = 0; + ModuleBase::Vector3 tau; +}; + +struct DenseMatrix +{ + std::size_t dimension = 0; + std::vector> values; + + std::complex& operator()(std::size_t row, std::size_t column) + { + return values.at(row * dimension + column); + } + + const std::complex& operator()(std::size_t row, std::size_t column) const + { + return values.at(row * dimension + column); + } +}; + +struct Result +{ + DenseMatrix matrix; + std::size_t regular_g_count = 0; + double hermitian_residual = 0.0; + double elapsed_seconds = 0.0; +}; + +struct MethodMetadata +{ + std::string method = "ewald"; + double ecut_ry = 0.0; + int kz_order = 0; + int gamma_order = 8; + int nq = 0; + std::size_t naux = 0; + std::string source_revision; +}; + +struct MethodMetadata3D +{ + std::string method = "ewald"; + double ecut_ry = 0.0; + int nq = 0; + std::size_t naux = 0; + std::string source_revision; +}; + +struct Metadata +{ + int iq = 0; + ModuleBase::Vector3 q_direct; + ModuleBase::Vector3 q_cart; + double ecut_ry = 0.0; + double alpha = 0.0; + double chi = 0.0; + int mpi_owner = 0; + int mpi_size = 1; + std::string build_version; +}; + +ModuleBase::Vector3 cartesian_to_direct( + const ModuleBase::Matrix3& reciprocal, + const ModuleBase::Vector3& cartesian); + +std::vector enumerate_reciprocal_points( + const ModuleBase::Matrix3& reciprocal, + const ModuleBase::Vector3& q_direct, + double tpiba, + double ecut_ry); + +std::vector gauss_legendre_interval(int order, + double lower_bohr, + double upper_bohr); + +std::vector enumerate_reciprocal_lines( + const ModuleBase::Matrix3& reciprocal, + const ModuleBase::Vector3& q_direct, + double tpiba, + double ecut_ry); + +std::vector expand_mixed_fourier_points( + const std::vector& lines, + double tpiba, + int kz_order); + +std::vector gamma_plane_quadrature( + const ModuleBase::Matrix3& reciprocal, + double tpiba, + const std::array& nmp, + int plane_order, + double massidda_chi); + +std::vector expand_gamma_zero_line( + const std::vector& plane_points, + double tpiba, + double ecut_ry, + int kz_order); + +std::vector> fourier_coefficients( + const std::vector& basis, + const ReciprocalPoint& point, + double omega); + +Result build_coulomb_matrix(const std::vector& basis, + const std::vector& points, + double omega, + double alpha, + double chi, + bool is_gamma); + +Result build_gram_matrix(const std::vector& basis, + const std::vector& points, + double omega, + double lz_bohr, + double alpha); + +std::vector atom_global_indices( + const std::vector& atom_types, + const std::vector>& type_traversal_to_local); + +void write_matrix_market(const DenseMatrix& matrix, const std::string& path); +void write_reader_v1(const DenseMatrix& matrix, + const std::vector& atom_naux, + int iq_one_based, + int mpi_rank, + const std::string& prefix); +std::string format_method_metadata(const MethodMetadata& metadata); +std::string format_3d_method_metadata(const MethodMetadata3D& metadata); +void write_metadata(const Result& result, const Metadata& metadata, const std::string& path); +} // namespace Direct2dCoulomb + +#endif diff --git a/source/source_lcao/module_ri/direct_2d_coulomb_ewald.hpp b/source/source_lcao/module_ri/direct_2d_coulomb_ewald.hpp new file mode 100644 index 00000000000..068ed710dfb --- /dev/null +++ b/source/source_lcao/module_ri/direct_2d_coulomb_ewald.hpp @@ -0,0 +1,258 @@ +#ifndef DIRECT_2D_COULOMB_EWALD_HPP +#define DIRECT_2D_COULOMB_EWALD_HPP + +#include +#include +#include +#include +#include +#include + +// Driver for the direct mixed-Fourier 2D Coulomb output (see +// direct_2d_coulomb.h for the construction). It walks the complete full-q +// Cartesian mesh, reuses the Ewald Fock alpha and the Massidda chi so that +// the direct route evaluates the same physical operator as the split Ewald +// route, assembles the auxiliary basis from the ABFS radial tables, and +// writes the reader-v1 V_full files plus method metadata. Opt-in through +// out_librpa_2d_coulomb_method = direct_mixed_fourier. +template +void Ewald_Vq::output_direct_2d_coulomb(const UnitCell& ucell, + const double ecut_ry, + const int kz_order, + const int gamma_order) +{ + if (this->ewald_dimension != 2) + { + throw std::invalid_argument("Direct mixed-Fourier Coulomb requires Ewald dimension 2."); + } + if (!(ecut_ry > 0.0) || !std::isfinite(ecut_ry) || kz_order <= 0 + || gamma_order <= 0 || gamma_order % 2 != 0) + { + throw std::invalid_argument("Direct mixed-Fourier Coulomb received invalid quadrature controls."); + } + if (this->p_kv == nullptr + || static_cast(this->p_kv->kvec_c_full.size()) != this->nks0) + { + throw std::runtime_error("Direct G-sum Coulomb requires a complete full-q Cartesian mesh."); + } + + const auto fock_params = this->coulomb_param.find(Conv_Coulomb_Pot_K::Coulomb_Type::Fock); + if (fock_params == this->coulomb_param.end() || fock_params->second.empty()) + { + throw std::runtime_error("Direct G-sum Coulomb could not find the Ewald Fock parameters."); + } + const double chi = this->get_singular_chi(ucell, fock_params->second, 2.0); + const double alpha = this->bare_multipole_scale; + if (alpha <= 0.0) + { + throw std::runtime_error("Direct G-sum Coulomb requires a positive total Ewald Fock alpha."); + } + + std::vector> type_traversal_to_local(this->abfs.size()); + for (std::size_t type = 0; type < this->abfs.size(); ++type) + { + for (std::size_t l = 0; l < this->abfs[type].size(); ++l) + { + for (std::size_t n = 0; n < this->abfs[type][l].size(); ++n) + { + for (std::size_t m = 0; m < 2 * l + 1; ++m) + { + type_traversal_to_local[type].push_back(this->index_abfs[type][l][n][m]); + } + } + } + } + std::vector atom_types(static_cast(ucell.nat)); + for (int atom = 0; atom < ucell.nat; ++atom) + { + atom_types[static_cast(atom)] = ucell.iat2it[atom]; + } + const auto global_indices + = Direct2dCoulomb::atom_global_indices(atom_types, type_traversal_to_local); + + std::vector basis; + basis.reserve(global_indices.size()); + std::size_t traversal = 0; + for (int atom = 0; atom < ucell.nat; ++atom) + { + const int type = ucell.iat2it[atom]; + const int atom_in_type = ucell.iat2ia[atom]; + const auto tau = ucell.atoms[type].tau[atom_in_type]; + for (std::size_t l = 0; l < this->abfs[static_cast(type)].size(); ++l) + { + for (std::size_t n = 0; n < this->abfs[static_cast(type)][l].size(); ++n) + { + const auto& orbital = this->abfs[static_cast(type)][l][n]; + const Direct2dCoulomb::RadialTable radial{static_cast(l), + orbital.getDk(), + orbital.getPsif(), + static_cast(orbital.getNk())}; + for (std::size_t m = 0; m < 2 * l + 1; ++m) + { + basis.push_back({radial, + static_cast(m), + global_indices.at(traversal), + tau}); + ++traversal; + } + } + } + } + if (traversal != global_indices.size()) + { + throw std::runtime_error("Direct G-sum Coulomb auxiliary traversal size is inconsistent."); + } + std::vector atom_naux(static_cast(ucell.nat), 0); + for (int atom = 0; atom < ucell.nat; ++atom) + { + atom_naux[static_cast(atom)] + = static_cast(type_traversal_to_local[static_cast(ucell.iat2it[atom])].size()); + } + + int mpi_rank = 0; + int mpi_size = 1; + MPI_Comm_rank(this->mpi_comm, &mpi_rank); + MPI_Comm_size(this->mpi_comm, &mpi_size); + + struct DirectQOutput + { + int reader_iq = 0; + int full_iq = -1; + ModuleBase::Vector3 q_direct; + ModuleBase::Vector3 q_cart; + }; + std::vector q_outputs; + if (!this->p_kv->kstars.empty()) + { + q_outputs.reserve(this->p_kv->kstars.size()); + for (std::size_t ibz = 0; ibz < this->p_kv->kstars.size(); ++ibz) + { + const auto identity = this->p_kv->kstars[ibz].find(0); + if (identity == this->p_kv->kstars[ibz].end()) + { + throw std::runtime_error( + "Direct G-sum Ewald replacement could not find the identity member " + "of an IBZ q-star."); + } + const auto q_direct = identity->second; + q_outputs.push_back({static_cast(ibz), -1, q_direct, q_direct * ucell.G}); + } + if (mpi_rank == 0) + { + std::cout << "Direct mixed-Fourier Coulomb writes " << q_outputs.size() + << " IBZ q representatives in reader-v1 star order instead of the " + << this->nks0 << "-point full-q diagnostic order." << std::endl; + } + } + else + { + q_outputs.reserve(static_cast(this->nks0)); + for (int iq = 0; iq < this->nks0; ++iq) + { + const auto q_cart = this->p_kv->kvec_c_full[iq]; + q_outputs.push_back({iq, + iq, + Direct2dCoulomb::cartesian_to_direct(ucell.G, q_cart), + q_cart}); + } + } + + for (std::size_t requested = 0; requested < q_outputs.size(); ++requested) + { + const int owner = static_cast(requested % static_cast(mpi_size)); + if (owner != mpi_rank) + { + continue; + } + const auto& q_output = q_outputs[requested]; + const int iq = q_output.reader_iq; + const auto q_cart = q_output.q_cart; + const auto q_direct = q_output.q_direct; + const bool is_gamma = q_cart.norm2() <= 1.0e-20; + Direct2dCoulomb::Result result; + const auto lines = Direct2dCoulomb::enumerate_reciprocal_lines( + ucell.G, q_direct, ucell.tpiba, ecut_ry); + std::vector regular_lines; + regular_lines.reserve(lines.size()); + for (const auto& line: lines) + { + if (line.k_parallel_bohr > 1.0e-14) + { + regular_lines.push_back(line); + } + } + auto points = Direct2dCoulomb::expand_mixed_fourier_points( + regular_lines, ucell.tpiba, kz_order); + if (is_gamma) + { + const auto gamma_plane = Direct2dCoulomb::gamma_plane_quadrature( + ucell.G, ucell.tpiba, this->nmp, gamma_order, chi); + auto gamma_points = Direct2dCoulomb::expand_gamma_zero_line( + gamma_plane, ucell.tpiba, ecut_ry, kz_order); + points.insert(points.end(), gamma_points.begin(), gamma_points.end()); + } + const double lz_bohr = ucell.a3.norm() * ucell.lat0; + result = Direct2dCoulomb::build_gram_matrix( + basis, points, ucell.omega, lz_bohr, alpha); + std::ostringstream stem; + stem << "direct_2d_coulomb_iq" << iq; + Direct2dCoulomb::write_reader_v1( + result.matrix, atom_naux, iq + 1, mpi_rank, "v1_coulomb_full_iq_"); +#ifdef VERSION + const std::string build_version = VERSION; +#else + const std::string build_version = "unknown"; +#endif + const Direct2dCoulomb::Metadata metadata{iq, + q_direct, + q_cart, + ecut_ry, + alpha, + chi, + owner, + mpi_size, + build_version}; + Direct2dCoulomb::write_metadata(result, metadata, stem.str() + ".json"); + std::cout << "Direct G-sum Coulomb diagnostic: iq=" << iq + << " full_iq=" << q_output.full_iq + << " dimension=" << this->ewald_dimension + << " naux=" << result.matrix.dimension + << " regular_points=" << result.regular_g_count + << " ecut_ry=" << ecut_ry + << " kz_order=" << kz_order + << " gamma_plane_order=" << gamma_order + << " hermitian_residual=" << result.hermitian_residual + << " elapsed_seconds=" << result.elapsed_seconds + << std::endl; + } + MPI_Barrier(this->mpi_comm); + if (mpi_rank == 0) + { +#ifdef VERSION + const std::string source_revision = VERSION; +#else + const std::string source_revision = "unknown"; +#endif + const Direct2dCoulomb::MethodMetadata method_metadata{ + "direct_mixed_fourier", + ecut_ry, + kz_order, + gamma_order, + static_cast(q_outputs.size()), + basis.size(), + source_revision}; + const std::string filename = "librpa_2d_coulomb_method.dat"; + std::ofstream output(filename, std::ios::out | std::ios::trunc); + if (!output.good()) + { + throw std::runtime_error("Failed to open " + filename); + } + output << Direct2dCoulomb::format_method_metadata(method_metadata); + if (!output.good()) + { + throw std::runtime_error("Failed to write " + filename); + } + } +} + +#endif diff --git a/source/source_lcao/module_ri/direct_3d_coulomb_ewald.hpp b/source/source_lcao/module_ri/direct_3d_coulomb_ewald.hpp new file mode 100644 index 00000000000..d35e5848380 --- /dev/null +++ b/source/source_lcao/module_ri/direct_3d_coulomb_ewald.hpp @@ -0,0 +1,233 @@ +#ifndef DIRECT_3D_COULOMB_EWALD_HPP +#define DIRECT_3D_COULOMB_EWALD_HPP + +#include +#include +#include +#include +#include +#include +#include + +// Driver for the direct reciprocal 3D Coulomb output: the plain G-sum +// V = alpha * sum_G 4*pi/k^2 rho^* rho (plus the Massidda Gamma term) over +// the complete full-q mesh, built by Direct2dCoulomb::build_coulomb_matrix. +// Like the 2D driver it reuses the Ewald Fock alpha and chi so it evaluates +// the same operator as the split route, and writes the reader-v1 V_full +// files. Opt-in through out_librpa_3d_coulomb_method = direct_reciprocal. +template +void Ewald_Vq::output_direct_3d_coulomb(const UnitCell& ucell, + const double ecut_ry) +{ + if (this->ewald_dimension != 3) + { + throw std::invalid_argument("Direct reciprocal Coulomb requires Ewald dimension 3."); + } + if (!(ecut_ry > 0.0) || !std::isfinite(ecut_ry)) + { + throw std::invalid_argument("Direct reciprocal Coulomb requires a positive cutoff."); + } + if (this->p_kv == nullptr + || static_cast(this->p_kv->kvec_c_full.size()) != this->nks0) + { + throw std::runtime_error("Direct reciprocal Coulomb requires a complete full-q mesh."); + } + + const auto fock_params = this->coulomb_param.find(Conv_Coulomb_Pot_K::Coulomb_Type::Fock); + if (fock_params == this->coulomb_param.end() || fock_params->second.empty()) + { + throw std::runtime_error("Direct reciprocal Coulomb could not find the Ewald Fock parameters."); + } + const double chi = this->get_singular_chi(ucell, fock_params->second, 2.0); + const double alpha = this->bare_multipole_scale; + if (alpha <= 0.0) + { + throw std::runtime_error("Direct reciprocal Coulomb requires a positive total Fock alpha."); + } + + std::vector> type_traversal_to_local(this->abfs.size()); + for (std::size_t type = 0; type < this->abfs.size(); ++type) + { + for (std::size_t l = 0; l < this->abfs[type].size(); ++l) + { + for (std::size_t n = 0; n < this->abfs[type][l].size(); ++n) + { + for (std::size_t m = 0; m < 2 * l + 1; ++m) + { + type_traversal_to_local[type].push_back(this->index_abfs[type][l][n][m]); + } + } + } + } + std::vector atom_types(static_cast(ucell.nat)); + for (int atom = 0; atom < ucell.nat; ++atom) + { + atom_types[static_cast(atom)] = ucell.iat2it[atom]; + } + const auto global_indices + = Direct2dCoulomb::atom_global_indices(atom_types, type_traversal_to_local); + + std::vector basis; + basis.reserve(global_indices.size()); + std::size_t traversal = 0; + for (int atom = 0; atom < ucell.nat; ++atom) + { + const int type = ucell.iat2it[atom]; + const int atom_in_type = ucell.iat2ia[atom]; + const auto tau = ucell.atoms[type].tau[atom_in_type]; + for (std::size_t l = 0; l < this->abfs[static_cast(type)].size(); ++l) + { + for (std::size_t n = 0; n < this->abfs[static_cast(type)][l].size(); ++n) + { + const auto& orbital = this->abfs[static_cast(type)][l][n]; + const Direct2dCoulomb::RadialTable radial{static_cast(l), + orbital.getDk(), + orbital.getPsif(), + static_cast(orbital.getNk())}; + for (std::size_t m = 0; m < 2 * l + 1; ++m) + { + basis.push_back({radial, + static_cast(m), + global_indices.at(traversal), + tau}); + ++traversal; + } + } + } + } + if (traversal != global_indices.size()) + { + throw std::runtime_error("Direct reciprocal Coulomb auxiliary traversal is inconsistent."); + } + std::vector atom_naux(static_cast(ucell.nat), 0); + for (int atom = 0; atom < ucell.nat; ++atom) + { + atom_naux[static_cast(atom)] + = static_cast(type_traversal_to_local[static_cast(ucell.iat2it[atom])].size()); + } + + int mpi_rank = 0; + int mpi_size = 1; + MPI_Comm_rank(this->mpi_comm, &mpi_rank); + MPI_Comm_size(this->mpi_comm, &mpi_size); + + struct DirectQOutput + { + int reader_iq = 0; + int full_iq = -1; + ModuleBase::Vector3 q_direct; + ModuleBase::Vector3 q_cart; + }; + std::vector q_outputs; + if (!this->p_kv->kstars.empty()) + { + q_outputs.reserve(this->p_kv->kstars.size()); + for (std::size_t ibz = 0; ibz < this->p_kv->kstars.size(); ++ibz) + { + const auto identity = this->p_kv->kstars[ibz].find(0); + if (identity == this->p_kv->kstars[ibz].end()) + { + throw std::runtime_error( + "Direct reciprocal Coulomb could not find the identity member of an IBZ q-star."); + } + const auto q_direct = identity->second; + q_outputs.push_back({static_cast(ibz), -1, q_direct, q_direct * ucell.G}); + } + if (mpi_rank == 0) + { + std::cout << "Direct reciprocal Coulomb writes " << q_outputs.size() + << " IBZ q representatives in reader-v1 star order instead of the " + << this->nks0 << "-point full-q diagnostic order." << std::endl; + } + } + else + { + q_outputs.reserve(static_cast(this->nks0)); + for (int iq = 0; iq < this->nks0; ++iq) + { + const auto q_cart = this->p_kv->kvec_c_full[iq]; + q_outputs.push_back({iq, + iq, + Direct2dCoulomb::cartesian_to_direct(ucell.G, q_cart), + q_cart}); + } + } + + for (std::size_t requested = 0; requested < q_outputs.size(); ++requested) + { + const int owner = static_cast(requested % static_cast(mpi_size)); + if (owner != mpi_rank) + { + continue; + } + const auto& q_output = q_outputs[requested]; + const int iq = q_output.reader_iq; + const bool is_gamma = q_output.q_cart.norm2() <= 1.0e-20; + const auto points = Direct2dCoulomb::enumerate_reciprocal_points( + ucell.G, q_output.q_direct, ucell.tpiba, ecut_ry); + if (points.empty()) + { + throw std::runtime_error( + "Direct reciprocal Coulomb cutoff retained no reciprocal vectors."); + } + const auto result = Direct2dCoulomb::build_coulomb_matrix( + basis, points, ucell.omega, alpha, chi, is_gamma); + + Direct2dCoulomb::write_reader_v1( + result.matrix, atom_naux, iq + 1, mpi_rank, "v1_coulomb_full_iq_"); + std::ostringstream stem; + stem << "direct_3d_coulomb_iq" << iq; +#ifdef VERSION + const std::string build_version = VERSION; +#else + const std::string build_version = "unknown"; +#endif + const Direct2dCoulomb::Metadata metadata{iq, + q_output.q_direct, + q_output.q_cart, + ecut_ry, + alpha, + chi, + owner, + mpi_size, + build_version}; + Direct2dCoulomb::write_metadata(result, metadata, stem.str() + ".json"); + std::cout << "Direct reciprocal Coulomb diagnostic: iq=" << iq + << " full_iq=" << q_output.full_iq + << " dimension=" << this->ewald_dimension + << " naux=" << result.matrix.dimension + << " regular_g_count=" << result.regular_g_count + << " ecut_ry=" << ecut_ry + << " hermitian_residual=" << result.hermitian_residual + << " elapsed_seconds=" << result.elapsed_seconds + << std::endl; + } + MPI_Barrier(this->mpi_comm); + if (mpi_rank == 0) + { +#ifdef VERSION + const std::string source_revision = VERSION; +#else + const std::string source_revision = "unknown"; +#endif + const Direct2dCoulomb::MethodMetadata3D method_metadata{ + "direct_reciprocal", + ecut_ry, + static_cast(q_outputs.size()), + basis.size(), + source_revision}; + const std::string filename = "librpa_3d_coulomb_method.dat"; + std::ofstream output(filename, std::ios::out | std::ios::trunc); + if (!output.good()) + { + throw std::runtime_error("Failed to open " + filename); + } + output << Direct2dCoulomb::format_3d_method_metadata(method_metadata); + if (!output.good()) + { + throw std::runtime_error("Failed to write " + filename); + } + } +} + +#endif diff --git a/source/source_lcao/module_ri/ewald_gaussian_cutoff.h b/source/source_lcao/module_ri/ewald_gaussian_cutoff.h new file mode 100644 index 00000000000..1bb674fd6a8 --- /dev/null +++ b/source/source_lcao/module_ri/ewald_gaussian_cutoff.h @@ -0,0 +1,134 @@ +#ifndef EWALD_GAUSSIAN_CUTOFF_H +#define EWALD_GAUSSIAN_CUTOFF_H + +#include +#include +#include + +// Gaussian decay exponents that make the Ewald real-space and reciprocal +// tails negligible (1e-14) for a given auxiliary angular momentum. +// +// The historical defaults (17.5 real / 35.0 reciprocal at l=0) are kept as +// starting points for compatibility; for higher l the multipole tail +// erfc(sqrt(x)) + sum_k x^{k/2} e^{-x} / Gamma(k/2+1) decays more slowly, so +// the exponent is enlarged by bisection until the tail bound is met. This +// keeps the truncated Gaussian compensation accurate at every l instead of +// silently reusing the l=0 cutoffs. +namespace EwaldGaussianCutoff +{ +constexpr double historical_real_space_exponent = 17.5; +constexpr double real_space_tail_tolerance = 1.0e-14; +constexpr double historical_reciprocal_exponent = 35.0; +constexpr double reciprocal_tail_tolerance = 1.0e-14; + +inline double real_space_multipole_tail(const double exponent, const int angular_momentum) +{ + if (exponent < 0.0) + { + throw std::invalid_argument("Ewald Gaussian decay exponent must be non-negative"); + } + if (angular_momentum < 0) + { + throw std::invalid_argument("Ewald Gaussian angular momentum must be non-negative"); + } + + double shape = 0.5; + double tail = std::erfc(std::sqrt(exponent)); + for (int step = 0; step <= angular_momentum; ++step) + { + tail += std::pow(exponent, shape) * std::exp(-exponent) / std::tgamma(shape + 1.0); + shape += 1.0; + } + return tail; +} + +inline double real_space_decay_exponent(const int angular_momentum) +{ + if (angular_momentum < 0) + { + throw std::invalid_argument("Ewald Gaussian angular momentum must be non-negative"); + } + + double lower = historical_real_space_exponent; + double upper = lower; + while (real_space_multipole_tail(upper, angular_momentum) > real_space_tail_tolerance) + { + upper += 1.0; + } + for (int iteration = 0; iteration != 80; ++iteration) + { + const double midpoint = 0.5 * (lower + upper); + if (real_space_multipole_tail(midpoint, angular_momentum) > real_space_tail_tolerance) + { + lower = midpoint; + } + else + { + upper = midpoint; + } + } + return upper; +} + +inline double real_space_radius(const double lambda, const int angular_momentum) +{ + if (lambda <= 0.0) + { + throw std::invalid_argument("Ewald Gaussian lambda must be positive"); + } + return std::sqrt(2.0 * real_space_decay_exponent(angular_momentum) / lambda); +} + +inline double reciprocal_tail_scale(const double exponent, const int lmax) +{ + if (exponent < 0.0) + { + throw std::invalid_argument("Ewald Gaussian decay exponent must be non-negative"); + } + if (lmax < 0) + { + throw std::invalid_argument("Ewald Gaussian angular momentum must be non-negative"); + } + const int polynomial_power = std::max(0, lmax - 2); + return std::pow(exponent, 0.5 * polynomial_power) * std::exp(-exponent); +} + +inline double reciprocal_decay_exponent(const int lmax) +{ + if (lmax < 0) + { + throw std::invalid_argument("Ewald Gaussian angular momentum must be non-negative"); + } + + double lower = historical_reciprocal_exponent; + double upper = lower; + while (reciprocal_tail_scale(upper, lmax) > reciprocal_tail_tolerance) + { + upper += 1.0; + } + for (int iteration = 0; iteration != 80; ++iteration) + { + const double midpoint = 0.5 * (lower + upper); + if (reciprocal_tail_scale(midpoint, lmax) > reciprocal_tail_tolerance) + { + lower = midpoint; + } + else + { + upper = midpoint; + } + } + return upper; +} + +inline double reciprocal_radius(const double lambda, const int lmax) +{ + if (lambda <= 0.0) + { + throw std::invalid_argument("Ewald Gaussian lambda must be positive"); + } + return std::sqrt(reciprocal_decay_exponent(lmax) * lambda); +} +} // namespace EwaldGaussianCutoff + +#endif diff --git a/source/source_lcao/module_ri/ewald_mpi_utils.h b/source/source_lcao/module_ri/ewald_mpi_utils.h new file mode 100644 index 00000000000..b897f76c665 --- /dev/null +++ b/source/source_lcao/module_ri/ewald_mpi_utils.h @@ -0,0 +1,104 @@ +#ifndef EWALD_MPI_UTILS_H +#define EWALD_MPI_UTILS_H + +#include "ewald_tail_utils.h" + +#include +#include +#include +#include +#include +#include + +namespace EwaldVqDetail +{ +template +auto distribute_common_realspace_tasks(const MPI_Comm& mpi_comm, + const std::vector& atoms, + const std::array& bare_period) +{ + return RI::Distribute_Equally::distribute_atoms_periods(mpi_comm, atoms, bare_period, 2, false); +} + +inline std::vector distribute_tail_shell_keys(const std::vector& atoms, + const TailCell& production_period, + const TailCell& probe_period, + const int mpi_rank, + const int mpi_size) +{ + if (mpi_rank < 0 || mpi_rank >= mpi_size) + { + throw std::invalid_argument("MPI rank is outside the communicator"); + } + const auto shell_cells = make_shell_cells(production_period, probe_period); + std::vector local_shell; + const std::size_t global_key_count = atoms.size() * atoms.size() * shell_cells.size(); + local_shell.reserve(global_key_count / static_cast(mpi_size) + 1); + for (const int atom_i: atoms) + { + for (const int atom_j: atoms) + { + for (const TailCell& cell: shell_cells) + { + const TailKey key{atom_i, atom_j, cell}; + if (shell_owner(key, mpi_size) == mpi_rank) local_shell.push_back(key); + } + } + } + return local_shell; +} + +inline TailStats reduce_tail_stats(const MPI_Comm mpi_comm, const TailStats& local) +{ + int mpi_rank = 0; + MPI_Comm_rank(mpi_comm, &mpi_rank); + + TailStats global; + const unsigned long long local_counts[2] + = {static_cast(local.production_blocks), + static_cast(local.shell_blocks)}; + unsigned long long global_counts[2] = {0, 0}; + MPI_Allreduce(local_counts, global_counts, 2, MPI_UNSIGNED_LONG_LONG, MPI_SUM, mpi_comm); + global.production_blocks = static_cast(global_counts[0]); + global.shell_blocks = static_cast(global_counts[1]); + + MPI_Allreduce(&local.reference_norm, &global.reference_norm, 1, MPI_DOUBLE, MPI_MAX, mpi_comm); + MPI_Allreduce(&local.sum_norm, &global.sum_norm, 1, MPI_DOUBLE, MPI_SUM, mpi_comm); + MPI_Allreduce(&local.hermitian_residual, + &global.hermitian_residual, + 1, + MPI_DOUBLE, + MPI_MAX, + mpi_comm); + + struct + { + double value; + int rank; + } local_max{local.max_norm, mpi_rank}, global_max{}; + MPI_Allreduce(&local_max, &global_max, 1, MPI_DOUBLE_INT, MPI_MAXLOC, mpi_comm); + global.max_norm = global_max.value; + + int coverage_complete = local.coverage_complete ? 1 : 0; + MPI_Allreduce(MPI_IN_PLACE, &coverage_complete, 1, MPI_INT, MPI_MIN, mpi_comm); + global.coverage_complete = coverage_complete != 0; + + int worst_key[5] + = {local.worst_key.atom_i, + local.worst_key.atom_j, + local.worst_key.cell[0], + local.worst_key.cell[1], + local.worst_key.cell[2]}; + double worst_values[3] + = {local.worst_bare_norm, local.worst_gaussian_norm, local.worst_distance}; + MPI_Bcast(worst_key, 5, MPI_INT, global_max.rank, mpi_comm); + MPI_Bcast(worst_values, 3, MPI_DOUBLE, global_max.rank, mpi_comm); + global.worst_key = TailKey{worst_key[0], worst_key[1], {worst_key[2], worst_key[3], worst_key[4]}}; + global.worst_bare_norm = worst_values[0]; + global.worst_gaussian_norm = worst_values[1]; + global.worst_distance = worst_values[2]; + return global; +} +} // namespace EwaldVqDetail + +#endif diff --git a/source/source_lcao/module_ri/ewald_tail_utils.h b/source/source_lcao/module_ri/ewald_tail_utils.h new file mode 100644 index 00000000000..007d438182e --- /dev/null +++ b/source/source_lcao/module_ri/ewald_tail_utils.h @@ -0,0 +1,441 @@ +#ifndef EWALD_TAIL_UTILS_H +#define EWALD_TAIL_UTILS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Shared machinery of the Ewald short-range tail check. +// +// The split Ewald real-space term is a difference of two compact-support +// lattice sums, bare Coulomb minus Gaussian compensation. Inside the supports +// both blocks must exist as numerical tensors; beyond BOTH supports the two +// sums are described by the same analytic multipole expansion of the pair, +// so their difference cancels analytically and no block is needed. The tail +// check probes a guard shell of lattice cells around the production range, +// classifies every (I, J, R) block by that logic, and reduces the norm of +// the residual difference against tolerances. The verdict drives the +// exx_ewald_tail_check modes: warn reports and fails, enlarge adaptively +// expands the real-space range, strict fails without expansion budget. +namespace EwaldVqDetail +{ +using TailCell = std::array; + +// Behavior of the tail check (INPUT exx_ewald_tail_check). +enum class TailMode +{ + Off, + Warn, + Enlarge, + Strict +}; + +// Verdict after one probe round: accepted within tolerance, warn-only, +// expand the real-space range and reprobe, or hard failure. +enum class TailDecision +{ + Accept, + Warn, + Expand, + Fail +}; + +// Classification of one guard-shell block: +// Common - both tensors exist (or one is replaced by its +// multipole beyond its own support); numerical +// difference is formed. +// AnalyticCancellation - both supports exceeded: the bare and Gaussian +// multipoles coincide, difference is exactly zero. +// Incomplete - a tensor is missing inside its own support; +// the production range does not cover the shell. +enum class TailBlockCoverage +{ + Common, + AnalyticCancellation, + Incomplete +}; + +struct TailBlockPlan +{ + TailBlockCoverage coverage = TailBlockCoverage::Incomplete; + bool use_bare_multipole = false; + bool use_gaussian_multipole = false; +}; + +struct TailKey +{ + int atom_i = 0; + int atom_j = 0; + TailCell cell{0, 0, 0}; +}; + +inline bool operator==(const TailKey& lhs, const TailKey& rhs) +{ + return lhs.atom_i == rhs.atom_i && lhs.atom_j == rhs.atom_j && lhs.cell == rhs.cell; +} + +inline bool operator<(const TailKey& lhs, const TailKey& rhs) +{ + return std::tie(lhs.atom_i, lhs.atom_j, lhs.cell) + < std::tie(rhs.atom_i, rhs.atom_j, rhs.cell); +} + +struct TailTolerances +{ + double abs_tol = 1e-12; + double rel_tol = 1e-10; + double sum_rel_tol = 1e-8; +}; + +struct TailStats +{ + std::size_t production_blocks = 0; + std::size_t shell_blocks = 0; + double reference_norm = 0.0; + double max_norm = 0.0; + double sum_norm = 0.0; + double hermitian_residual = 0.0; + TailKey worst_key{}; + double worst_bare_norm = 0.0; + double worst_gaussian_norm = 0.0; + double worst_distance = 0.0; + bool coverage_complete = true; +}; + +struct TailSample +{ + TailKey key{}; + double bare_norm = 0.0; + double gaussian_norm = 0.0; + double difference_norm = 0.0; + double distance = 0.0; +}; + +inline TailMode parse_tail_mode(const std::string& mode) +{ + if (mode == "off") return TailMode::Off; + if (mode == "warn") return TailMode::Warn; + if (mode == "enlarge") return TailMode::Enlarge; + if (mode == "strict") return TailMode::Strict; + throw std::invalid_argument("Unknown Ewald tail-check mode: " + mode); +} + +inline const char* tail_mode_name(const TailMode mode) +{ + switch (mode) + { + case TailMode::Off: return "off"; + case TailMode::Warn: return "warn"; + case TailMode::Enlarge: return "enlarge"; + case TailMode::Strict: return "strict"; + } + return "unknown"; +} + +inline const char* tail_decision_name(const TailDecision decision) +{ + switch (decision) + { + case TailDecision::Accept: return "accepted"; + case TailDecision::Warn: return "warning"; + case TailDecision::Expand: return "expanded"; + case TailDecision::Fail: return "failed"; + } + return "unknown"; +} + +inline bool uses_adaptive_realspace_range(const TailMode mode) +{ + return mode == TailMode::Enlarge || mode == TailMode::Strict; +} + +inline double adaptive_exact_rmesh_times(const double requested, + const std::vector& lcaos_cutoff, + const std::vector& abfs_cutoff) +{ + if (requested <= 0.0) + { + throw std::invalid_argument("Ewald radial-mesh factor must be positive"); + } + if (lcaos_cutoff.size() != abfs_cutoff.size()) + { + throw std::invalid_argument("LCAO and ABFS cutoff arrays must have the same size"); + } + + // A factor of one is also the minimum exact Gaussian pair range. For the + // bare pair, require c*r_I^ABFS + r_J^LCAO to reach the ABFS nonoverlap + // boundary r_I^ABFS + r_J^ABFS for every ordered type pair. + double exact = std::max(requested, 1.0); + for (std::size_t type_i = 0; type_i != abfs_cutoff.size(); ++type_i) + { + if (abfs_cutoff[type_i] <= 0.0) continue; + for (std::size_t type_j = 0; type_j != abfs_cutoff.size(); ++type_j) + { + const double required = (abfs_cutoff[type_i] + abfs_cutoff[type_j] + - lcaos_cutoff[type_j]) + / abfs_cutoff[type_i]; + exact = std::max(exact, required); + } + } + return exact; +} + +inline void validate_odd_period(const TailCell& period) +{ + for (const int value: period) + { + if (value <= 0 || value % 2 == 0) + { + throw std::invalid_argument("Ewald real-space periods must be positive odd integers"); + } + } +} + +inline TailCell expand_odd_period(const TailCell& period, + const std::array& periodic, + const int guard_cells) +{ + validate_odd_period(period); + if (guard_cells < 0) + { + throw std::invalid_argument("Ewald tail guard cells must be non-negative"); + } + TailCell expanded = period; + for (std::size_t dim = 0; dim != expanded.size(); ++dim) + { + if (periodic[dim]) expanded[dim] += 2 * guard_cells; + } + return expanded; +} + +inline bool cell_in_period(const TailCell& cell, const TailCell& period) +{ + validate_odd_period(period); + for (std::size_t dim = 0; dim != cell.size(); ++dim) + { + if (cell[dim] < -period[dim] / 2 || cell[dim] > period[dim] / 2) return false; + } + return true; +} + +inline std::vector make_shell_cells(const TailCell& production_period, + const TailCell& probe_period) +{ + validate_odd_period(production_period); + validate_odd_period(probe_period); + for (std::size_t dim = 0; dim != production_period.size(); ++dim) + { + if (probe_period[dim] < production_period[dim]) + { + throw std::invalid_argument("Ewald probe period cannot be smaller than production period"); + } + } + + std::vector shell; + for (int x = -probe_period[0] / 2; x <= probe_period[0] / 2; ++x) + { + for (int y = -probe_period[1] / 2; y <= probe_period[1] / 2; ++y) + { + for (int z = -probe_period[2] / 2; z <= probe_period[2] / 2; ++z) + { + const TailCell cell{x, y, z}; + if (!cell_in_period(cell, production_period)) shell.push_back(cell); + } + } + } + return shell; +} + +inline std::vector make_shell_keys(const std::vector& atoms, + const TailCell& production_period, + const TailCell& probe_period) +{ + const auto cells = make_shell_cells(production_period, probe_period); + std::vector keys; + keys.reserve(atoms.size() * atoms.size() * cells.size()); + for (const int atom_i: atoms) + { + for (const int atom_j: atoms) + { + for (const TailCell& cell: cells) + { + keys.push_back(TailKey{atom_i, atom_j, cell}); + } + } + } + return keys; +} + +inline TailKey hermitian_partner(const TailKey& key) +{ + return TailKey{key.atom_j, + key.atom_i, + TailCell{-key.cell[0], -key.cell[1], -key.cell[2]}}; +} + +inline TailKey canonical_hermitian_key(const TailKey& key) +{ + const TailKey partner = hermitian_partner(key); + return partner < key ? partner : key; +} + +// FNV-1a hash of the canonical (Hermitian-symmetrized) key, so that the I,J +// and J,I^-R partners always land on the same owner rank and the residual +// hermitian check never needs cross-rank pairing. +inline std::uint64_t stable_tail_key_hash(const TailKey& key) +{ + const TailKey canonical = canonical_hermitian_key(key); + std::uint64_t hash = 1469598103934665603ULL; + const auto mix = [&hash](const std::int64_t value) + { + const std::uint64_t encoded = static_cast(value); + for (int byte = 0; byte != 8; ++byte) + { + hash ^= (encoded >> (8 * byte)) & 0xffULL; + hash *= 1099511628211ULL; + } + }; + mix(canonical.atom_i); + mix(canonical.atom_j); + for (const int value: canonical.cell) mix(value); + return hash; +} + +inline int shell_owner(const TailKey& key, const int mpi_size) +{ + if (mpi_size <= 0) throw std::invalid_argument("MPI size must be positive"); + return static_cast(stable_tail_key_hash(key) % static_cast(mpi_size)); +} + +inline double tail_reference_scale(const TailStats& stats) +{ + return std::max(stats.reference_norm, std::numeric_limits::min()); +} + +inline double relative_tail_max(const TailStats& stats) +{ + return stats.max_norm / tail_reference_scale(stats); +} + +inline double relative_tail_sum(const TailStats& stats) +{ + return stats.sum_norm / tail_reference_scale(stats); +} + +inline bool tail_passes(const TailStats& stats, const TailTolerances& tolerances) +{ + if (!stats.coverage_complete) return false; + const double reference = tail_reference_scale(stats); + return stats.max_norm <= tolerances.abs_tol + tolerances.rel_tol * reference + && stats.sum_norm <= tolerances.sum_rel_tol * reference + && stats.hermitian_residual <= tolerances.abs_tol + tolerances.rel_tol * reference; +} + +// Policy mapping one round of shell statistics to a decision. A broken +// Hermitian residual always fails; incomplete coverage fails immediately in +// warn mode or after the expansion budget is spent, and expands otherwise. +inline TailDecision decide_tail(const TailStats& stats, + const TailTolerances& tolerances, + const TailMode mode, + const int expansions, + const int max_expansions) +{ + if (mode == TailMode::Off) return TailDecision::Accept; + const double hermitian_limit + = tolerances.abs_tol + tolerances.rel_tol * tail_reference_scale(stats); + if (stats.hermitian_residual > hermitian_limit) + { + return TailDecision::Fail; + } + if (!stats.coverage_complete) + { + if (mode == TailMode::Warn || expansions >= max_expansions) + { + return TailDecision::Fail; + } + return TailDecision::Expand; + } + if (tail_passes(stats, tolerances)) return TailDecision::Accept; + if (mode == TailMode::Warn) return TailDecision::Warn; + if (expansions >= max_expansions) + { + return TailDecision::Fail; + } + return TailDecision::Expand; +} + +inline TailBlockCoverage classify_tail_block(const bool has_bare, + const bool has_gaussian, + const bool far_multipoles_equivalent) +{ + if (has_bare && has_gaussian) return TailBlockCoverage::Common; + if (far_multipoles_equivalent) return TailBlockCoverage::AnalyticCancellation; + return TailBlockCoverage::Incomplete; +} + +// Decision table for one guard-shell block. A tensor that is absent beyond +// its own support is fine (the multipole replaces it); absent inside its +// support makes the block Incomplete. When both supports are exceeded the +// plan flags analytic cancellation so no numerical difference is formed. +inline TailBlockPlan plan_tail_block(const bool has_bare, + const bool has_gaussian, + const double distance, + const double bare_support, + const double gaussian_support) +{ + const bool use_bare_multipole = distance >= bare_support; + const bool use_gaussian_multipole = distance >= gaussian_support; + if ((!has_bare && !use_bare_multipole) || (!has_gaussian && !use_gaussian_multipole)) + { + return TailBlockPlan{}; + } + if (use_bare_multipole && use_gaussian_multipole) + { + return TailBlockPlan{TailBlockCoverage::AnalyticCancellation, false, false}; + } + return TailBlockPlan{TailBlockCoverage::Common, + use_bare_multipole, + use_gaussian_multipole}; +} + +inline void accumulate_tail_sample(TailStats& stats, const TailSample& sample) +{ + ++stats.shell_blocks; + stats.sum_norm += sample.difference_norm; + if (sample.difference_norm > stats.max_norm) + { + stats.max_norm = sample.difference_norm; + stats.worst_key = sample.key; + stats.worst_bare_norm = sample.bare_norm; + stats.worst_gaussian_norm = sample.gaussian_norm; + stats.worst_distance = sample.distance; + } +} + +inline std::map>> +group_tail_keys_by_atom(const std::vector& keys) +{ + std::map>> grouped; + for (const TailKey& key: keys) + { + grouped[key.atom_i].emplace_back(key.atom_j, key.cell); + } + return grouped; +} + +inline bool far_multipoles_equivalent(const double distance, + const double bare_support_radius, + const double gaussian_support_radius) +{ + return distance >= bare_support_radius && distance >= gaussian_support_radius; +} +} // namespace EwaldVqDetail + +#endif diff --git a/source/source_lcao/module_ri/ewald_vq.h b/source/source_lcao/module_ri/ewald_vq.h index f717d54206c..50c16f01c4c 100644 --- a/source/source_lcao/module_ri/ewald_vq.h +++ b/source/source_lcao/module_ri/ewald_vq.h @@ -7,6 +7,8 @@ #define EWALD_VQ_H #include "lri_cv.h" +#include "direct_2d_coulomb.h" +#include "ewald_tail_utils.h" #include "gaussian_abfs.h" #include "source_base/element_basis_index.h" #include "source_cell/klist.h" @@ -15,8 +17,10 @@ #include #include +#include #include #include +#include template class Ewald_Vq @@ -47,6 +51,8 @@ class Ewald_Vq using TAK = std::pair; public: + void set_info_ri(const Exx_Info_RI* p) { p_info_ri = p; } + void init(const UnitCell& ucell, const LCAO_Orbitals& orb, const MPI_Comm& mpi_comm_in, @@ -56,13 +62,24 @@ class Ewald_Vq const std::map>> &coulomb_param_in, std::shared_ptr MGT_in, const double &ccp_rmesh_times_in, + const double &exact_ccp_rmesh_times_in, + const double &ewald_lambda_in, const double &kmesh_times_in, - const int abfs_Lmax_in); + const int& ewald_dimension_in = 3, + const ModuleBase::Element_Basis_Index::IndexPermutation &abfs_old_to_new = {}); void init_ions(const UnitCell& ucell, const std::array& period_Vs_NAO); + inline double get_exact_ccp_rmesh_times() const { return this->exact_ccp_rmesh_times; } + double get_singular_chi(const UnitCell& ucell, const std::vector>& param_list, const double& qdiv); + void output_direct_2d_coulomb(const UnitCell& ucell, + double ecut_ry, + int kz_order, + int gamma_order); + void output_direct_3d_coulomb(const UnitCell& ucell, double ecut_ry); + inline std::map>>> cal_Vq( const UnitCell& ucell, const double& chi, @@ -75,14 +92,58 @@ class Ewald_Vq inline std::map>> cal_Vs(const UnitCell& ucell, const double& chi, std::map>>& Vs_in); + inline std::pair>>, + std::map>>> + cal_Vs_split(const UnitCell& ucell, + const double& chi, + std::map>>& Vs_in); + inline std::map>> cal_Vs_serial_full( + const UnitCell& ucell, + const double& chi, + std::map>>& Vs_in_full, + const std::array& period_Vs_NAO); inline std::map, Ndim>>> cal_dVs( const UnitCell& ucell, const double& chi, std::map, Ndim>>>& dVs_in); + inline std::map>> cal_short_range_Vs( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in); + inline std::map>> cal_bare_periodic_Vs( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in); + inline std::map>> cal_short_range_Vs_serial_full( + const UnitCell& ucell, + std::map>>& Vs_in_full, + const std::array& period_Vs_NAO); + inline std::map>> cal_long_range_Vs_gauss( + const UnitCell& ucell, + const double& chi); + inline std::map>> cal_long_range_Vs_gauss_serial_full( + const UnitCell& ucell, + const double& chi, + const std::array& period_Vs_NAO); + + inline std::map>> cal_realspace_difference( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in, + bool truncate_analytic_tail); + inline EwaldVqDetail::TailStats cal_short_range_tail_stats( + const UnitCell& ucell, + const std::vector& local_keys, + std::map>>& Vs_bare, + std::size_t local_production_blocks, + double local_reference_norm); private: double ccp_rmesh_times; - int abfs_Lmax = 0; + double exact_ccp_rmesh_times; LRI_CV cv; Gaussian_Abfs gaussian_abfs; const K_Vectors* p_kv = nullptr; @@ -91,13 +152,20 @@ class Ewald_Vq MPI_Comm mpi_comm; ModuleBase::realArray gaunt; std::array nmp; - const double ewald_lambda = 1.0; + double ewald_lambda = 1.0; + int ewald_dimension = 3; + int abfs_Lmax = 0; std::vector>> multipole; ModuleBase::Element_Basis_Index::IndexLNM index_abfs; + std::vector>> abfs; + ModuleBase::Element_Basis_Index::IndexPermutation abfs_old_to_new; + double bare_multipole_scale = 1.0; std::vector lcaos_rcut; + std::vector abfs_rcut; std::vector g_lcaos_rcut; + std::vector g_abfs_rcut; std::vector g_abfs_ccp_rcut; std::map>> coulomb_param; @@ -107,6 +175,7 @@ class Ewald_Vq std::vector>> g_lcaos; std::vector>> g_abfs; std::vector>> g_abfs_ccp; + EwaldVqDetail::TailMode tail_mode = EwaldVqDetail::TailMode::Warn; /* MPI distribute @@ -146,13 +215,29 @@ class Ewald_Vq const UnitCell& ucell, const std::vector& list_A0, const std::vector& list_A1, - std::map>>& Vs_in); + std::map>>& Vs_in, + const bool truncate_analytic_tail = false); inline std::map, Ndim>>> cal_dVs_minus_gauss( const UnitCell& ucell, const std::vector& list_A0, const std::vector& list_A1, std::map, Ndim>>>& dVs_in); template + std::map> project_Vs_dVs_gauss( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>& Vs_dVs_gauss_in); + inline std::map>> cal_analytic_multipole_Vs( + const UnitCell& ucell, + const std::vector& keys); + inline std::map>> set_Vs_minus_gauss_common_candidates( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in, + std::map>>& Vs_gauss_in); + template std::map> set_Vs_dVs_minus_gauss(const UnitCell& ucell, const std::vector& list_A0, const std::vector& list_A1, @@ -225,7 +310,8 @@ class Ewald_Vq inline double cal_V_Rcut(const int it0, const int it1); inline double get_Rcut_max(const int it0, const int it1); + const Exx_Info_RI* p_info_ri = nullptr; }; #include "ewald_vq.hpp" -#endif \ No newline at end of file +#endif diff --git a/source/source_lcao/module_ri/ewald_vq.hpp b/source/source_lcao/module_ri/ewald_vq.hpp index d368b5565c7..eace42462ad 100644 --- a/source/source_lcao/module_ri/ewald_vq.hpp +++ b/source/source_lcao/module_ri/ewald_vq.hpp @@ -7,6 +7,7 @@ #define EWALD_VQ_HPP #include +#include #include #include @@ -14,7 +15,9 @@ #include "ri_2d_comm.h" #include "ri_util.h" #include "conv_coulomb_pot_k.h" +#include "ewald_mpi_utils.h" #include "exx_abfs_ctor_orbs.h" +#include "exx_rotate_abfs.h" #include "gaussian_abfs.h" #include "source_basis/module_ao/elem_basis_idx_orb.h" #include "source_base/element_basis_index.h" @@ -22,7 +25,32 @@ #include "source_base/tool_title.h" #include "singular_value.h" +#include #include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace EwaldVqDetail +{ +template +inline RI::Tensor tensor_adjoint(const RI::Tensor& tensor) +{ + return tensor.transpose(); +} + +inline RI::Tensor> tensor_adjoint(const RI::Tensor>& tensor) +{ + return tensor.dagger(); +} +} // namespace EwaldVqDetail + +#include "direct_2d_coulomb_ewald.hpp" +#include "direct_3d_coulomb_ewald.hpp" template Ewald_Vq::Ewald_Vq(){} @@ -40,8 +68,11 @@ void Ewald_Vq::init(const UnitCell& ucell, const std::map>> &coulomb_param_in, std::shared_ptr MGT_in, const double &ccp_rmesh_times_in, + const double &exact_ccp_rmesh_times_in, + const double &ewald_lambda_in, const double &kmesh_times_in, - const int abfs_Lmax_in) + const int& ewald_dimension_in, + const ModuleBase::Element_Basis_Index::IndexPermutation &abfs_old_to_new) { ModuleBase::TITLE("Ewald_Vq", "init"); ModuleBase::timer::start("Ewald_Vq", "init"); @@ -51,24 +82,65 @@ void Ewald_Vq::init(const UnitCell& ucell, this->nks0 = this->p_kv->get_nkstot_nospin(); this->kvec_c.resize(this->nks0); this->ccp_rmesh_times = ccp_rmesh_times_in; - this->abfs_Lmax = abfs_Lmax_in; + this->exact_ccp_rmesh_times = exact_ccp_rmesh_times_in; + this->ewald_lambda = ewald_lambda_in; + if (this->p_info_ri == nullptr) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::init", "set_info_ri must be called before init"); + } + this->tail_mode = EwaldVqDetail::parse_tail_mode(this->p_info_ri->ewald_tail_check); this->coulomb_param = coulomb_param_in; + this->ewald_dimension = ewald_dimension_in; + if (this->ewald_dimension != 2 && this->ewald_dimension != 3) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::init", "ewald_dimension must be 2 or 3"); + } this->g_lcaos = this->init_gauss(lcaos_in); this->g_abfs = this->init_gauss(abfs_in); this->g_abfs_ccp = Conv_Coulomb_Pot_K::cal_orbs_ccp(this->g_abfs, this->coulomb_param, - this->ccp_rmesh_times); + this->exact_ccp_rmesh_times); this->multipole = Exx_Abfs::Construct_Orbs::get_multipole(abfs_in); + this->abfs = abfs_in; + this->abfs_Lmax = 0; + for (const auto& type_orbs : abfs_in) + { + this->abfs_Lmax = std::max(this->abfs_Lmax, static_cast(type_orbs.size()) - 1); + } + this->abfs_old_to_new = abfs_old_to_new; + this->bare_multipole_scale = 0.0; + const auto fock_params = this->coulomb_param.find(Conv_Coulomb_Pot_K::Coulomb_Type::Fock); + if (fock_params != this->coulomb_param.end()) + { + for (const auto& param: fock_params->second) + { + const auto alpha = param.find("alpha"); + if (alpha != param.end() && !alpha->second.empty()) + { + this->bare_multipole_scale += std::stod(alpha->second); + } + } + } this->lcaos_rcut = Exx_Abfs::Construct_Orbs::get_Rcut(lcaos_in); + this->abfs_rcut = Exx_Abfs::Construct_Orbs::get_Rcut(abfs_in); this->g_lcaos_rcut = Exx_Abfs::Construct_Orbs::get_Rcut(this->g_lcaos); + this->g_abfs_rcut = Exx_Abfs::Construct_Orbs::get_Rcut(this->g_abfs); this->g_abfs_ccp_rcut = Exx_Abfs::Construct_Orbs::get_Rcut(this->g_abfs_ccp); const ModuleBase::Element_Basis_Index::Range range_abfs = ModuleBase::Element_Basis_Index::construct_range(abfs_in); - this->index_abfs = ModuleBase::Element_Basis_Index::construct_index(range_abfs); + this->index_abfs = ModuleBase::Element_Basis_Index::construct_index(range_abfs, abfs_old_to_new); this->cv - .set_orbitals(ucell, orb, this->g_lcaos, this->g_abfs, this->g_abfs_ccp, kmesh_times_in, MGT_in, false); + .set_orbitals(ucell, + orb, + this->g_lcaos, + this->g_abfs, + this->g_abfs_ccp, + kmesh_times_in, + MGT_in, + false, + abfs_old_to_new); this->gaunt.create(MGT_in->Gaunt_Coefficients.getBound1(), MGT_in->Gaunt_Coefficients.getBound2(), MGT_in->Gaunt_Coefficients.getBound3()); @@ -77,6 +149,35 @@ void Ewald_Vq::init(const UnitCell& ucell, this->atoms_vec.resize(ucell.nat); std::iota(this->atoms_vec.begin(), this->atoms_vec.end(), 0); this->nmp = {this->p_kv->nmp[0], this->p_kv->nmp[1], this->p_kv->nmp[2]}; + if (this->ewald_dimension == 2) + { + const double eps = 1e-10; + const ModuleBase::Vector3 a1 = ucell.a1; + const ModuleBase::Vector3 a2 = ucell.a2; + const ModuleBase::Vector3 a3 = ucell.a3; + const double a3_norm = a3.norm(); + if (this->nmp[2] != 1) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::init", "2D Ewald requires nkz = 1"); + } + for (size_t ik = 0; ik != this->p_kv->kvec_c_full.size(); ++ik) + { + if (std::abs(this->p_kv->kvec_c_full[ik].z) > eps) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::init", "2D Ewald requires zero k-point offset along z"); + } + } + if (std::abs(a1.z) > eps || std::abs(a2.z) > eps || std::abs(a3.x) > eps || std::abs(a3.y) > eps + || std::abs(a1 * a3) > eps * a1.norm() * a3_norm + || std::abs(a2 * a3) > eps * a2.norm() * a3_norm) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::init", "2D Ewald requires a slab cell with the non-periodic direction along z"); + } + if (PARAM.inp.cal_force || PARAM.inp.cal_stress) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::init", "2D Ewald force/stress derivatives are not implemented"); + } + } ModuleBase::timer::end("Ewald_Vq", "init"); } @@ -87,11 +188,13 @@ void Ewald_Vq::init_ions(const UnitCell& ucell, const std::array period_Vs - = LRI_CV_Tools::cal_latvec_range(1 + this->ccp_rmesh_times, ucell, this->g_lcaos_rcut); - + // The bare and Gaussian real-space maps are subtracted locally below. + // Enumerate both with the bare-Coulomb period so every common key has the + // same MPI owner; the Gaussian cutoff is still applied when building it. const std::pair, std::vector>>>> list_As_Vs - = RI::Distribute_Equally::distribute_atoms_periods(this->mpi_comm, this->atoms_vec, period_Vs, 2, false); + = EwaldVqDetail::distribute_common_realspace_tasks(this->mpi_comm, + this->atoms_vec, + period_Vs_NAO); this->list_A0 = list_As_Vs.first; this->list_A1 = list_As_Vs.second[0]; @@ -138,14 +241,25 @@ double Ewald_Vq::get_singular_chi(const UnitCell& ucell, const std::vecto double chi = 0.0; for(const auto ¶m : param_list) - { + { if(param.at("singularity_correction") == "carrier") - { + { + if (this->ewald_dimension == 2) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::get_singular_chi", "2D Ewald supports massidda correction only"); + } chi = Singular_Value::cal_carrier(ucell, this->kvec_c, qdiv, 100, 30, 1e-6, 3); } else if(param.at("singularity_correction") == "massidda") - { - chi = Singular_Value::cal_massidda(ucell, this->nmp, qdiv, 1, 5, 1e-4); + { + if (this->ewald_dimension == 2) + { + chi = Singular_Value::cal_massidda_2d(ucell, this->nmp, 1.0, 5, 1e-4); + } + else + { + chi = Singular_Value::cal_massidda(ucell, this->nmp, qdiv, 1, 5, 1e-4); + } } else { @@ -194,15 +308,31 @@ template auto Ewald_Vq::cal_Vs_minus_gauss(const UnitCell& ucell, const std::vector& list_A0, const std::vector& list_A1, - std::map>>& Vs_in) + std::map>>& Vs_in, + const bool truncate_analytic_tail) -> std::map>> { ModuleBase::TITLE("Ewald_Vq", "cal_Vs_minus_gauss"); ModuleBase::timer::start("Ewald_Vq", "cal_Vs_minus_gauss"); std::map>> Vs_gauss = this->cal_Vs_gauss(ucell, list_A0, list_A1); - std::map>> Vs_minus_gauss - = this->set_Vs_dVs_minus_gauss(ucell, list_A0, list_A1, Vs_in, Vs_gauss); + std::map>> Vs_minus_gauss; + if (truncate_analytic_tail) + { + Vs_minus_gauss = this->set_Vs_minus_gauss_common_candidates(ucell, + list_A0, + list_A1, + Vs_in, + Vs_gauss); + } + else + { + Vs_minus_gauss = this->set_Vs_dVs_minus_gauss(ucell, + list_A0, + list_A1, + Vs_in, + Vs_gauss); + } ModuleBase::timer::end("Ewald_Vq", "cal_Vs_minus_gauss"); return Vs_minus_gauss; @@ -218,7 +348,8 @@ auto Ewald_Vq::cal_dVs_minus_gauss(const UnitCell& ucell, ModuleBase::TITLE("Ewald_Vq", "cal_dVs_minus_gauss"); ModuleBase::timer::start("Ewald_Vq", "cal_dVs_minus_gauss"); - std::map, Ndim>>> dVs_gauss = this->cal_dVs_gauss(list_A0, list_A1); + std::map, Ndim>>> dVs_gauss + = this->cal_dVs_gauss(ucell, list_A0, list_A1); std::map, Ndim>>> dVs_minus_gauss = this->set_Vs_dVs_minus_gauss(ucell, list_A0, list_A1, dVs_in, dVs_gauss); @@ -242,22 +373,17 @@ double Ewald_Vq::get_Rcut_max(const int it0, const int it1) template template -auto Ewald_Vq::set_Vs_dVs_minus_gauss(const UnitCell& ucell, - const std::vector& list_A0, - const std::vector& list_A1, - std::map>& Vs_dVs_in, - std::map>& Vs_dVs_gauss_in) +auto Ewald_Vq::project_Vs_dVs_gauss(const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>& Vs_dVs_gauss_in) -> std::map> { - ModuleBase::TITLE("Ewald_Vq", "set_Vs_dVs_minus_gauss"); - ModuleBase::timer::start("Ewald_Vq", "set_Vs_dVs_minus_gauss"); - using Tin_convert = typename LRI_CV_Tools::TinType::type; std::map> pVs_dVs_gauss; -#pragma omp parallel +#pragma omp parallel for collapse(2) schedule(dynamic) for (size_t i0 = 0; i0 < list_A0.size(); ++i0) { -#pragma omp for schedule(dynamic) nowait for (size_t i1 = 0; i1 < list_A1.size(); ++i1) { const TA iat0 = list_A0[i0]; @@ -319,9 +445,222 @@ auto Ewald_Vq::set_Vs_dVs_minus_gauss(const UnitCell& ucell, } } - std::map> Vs_dVs_minus_gauss = LRI_CV_Tools::minus(Vs_dVs_in, pVs_dVs_gauss); + return pVs_dVs_gauss; +} + +template +auto Ewald_Vq::cal_analytic_multipole_Vs( + const UnitCell& ucell, + const std::vector& keys) + -> std::map>> +{ + std::map>> multipole_blocks; + if (keys.empty()) return multipole_blocks; + if (std::abs(this->bare_multipole_scale) <= std::numeric_limits::epsilon()) + { + throw std::invalid_argument("Failed to determine the Ewald Fock alpha for analytic multipole evaluation."); + } + + if (this->p_info_ri == nullptr) + { + throw std::invalid_argument("Ewald_Vq multipole tail evaluation requires set_info_ri."); + } + Moment_abfs moment_abfs(*this->p_info_ri); + moment_abfs.cal_multipole(this->abfs); + const std::vector zero_cutoff(this->abfs.size(), 0.0); + const auto grouped = EwaldVqDetail::group_tail_keys_by_atom(keys); + for (typename std::map>::const_iterator grouped_it = grouped.begin(); + grouped_it != grouped.end(); + ++grouped_it) + { + std::vector nonzero_keys; + const int iat0 = grouped_it->first; + const int it0 = ucell.iat2it[iat0]; + const int ia0 = ucell.iat2ia[iat0]; + const auto tau0 = ucell.atoms[it0].tau[ia0]; + for (const TAC& jr: grouped_it->second) + { + const int it1 = ucell.iat2it[jr.first]; + const int ia1 = ucell.iat2ia[jr.first]; + const auto tau1 = ucell.atoms[it1].tau[ia1]; + const auto delta = -tau0 + tau1 + (RI_Util::array3_to_Vector3(jr.second) * ucell.latvec); + if (delta.norm() * ucell.lat0 > 1e-12) nonzero_keys.push_back(jr); + } + if (nonzero_keys.empty()) continue; + + std::map>> part; + moment_abfs.cal_VR(ucell, + this->abfs, + std::make_pair(std::vector{iat0}, std::vector>{nonzero_keys}), + zero_cutoff, + 0.0, + this->cv, + part, + this->abfs_old_to_new, + false, + false, + false, + true, + this->bare_multipole_scale, + true); + for (typename std::map>>::const_iterator part_it = part.begin(); + part_it != part.end(); + ++part_it) + { + multipole_blocks[part_it->first].insert(part_it->second.begin(), part_it->second.end()); + } + } + return multipole_blocks; +} + +template +auto Ewald_Vq::set_Vs_minus_gauss_common_candidates( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in, + std::map>>& Vs_gauss_in) + -> std::map>> +{ + const auto projected_gaussian = this->project_Vs_dVs_gauss(ucell, list_A0, list_A1, Vs_gauss_in); + const auto find_tensor = [](const auto& tensor_map, const EwaldVqDetail::TailKey& key) + -> const RI::Tensor* + { + const auto outer = tensor_map.find(key.atom_i); + if (outer == tensor_map.end()) return nullptr; + const TAC jr{key.atom_j, key.cell}; + const auto inner = outer->second.find(jr); + return inner == outer->second.end() ? nullptr : &inner->second; + }; + + std::vector multipole_keys; + bool local_complete = true; + EwaldVqDetail::TailKey first_incomplete_key{}; + double first_incomplete_distance = 0.0; + double first_incomplete_bare_support = 0.0; + double first_incomplete_gaussian_support = 0.0; + bool first_incomplete_has_bare = false; + bool first_incomplete_has_gaussian = false; + for (const TA iat0: list_A0) + { + const int it0 = ucell.iat2it[iat0]; + const int ia0 = ucell.iat2ia[iat0]; + const auto tau0 = ucell.atoms[it0].tau[ia0]; + for (const TAC& jr: list_A1) + { + const EwaldVqDetail::TailKey key{iat0, jr.first, jr.second}; + const int it1 = ucell.iat2it[jr.first]; + const int ia1 = ucell.iat2ia[jr.first]; + const auto tau1 = ucell.atoms[it1].tau[ia1]; + const auto delta = -tau0 + tau1 + (RI_Util::array3_to_Vector3(jr.second) * ucell.latvec); + const double distance = delta.norm() * ucell.lat0; + const double bare_support = this->abfs_rcut[it0] + this->abfs_rcut[it1]; + // The projected Gaussian tensor exists only inside the RI integral pipeline's + // own extent (cal_V_Rcut); beyond it the projection is identically zero, so + // the raw g_abfs radius sum would demand blocks that are never materialized. + const double gaussian_support = std::min(this->cal_V_Rcut(it0, it1), this->cal_V_Rcut(it1, it0)); + const auto plan = EwaldVqDetail::plan_tail_block(find_tensor(Vs_in, key) != nullptr, + find_tensor(projected_gaussian, key) != nullptr, + distance, + bare_support, + gaussian_support); + if (plan.coverage == EwaldVqDetail::TailBlockCoverage::Incomplete) + { + if (local_complete) + { + first_incomplete_key = key; + first_incomplete_distance = distance; + first_incomplete_bare_support = bare_support; + first_incomplete_gaussian_support = gaussian_support; + first_incomplete_has_bare = find_tensor(Vs_in, key) != nullptr; + first_incomplete_has_gaussian = find_tensor(projected_gaussian, key) != nullptr; + } + local_complete = false; + } + else if (plan.use_bare_multipole || plan.use_gaussian_multipole) + { + multipole_keys.push_back(key); + } + } + } + int complete = local_complete ? 1 : 0; + MPI_Allreduce(MPI_IN_PLACE, &complete, 1, MPI_INT, MPI_MIN, this->mpi_comm); + if (complete == 0) + { + if (!local_complete) + { + int mpi_rank = 0; + MPI_Comm_rank(this->mpi_comm, &mpi_rank); + std::cerr << "Ewald candidate coverage failure on rank " << mpi_rank << ": I,J,R=" + << first_incomplete_key.atom_i << ' ' << first_incomplete_key.atom_j << ' ' + << first_incomplete_key.cell[0] << ' ' << first_incomplete_key.cell[1] << ' ' + << first_incomplete_key.cell[2] << ", distance=" << first_incomplete_distance + << ", bare_support=" << first_incomplete_bare_support + << ", Gaussian_support=" << first_incomplete_gaussian_support + << ", has_bare=" << (first_incomplete_has_bare ? "true" : "false") + << ", has_Gaussian=" << (first_incomplete_has_gaussian ? "true" : "false") + << std::endl; + } + throw std::runtime_error("An Ewald candidate block is missing inside its compact support."); + } + + const auto multipole_blocks = this->cal_analytic_multipole_Vs(ucell, multipole_keys); + std::map>> difference; + for (const TA iat0: list_A0) + { + const int it0 = ucell.iat2it[iat0]; + const int ia0 = ucell.iat2ia[iat0]; + const auto tau0 = ucell.atoms[it0].tau[ia0]; + for (const TAC& jr: list_A1) + { + const EwaldVqDetail::TailKey key{iat0, jr.first, jr.second}; + const int it1 = ucell.iat2it[jr.first]; + const int ia1 = ucell.iat2ia[jr.first]; + const auto tau1 = ucell.atoms[it1].tau[ia1]; + const auto delta = -tau0 + tau1 + (RI_Util::array3_to_Vector3(jr.second) * ucell.latvec); + const double distance = delta.norm() * ucell.lat0; + const double bare_support = this->abfs_rcut[it0] + this->abfs_rcut[it1]; + // The projected Gaussian tensor exists only inside the RI integral pipeline's + // own extent (cal_V_Rcut); beyond it the projection is identically zero, so + // the raw g_abfs radius sum would demand blocks that are never materialized. + const double gaussian_support = std::min(this->cal_V_Rcut(it0, it1), this->cal_V_Rcut(it1, it0)); + const RI::Tensor* bare = find_tensor(Vs_in, key); + const RI::Tensor* gaussian = find_tensor(projected_gaussian, key); + const auto plan = EwaldVqDetail::plan_tail_block(bare != nullptr, + gaussian != nullptr, + distance, + bare_support, + gaussian_support); + if (plan.coverage == EwaldVqDetail::TailBlockCoverage::AnalyticCancellation) continue; + const RI::Tensor* analytic = find_tensor(multipole_blocks, key); + if (plan.use_bare_multipole) bare = analytic; + if (plan.use_gaussian_multipole) gaussian = analytic; + if (bare == nullptr || gaussian == nullptr) + { + throw std::runtime_error("Failed to evaluate an Ewald analytic multipole block."); + } + difference[iat0][jr] = *bare - *gaussian; + } + } + return difference; +} + +template +template +auto Ewald_Vq::set_Vs_dVs_minus_gauss(const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>& Vs_dVs_in, + std::map>& Vs_dVs_gauss_in) + -> std::map> +{ + ModuleBase::TITLE("Ewald_Vq", "set_Vs_dVs_minus_gauss"); + ModuleBase::timer::start("Ewald_Vq", "set_Vs_dVs_minus_gauss"); + + auto pVs_dVs_gauss = this->project_Vs_dVs_gauss(ucell, list_A0, list_A1, Vs_dVs_gauss_in); + auto difference = LRI_CV_Tools::minus_common_keys(Vs_dVs_in, pVs_dVs_gauss); ModuleBase::timer::end("Ewald_Vq", "set_Vs_dVs_minus_gauss"); - return Vs_dVs_minus_gauss; + return difference; } template @@ -335,14 +674,29 @@ auto Ewald_Vq::cal_Vq_gauss(const UnitCell& ucell, ModuleBase::TITLE("Ewald_Vq", "cal_Vq_gauss"); ModuleBase::timer::start("Ewald_Vq", "cal_Vq_gauss"); - const T_func_DPget_Vq_dVq>> func_DPget_Vq = std::bind(&Gaussian_Abfs::get_Vq, - &this->gaussian_abfs, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - chi, - std::placeholders::_4, - this->gaunt); + T_func_DPget_Vq_dVq>> func_DPget_Vq; + if (this->ewald_dimension == 2) + { + func_DPget_Vq = std::bind(&Gaussian_Abfs::get_Vq_2d, + &this->gaussian_abfs, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + chi, + std::placeholders::_4, + this->gaunt); + } + else + { + func_DPget_Vq = std::bind(&Gaussian_Abfs::get_Vq, + &this->gaussian_abfs, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + chi, + std::placeholders::_4, + this->gaunt); + } auto Vq_gauss = this->set_Vq_dVq_gauss(ucell, list_A0_k, list_A1_k, shift_for_mpi, func_DPget_Vq); ModuleBase::timer::end("Ewald_Vq", "cal_Vq_gauss"); @@ -360,6 +714,11 @@ auto Ewald_Vq::cal_dVq_gauss(const UnitCell& ucell, ModuleBase::TITLE("Ewald_Vq", "cal_dVq_gauss"); ModuleBase::timer::start("Ewald_Vq", "cal_dVq_gauss"); + if (this->ewald_dimension == 2) + { + ModuleBase::WARNING_QUIT("Ewald_Vq::cal_dVq_gauss", "2D Ewald force/stress derivatives are not implemented"); + } + const T_func_DPget_Vq_dVq>, Ndim>> func_DPget_dVq = std::bind(&Gaussian_Abfs::get_dVq, &this->gaussian_abfs, @@ -413,16 +772,15 @@ auto Ewald_Vq::set_Vq_dVq_gauss(const UnitCell& ucell, std::map> Vq_dVq_gauss_out; for(const auto ¶m_list : this->coulomb_param) - { + { std::complex alpha; for(const auto ¶m : param_list.second) { alpha = std::complex(std::stod(param.at("alpha")), 0); } -#pragma omp parallel +#pragma omp parallel for collapse(2) schedule(dynamic) for (size_t i0 = 0; i0 < list_A0_k.size(); ++i0) { -#pragma omp for schedule(dynamic) nowait for (size_t i1 = 0; i1 < list_A1_k.size(); ++i1) { const TA iat0 = list_A0_k[i0]; @@ -526,19 +884,26 @@ auto Ewald_Vq::set_Vq_dVq_minus_gauss(const UnitCell& ucell, const ModuleBase::Vector3 tau0 = ucell.atoms[it0].tau[ia0]; const ModuleBase::Vector3 tau1 = ucell.atoms[it1].tau[ia1]; - const double Rcut = std::min(this->get_Rcut_max(it0, it1), this->get_Rcut_max(it1, it0)); const ModuleBase::Vector3 R_delta = -tau0 + tau1 + (RI_Util::array3_to_Vector3(cell1) * ucell.latvec); - - if (R_delta.norm() * ucell.lat0 < Rcut) + const auto atom_blocks = Vs_dVs_minus_gauss.find(iat0); + if (atom_blocks == Vs_dVs_minus_gauss.end()) continue; + const auto block = atom_blocks->second.find(list_A1[i1]); + if (block == atom_blocks->second.end()) continue; + + const double Rcut = std::min(this->get_Rcut_max(it0, it1), + this->get_Rcut_max(it1, it0)); + if (EwaldVqDetail::uses_adaptive_realspace_range(this->tail_mode) + || R_delta.norm() * ucell.lat0 < Rcut) { const std::complex phase = std::exp(ModuleBase::TWO_PI * ModuleBase::IMAG_UNIT * (this->kvec_c[ik] * (RI_Util::array3_to_Vector3(cell1) * ucell.latvec))); + Tin block_value = block->second; Tout Vs_dVs_tmp = LRI_CV_Tools::mul2( phase, - LRI_CV_Tools::convert(std::move(Vs_dVs_minus_gauss.at(iat0).at(list_A1[i1])))); + LRI_CV_Tools::convert(std::move(block_value))); const TAK index = std::make_pair(iat1, TK{static_cast(ik)}); if (!LRI_CV_Tools::exist(local_datas[iat0][index])) @@ -593,7 +958,9 @@ auto Ewald_Vq::cal_Vq(const UnitCell& ucell, std::map>> Vs_minus_gauss = this->cal_Vs_minus_gauss(ucell, this->list_A0, this->list_A1, - Vs_in); //{ia0, {ia1, R}} + Vs_in, + this->tail_mode == EwaldVqDetail::TailMode::Enlarge + || this->tail_mode == EwaldVqDetail::TailMode::Strict); //{ia0, {ia1, R}} const T_func_DPcal_Vq_dVq_minus_gauss>, RI::Tensor> func_cal_Vq_minus_gauss = std::bind(&Ewald_Vq::cal_Vq_minus_gauss, this, @@ -613,9 +980,9 @@ auto Ewald_Vq::cal_Vq(const UnitCell& ucell, auto Vq = this->set_Vq_dVq(ucell, this->list_A0_pair_k, this->list_A1_pair_k, - Vs_minus_gauss, - func_cal_Vq_minus_gauss, - func_cal_Vq_gauss); //{ia0, ia1} + Vs_minus_gauss, + func_cal_Vq_minus_gauss, + func_cal_Vq_gauss); //{ia0, ia1} ModuleBase::timer::end("Ewald_Vq", "cal_Vq"); return Vq; @@ -695,11 +1062,11 @@ auto Ewald_Vq::set_Vq_dVq(const UnitCell& ucell, std::map> Vs_dVs_minus_gauss = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vs_dVs_minus_gauss_in, atoms00, atoms01); - std::map> Vq_dVq_minus_gauss = func_cal_Vq_dVq_minus_gauss(Vs_dVs_minus_gauss); //{ia0, ia1} + std::map> Vq_dVq_minus_gauss_local + = func_cal_Vq_dVq_minus_gauss(Vs_dVs_minus_gauss); // partial sum over local R blocks - // MPI: {ia0, {ia1, k}} to {ia0, ia1} - std::map> Vq_dVq_gauss_out = func_cal_Vq_dVq_gauss(shift_for_mpi); //{ia0, {ia1, k}} - // MPI: {ia0, {ia1, k}} to {ia0, ia1} + // MPI: combine partial Fourier sums from different local R subsets onto the + // target (I, J, k) distribution. std::set atoms10; std::set atoms11; for (const auto& I: this->list_A0_pair_k) @@ -710,14 +1077,17 @@ auto Ewald_Vq::set_Vq_dVq(const UnitCell& ucell, { atoms11.insert(JR.first); } + std::map> Vq_dVq_minus_gauss + = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vq_dVq_minus_gauss_local, atoms10, atoms11); + // MPI: {ia0, {ia1, k}} to {ia0, ia1} + std::map> Vq_dVq_gauss_out = func_cal_Vq_dVq_gauss(shift_for_mpi); //{ia0, {ia1, k}} std::map> Vq_dVq_gauss = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vq_dVq_gauss_out, atoms10, atoms11); //{ia0, ia1} -#pragma omp parallel +#pragma omp parallel for collapse(2) schedule(dynamic) for (size_t i0 = 0; i0 < list_A0_pair_k.size(); ++i0) { -#pragma omp for schedule(dynamic) nowait for (size_t i1 = 0; i1 < list_A1_pair_k.size(); ++i1) { const TA iat0 = list_A0_pair_k[i0]; @@ -792,8 +1162,8 @@ auto Ewald_Vq::cal_Vs(const UnitCell& ucell, std::map>>& Vs_in) //{ia0, {ia1, R}} -> std::map>> { - ModuleBase::TITLE("Ewald_Vq", "cal_Vs"); - ModuleBase::timer::start("Ewald_Vq", "cal_Vs"); + ModuleBase::TITLE("Ewald_Vq", "cal_Vs"); + ModuleBase::timer::start("Ewald_Vq", "cal_Vs"); std::map>>> Vq = this->cal_Vq(ucell, chi, Vs_in); auto Vs = this->set_Vs_dVs>(ucell, @@ -805,6 +1175,200 @@ auto Ewald_Vq::cal_Vs(const UnitCell& ucell, return Vs; } +template +auto Ewald_Vq::cal_Vs_split(const UnitCell& ucell, + const double& chi, + std::map>>& Vs_in) + -> std::pair>>, + std::map>>> +{ + ModuleBase::TITLE("Ewald_Vq", "cal_Vs_split"); + ModuleBase::timer::start("Ewald_Vq", "cal_Vs_split"); + + std::map>> Vs_minus_gauss = this->cal_Vs_minus_gauss(ucell, + this->list_A0, + this->list_A1, + Vs_in, + this->tail_mode == EwaldVqDetail::TailMode::Enlarge + || this->tail_mode == EwaldVqDetail::TailMode::Strict); + const int shift_for_mpi = std::floor(this->nks0 / 2.0); + std::set atoms00; + std::set atoms01; + for (const auto& I: this->list_A0_pair_R) + { + atoms00.insert(I); + } + for (const auto& JR: this->list_A1_pair_R) + { + atoms01.insert(JR.first); + } + std::map>> Vs_minus_gauss_comm + = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vs_minus_gauss, atoms00, atoms01); + std::map>>> Vq_short_local + = this->cal_Vq_minus_gauss(ucell, + this->list_A0_pair_R, + this->list_A1_pair_R, + Vs_minus_gauss_comm); + + std::set atoms10; + std::set atoms11; + for (const auto& I: this->list_A0_pair_k) + { + atoms10.insert(I); + } + for (const auto& JR: this->list_A1_pair_k) + { + atoms11.insert(JR.first); + } + auto Vq_short = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vq_short_local, atoms10, atoms11); + auto Vq_long_out = this->cal_Vq_gauss(ucell, + this->list_A0_k, + this->list_A1_k, + chi, + shift_for_mpi); + auto Vq_long_gauss = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vq_long_out, atoms10, atoms11); + std::map>>> Vq_long; + using Tin_convert = typename LRI_CV_Tools::TinType>>::type; + +#pragma omp parallel for collapse(2) schedule(dynamic) + for (size_t i0 = 0; i0 < this->list_A0_pair_k.size(); ++i0) + { + for (size_t i1 = 0; i1 < this->list_A1_pair_k.size(); ++i1) + { + const TA iat0 = this->list_A0_pair_k[i0]; + const int it0 = ucell.iat2it[iat0]; + const TA iat1 = this->list_A1_pair_k[i1].first; + const int it1 = ucell.iat2it[iat1]; + const int ik = this->list_A1_pair_k[i1].second[0] + shift_for_mpi; + const TAK re_index = std::make_pair(iat1, std::array{ik}); + + auto it_short_outer = Vq_short.find(iat0); + if (it_short_outer == Vq_short.end()) + { + continue; + } + if (it_short_outer->second.find(re_index) == it_short_outer->second.end()) + { + continue; + } + auto it_long_outer = Vq_long_gauss.find(iat0); + if (it_long_outer == Vq_long_gauss.end()) + { + continue; + } + auto it_long_inner = it_long_outer->second.find(this->list_A1_pair_k[i1]); + if (it_long_inner == it_long_outer->second.end()) + { + continue; + } + + RI::Tensor> data; + LRI_CV_Tools::init_elem(data, this->index_abfs[it0].count_size, this->index_abfs[it1].count_size); + for (int l0 = 0; l0 != this->g_abfs_ccp.at(it0).size(); ++l0) + { + for (int l1 = 0; l1 != this->g_abfs.at(it1).size(); ++l1) + { + for (size_t n0 = 0; n0 != this->g_abfs_ccp.at(it0).at(l0).size(); ++n0) + { + const double pA = this->multipole.at(it0).at(l0).at(n0); + for (size_t n1 = 0; n1 != this->g_abfs.at(it1).at(l1).size(); ++n1) + { + const double pB = this->multipole.at(it1).at(l1).at(n1); + Tin_convert frac = RI::Global_Func::convert(pA * pB); + for (size_t m0 = 0; m0 != 2 * l0 + 1; ++m0) + { + const size_t index0 = this->index_abfs.at(it0).at(l0).at(n0).at(m0); + const size_t lm0 = l0 * l0 + m0; + for (size_t m1 = 0; m1 != 2 * l1 + 1; ++m1) + { + const size_t index1 = this->index_abfs.at(it1).at(l1).at(n1).at(m1); + const size_t lm1 = l1 * l1 + m1; + LRI_CV_Tools::add_elem(data, + index0, + index1, + it_long_inner->second, + lm0, + lm1, + frac); + } + } + } + } + } + } + +#pragma omp critical(Ewald_Vq_cal_Vs_split) + if (LRI_CV_Tools::exist(data)) + { + Vq_long[iat0][re_index] = std::move(data); + } + } + } + + auto Vs_short = this->set_Vs_dVs>(ucell, + this->list_A0_pair_R_period, + this->list_A1_pair_R_period, + Vq_short); + auto Vs_long = this->set_Vs_dVs>(ucell, + this->list_A0_pair_R_period, + this->list_A1_pair_R_period, + Vq_long); + + ModuleBase::timer::end("Ewald_Vq", "cal_Vs_split"); + return {Vs_short, Vs_long}; +} + +template +auto Ewald_Vq::cal_Vs_serial_full(const UnitCell& ucell, + const double& chi, + std::map>>& Vs_in_full, + const std::array& period_Vs_NAO) + -> std::map>> +{ + const MPI_Comm mpi_comm_saved = this->mpi_comm; + const auto list_A0_saved = this->list_A0; + const auto list_A1_saved = this->list_A1; + const auto list_A0_k_saved = this->list_A0_k; + const auto list_A1_k_saved = this->list_A1_k; + const auto list_A0_pair_R_saved = this->list_A0_pair_R; + const auto list_A1_pair_R_saved = this->list_A1_pair_R; + const auto list_A0_pair_R_period_saved = this->list_A0_pair_R_period; + const auto list_A1_pair_R_period_saved = this->list_A1_pair_R_period; + const auto list_A0_pair_k_saved = this->list_A0_pair_k; + const auto list_A1_pair_k_saved = this->list_A1_pair_k; + + const std::array& period_Vs = period_Vs_NAO; + const std::array Nks = {this->nks0}; + + this->mpi_comm = MPI_COMM_SELF; + this->list_A0 = this->atoms_vec; + this->list_A1 = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, period_Vs); + this->list_A0_k = this->atoms_vec; + this->list_A1_k = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, Nks); + this->list_A0_pair_R = this->atoms_vec; + this->list_A1_pair_R = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, period_Vs_NAO); + this->list_A0_pair_R_period = this->atoms_vec; + this->list_A1_pair_R_period = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, this->nmp); + this->list_A0_pair_k = this->atoms_vec; + this->list_A1_pair_k = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, Nks); + + auto Vs_full = this->cal_Vs(ucell, chi, Vs_in_full); + + this->mpi_comm = mpi_comm_saved; + this->list_A0 = list_A0_saved; + this->list_A1 = list_A1_saved; + this->list_A0_k = list_A0_k_saved; + this->list_A1_k = list_A1_k_saved; + this->list_A0_pair_R = list_A0_pair_R_saved; + this->list_A1_pair_R = list_A1_pair_R_saved; + this->list_A0_pair_R_period = list_A0_pair_R_period_saved; + this->list_A1_pair_R_period = list_A1_pair_R_period_saved; + this->list_A0_pair_k = list_A0_pair_k_saved; + this->list_A1_pair_k = list_A1_pair_k_saved; + + return Vs_full; +} + template auto Ewald_Vq::cal_dVs( const UnitCell& ucell, @@ -826,6 +1390,454 @@ auto Ewald_Vq::cal_dVs( return dVs; } +template +auto Ewald_Vq::cal_realspace_difference( + const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in, + const bool truncate_analytic_tail) + -> std::map>> +{ + return this->cal_Vs_minus_gauss(ucell, + list_A0, + list_A1, + Vs_in, + truncate_analytic_tail); +} + +template +auto Ewald_Vq::cal_short_range_tail_stats( + const UnitCell& ucell, + const std::vector& local_keys, + std::map>>& Vs_bare, + const std::size_t local_production_blocks, + const double local_reference_norm) -> EwaldVqDetail::TailStats +{ + using TailKey = EwaldVqDetail::TailKey; + using TailBlockCoverage = EwaldVqDetail::TailBlockCoverage; + + std::map>> projected_gaussian; + const auto grouped = EwaldVqDetail::group_tail_keys_by_atom(local_keys); + for (std::map>>::const_iterator grouped_it + = grouped.begin(); + grouped_it != grouped.end(); + ++grouped_it) + { + const int iat0 = grouped_it->first; + const std::vector>& list_A1 = grouped_it->second; + const std::vector list_A0{iat0}; + auto gaussian = this->cal_Vs_gauss(ucell, list_A0, list_A1); + auto projected = this->project_Vs_dVs_gauss(ucell, list_A0, list_A1, gaussian); + for (typename std::map>>::const_iterator projected_it + = projected.begin(); + projected_it != projected.end(); + ++projected_it) + { + projected_gaussian[projected_it->first].insert(projected_it->second.begin(), + projected_it->second.end()); + } + } + + const auto find_tensor = [](const auto& tensor_map, const TailKey& key) + -> const RI::Tensor* + { + const auto outer = tensor_map.find(key.atom_i); + if (outer == tensor_map.end()) return nullptr; + const TAC jr{key.atom_j, key.cell}; + const auto inner = outer->second.find(jr); + return inner == outer->second.end() ? nullptr : &inner->second; + }; + + std::map plans; + std::vector multipole_keys; + for (const TailKey& key: local_keys) + { + const int it0 = ucell.iat2it[key.atom_i]; + const int ia0 = ucell.iat2ia[key.atom_i]; + const int it1 = ucell.iat2it[key.atom_j]; + const int ia1 = ucell.iat2ia[key.atom_j]; + const auto tau0 = ucell.atoms[it0].tau[ia0]; + const auto tau1 = ucell.atoms[it1].tau[ia1]; + const auto delta = -tau0 + tau1 + (RI_Util::array3_to_Vector3(key.cell) * ucell.latvec); + const double distance = delta.norm() * ucell.lat0; + const double bare_support = this->abfs_rcut[it0] + this->abfs_rcut[it1]; + // Same pipeline-extent bound as the candidate loops above. + const double gaussian_support = std::min(this->cal_V_Rcut(it0, it1), this->cal_V_Rcut(it1, it0)); + + const RI::Tensor* bare = find_tensor(Vs_bare, key); + const RI::Tensor* gaussian = find_tensor(projected_gaussian, key); + const auto plan = EwaldVqDetail::plan_tail_block(bare != nullptr, + gaussian != nullptr, + distance, + bare_support, + gaussian_support); + plans[key] = plan; + if (plan.use_bare_multipole || plan.use_gaussian_multipole) + { + multipole_keys.push_back(key); + } + } + const auto multipole_blocks = this->cal_analytic_multipole_Vs(ucell, multipole_keys); + + EwaldVqDetail::TailStats local_stats; + local_stats.production_blocks = local_production_blocks; + local_stats.reference_norm = local_reference_norm; + std::map coverage; + std::map> differences; + for (const TailKey& key: local_keys) + { + const int it0 = ucell.iat2it[key.atom_i]; + const int ia0 = ucell.iat2ia[key.atom_i]; + const int it1 = ucell.iat2it[key.atom_j]; + const int ia1 = ucell.iat2ia[key.atom_j]; + const auto tau0 = ucell.atoms[it0].tau[ia0]; + const auto tau1 = ucell.atoms[it1].tau[ia1]; + const auto delta = -tau0 + tau1 + (RI_Util::array3_to_Vector3(key.cell) * ucell.latvec); + const double distance = delta.norm() * ucell.lat0; + const auto plan = plans.at(key); + coverage[key] = plan.coverage; + + const RI::Tensor* bare = find_tensor(Vs_bare, key); + const RI::Tensor* gaussian = find_tensor(projected_gaussian, key); + const RI::Tensor* analytic = find_tensor(multipole_blocks, key); + if (plan.use_bare_multipole) bare = analytic; + if (plan.use_gaussian_multipole) gaussian = analytic; + + EwaldVqDetail::TailSample sample; + sample.key = key; + sample.distance = distance; + if (bare != nullptr) sample.bare_norm = bare->norm(2.0); + if (gaussian != nullptr) sample.gaussian_norm = gaussian->norm(2.0); + + if (plan.coverage == TailBlockCoverage::Common && bare != nullptr && gaussian != nullptr) + { + RI::Tensor difference = *bare - *gaussian; + sample.difference_norm = difference.norm(2.0); + differences.emplace(key, std::move(difference)); + } + else if (plan.coverage == TailBlockCoverage::AnalyticCancellation) + { + sample.bare_norm = 0.0; + sample.gaussian_norm = 0.0; + } + else + { + local_stats.coverage_complete = false; + } + EwaldVqDetail::accumulate_tail_sample(local_stats, sample); + } + + for (std::map::const_iterator coverage_it = coverage.begin(); + coverage_it != coverage.end(); + ++coverage_it) + { + const TailKey& key = coverage_it->first; + const TailBlockCoverage block_coverage = coverage_it->second; + if (!(EwaldVqDetail::canonical_hermitian_key(key) == key)) continue; + const TailKey partner = EwaldVqDetail::hermitian_partner(key); + const auto partner_coverage = coverage.find(partner); + if (partner_coverage == coverage.end() + || block_coverage == TailBlockCoverage::Incomplete + || partner_coverage->second == TailBlockCoverage::Incomplete) + { + local_stats.coverage_complete = false; + continue; + } + const auto difference = differences.find(key); + const auto partner_difference = differences.find(partner); + if (difference != differences.end() && partner_difference != differences.end()) + { + const RI::Tensor partner_adjoint + = EwaldVqDetail::tensor_adjoint(partner_difference->second); + local_stats.hermitian_residual + = std::max(local_stats.hermitian_residual, + (difference->second - partner_adjoint).norm(2.0)); + } + else if (difference != differences.end()) + { + local_stats.hermitian_residual + = std::max(local_stats.hermitian_residual, difference->second.norm(2.0)); + } + else if (partner_difference != differences.end()) + { + local_stats.hermitian_residual + = std::max(local_stats.hermitian_residual, partner_difference->second.norm(2.0)); + } + } + return EwaldVqDetail::reduce_tail_stats(this->mpi_comm, local_stats); +} + +template +auto Ewald_Vq::cal_short_range_Vs(const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in) + -> std::map>> +{ + ModuleBase::TITLE("Ewald_Vq", "cal_short_range_Vs"); + ModuleBase::timer::start("Ewald_Vq", "cal_short_range_Vs"); + + auto Vs_minus_gauss = this->cal_Vs_minus_gauss(ucell, + list_A0, + list_A1, + Vs_in, + this->tail_mode == EwaldVqDetail::TailMode::Enlarge + || this->tail_mode == EwaldVqDetail::TailMode::Strict); + auto Vq_minus_gauss = this->cal_Vq_minus_gauss(ucell, list_A0, list_A1, Vs_minus_gauss); + auto Vs_short = this->set_Vs_dVs>(ucell, + this->list_A0_pair_R_period, + this->list_A1_pair_R_period, + Vq_minus_gauss); + + ModuleBase::timer::end("Ewald_Vq", "cal_short_range_Vs"); + return Vs_short; +} + +template +auto Ewald_Vq::cal_bare_periodic_Vs(const UnitCell& ucell, + const std::vector& list_A0, + const std::vector& list_A1, + std::map>>& Vs_in) + -> std::map>> +{ + ModuleBase::TITLE("Ewald_Vq", "cal_bare_periodic_Vs"); + ModuleBase::timer::start("Ewald_Vq", "cal_bare_periodic_Vs"); + + // This helper performs the distributed R-to-q transform; the input map selects the physical term. + auto Vq_bare = this->cal_Vq_minus_gauss(ucell, list_A0, list_A1, Vs_in); + auto Vs_bare = this->set_Vs_dVs>(ucell, + this->list_A0_pair_R_period, + this->list_A1_pair_R_period, + Vq_bare); + + ModuleBase::timer::end("Ewald_Vq", "cal_bare_periodic_Vs"); + return Vs_bare; +} + +template +auto Ewald_Vq::cal_short_range_Vs_serial_full( + const UnitCell& ucell, + std::map>>& Vs_in_full, + const std::array& period_Vs_NAO) -> std::map>> +{ + const MPI_Comm mpi_comm_saved = this->mpi_comm; + const auto list_A0_saved = this->list_A0; + const auto list_A1_saved = this->list_A1; + const auto list_A0_k_saved = this->list_A0_k; + const auto list_A1_k_saved = this->list_A1_k; + const auto list_A0_pair_R_saved = this->list_A0_pair_R; + const auto list_A1_pair_R_saved = this->list_A1_pair_R; + const auto list_A0_pair_R_period_saved = this->list_A0_pair_R_period; + const auto list_A1_pair_R_period_saved = this->list_A1_pair_R_period; + const auto list_A0_pair_k_saved = this->list_A0_pair_k; + const auto list_A1_pair_k_saved = this->list_A1_pair_k; + + const std::array& period_Vs = period_Vs_NAO; + const std::array Nks = {this->nks0}; + + this->mpi_comm = MPI_COMM_SELF; + this->list_A0 = this->atoms_vec; + this->list_A1 = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, period_Vs); + this->list_A0_k = this->atoms_vec; + this->list_A1_k = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, Nks); + this->list_A0_pair_R = this->atoms_vec; + this->list_A1_pair_R = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, period_Vs_NAO); + this->list_A0_pair_R_period = this->atoms_vec; + this->list_A1_pair_R_period = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, this->nmp); + this->list_A0_pair_k = this->atoms_vec; + this->list_A1_pair_k = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, Nks); + + auto Vs_short_full = this->cal_short_range_Vs(ucell, this->list_A0, this->list_A1, Vs_in_full); + + this->mpi_comm = mpi_comm_saved; + this->list_A0 = list_A0_saved; + this->list_A1 = list_A1_saved; + this->list_A0_k = list_A0_k_saved; + this->list_A1_k = list_A1_k_saved; + this->list_A0_pair_R = list_A0_pair_R_saved; + this->list_A1_pair_R = list_A1_pair_R_saved; + this->list_A0_pair_R_period = list_A0_pair_R_period_saved; + this->list_A1_pair_R_period = list_A1_pair_R_period_saved; + this->list_A0_pair_k = list_A0_pair_k_saved; + this->list_A1_pair_k = list_A1_pair_k_saved; + + return Vs_short_full; +} + +template +auto Ewald_Vq::cal_long_range_Vs_gauss(const UnitCell& ucell, const double& chi) + -> std::map>> +{ + ModuleBase::TITLE("Ewald_Vq", "cal_long_range_Vs_gauss"); + ModuleBase::timer::start("Ewald_Vq", "cal_long_range_Vs_gauss"); + + const int shift_for_mpi = std::floor(this->nks0 / 2.0); + T_func_DPget_Vq_dVq>> func_DPget_Vq; + if (this->ewald_dimension == 2) + { + func_DPget_Vq = std::bind(&Gaussian_Abfs::get_Vq_2d, + &this->gaussian_abfs, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + chi, + std::placeholders::_4, + this->gaunt); + } + else + { + func_DPget_Vq = std::bind(&Gaussian_Abfs::get_Vq, + &this->gaussian_abfs, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + chi, + std::placeholders::_4, + this->gaunt); + } + std::map>>> Vq_gauss_out + = this->set_Vq_dVq_gauss(ucell, this->list_A0_k, this->list_A1_k, shift_for_mpi, func_DPget_Vq); + + std::set atoms10; + std::set atoms11; + for (const auto& I: this->list_A0_pair_k) + { + atoms10.insert(I); + } + for (const auto& JR: this->list_A1_pair_k) + { + atoms11.insert(JR.first); + } + std::map>>> Vq_gauss + = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vq_gauss_out, atoms10, atoms11); + + using Tin_convert = typename LRI_CV_Tools::TinType>>::type; + std::map>>> Vq_long_full; +#pragma omp parallel + for (size_t i0 = 0; i0 < this->list_A0_pair_k.size(); ++i0) + { +#pragma omp for schedule(dynamic) nowait + for (size_t i1 = 0; i1 < this->list_A1_pair_k.size(); ++i1) + { + const TA iat0 = this->list_A0_pair_k[i0]; + const int it0 = ucell.iat2it[iat0]; + const TA iat1 = this->list_A1_pair_k[i1].first; + const int it1 = ucell.iat2it[iat1]; + const int ik = this->list_A1_pair_k[i1].second[0] + shift_for_mpi; + const TAK re_index = std::make_pair(iat1, std::array{ik}); + + auto it_outer = Vq_gauss.find(this->list_A0_pair_k[i0]); + if (it_outer == Vq_gauss.end()) + { + continue; + } + auto it_inner = it_outer->second.find(this->list_A1_pair_k[i1]); + if (it_inner == it_outer->second.end()) + { + continue; + } + + RI::Tensor> data; + LRI_CV_Tools::init_elem(data, this->index_abfs[it0].count_size, this->index_abfs[it1].count_size); + for (int l0 = 0; l0 != this->g_abfs_ccp.at(it0).size(); ++l0) + { + for (int l1 = 0; l1 != this->g_abfs.at(it1).size(); ++l1) + { + for (size_t n0 = 0; n0 != this->g_abfs_ccp.at(it0).at(l0).size(); ++n0) + { + const double pA = this->multipole.at(it0).at(l0).at(n0); + for (size_t n1 = 0; n1 != this->g_abfs.at(it1).at(l1).size(); ++n1) + { + const double pB = this->multipole.at(it1).at(l1).at(n1); + Tin_convert frac = RI::Global_Func::convert(pA * pB); + for (size_t m0 = 0; m0 != 2 * l0 + 1; ++m0) + { + const size_t index0 = this->index_abfs.at(it0).at(l0).at(n0).at(m0); + const size_t lm0 = l0 * l0 + m0; + for (size_t m1 = 0; m1 != 2 * l1 + 1; ++m1) + { + const size_t index1 = this->index_abfs.at(it1).at(l1).at(n1).at(m1); + const size_t lm1 = l1 * l1 + m1; + LRI_CV_Tools::add_elem(data, + index0, + index1, + it_inner->second, + lm0, + lm1, + frac); + } + } + } + } + } + } + +#pragma omp critical(Ewald_Vq_cal_long_range_Vs_gauss) + Vq_long_full[this->list_A0_pair_k[i0]][re_index] = std::move(data); + } + } + + auto Vs_long = this->set_Vs_dVs>(ucell, + this->list_A0_pair_R_period, + this->list_A1_pair_R_period, + Vq_long_full); + + ModuleBase::timer::end("Ewald_Vq", "cal_long_range_Vs_gauss"); + return Vs_long; +} + +template +auto Ewald_Vq::cal_long_range_Vs_gauss_serial_full(const UnitCell& ucell, + const double& chi, + const std::array& period_Vs_NAO) + -> std::map>> +{ + const MPI_Comm mpi_comm_saved = this->mpi_comm; + const auto list_A0_saved = this->list_A0; + const auto list_A1_saved = this->list_A1; + const auto list_A0_k_saved = this->list_A0_k; + const auto list_A1_k_saved = this->list_A1_k; + const auto list_A0_pair_R_saved = this->list_A0_pair_R; + const auto list_A1_pair_R_saved = this->list_A1_pair_R; + const auto list_A0_pair_R_period_saved = this->list_A0_pair_R_period; + const auto list_A1_pair_R_period_saved = this->list_A1_pair_R_period; + const auto list_A0_pair_k_saved = this->list_A0_pair_k; + const auto list_A1_pair_k_saved = this->list_A1_pair_k; + + const std::array period_Vs + = LRI_CV_Tools::cal_latvec_range(1 + this->ccp_rmesh_times, ucell, this->g_lcaos_rcut); + const std::array Nks = {this->nks0}; + + this->mpi_comm = MPI_COMM_SELF; + this->list_A0 = this->atoms_vec; + this->list_A1 = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, period_Vs); + this->list_A0_k = this->atoms_vec; + this->list_A1_k = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, Nks); + this->list_A0_pair_R = this->atoms_vec; + this->list_A1_pair_R = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, period_Vs_NAO); + this->list_A0_pair_R_period = this->atoms_vec; + this->list_A1_pair_R_period = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, this->nmp); + this->list_A0_pair_k = this->atoms_vec; + this->list_A1_pair_k = RI::Divide_Atoms::traversal_atom_period(this->atoms_vec, Nks); + + auto Vs_long_full = this->cal_long_range_Vs_gauss(ucell, chi); + + this->mpi_comm = mpi_comm_saved; + this->list_A0 = list_A0_saved; + this->list_A1 = list_A1_saved; + this->list_A0_k = list_A0_k_saved; + this->list_A1_k = list_A1_k_saved; + this->list_A0_pair_R = list_A0_pair_R_saved; + this->list_A1_pair_R = list_A1_pair_R_saved; + this->list_A0_pair_R_period = list_A0_pair_R_period_saved; + this->list_A1_pair_R_period = list_A1_pair_R_period_saved; + this->list_A0_pair_k = list_A0_pair_k_saved; + this->list_A1_pair_k = list_A1_pair_k_saved; + + return Vs_long_full; +} + template template auto Ewald_Vq::set_Vs_dVs(const UnitCell& ucell, @@ -842,66 +1854,63 @@ auto Ewald_Vq::set_Vs_dVs(const UnitCell& ucell, const double cfrac = 1.0 / this->nks0; std::map> datas; - // auto start = std::chrono::system_clock::now(); -#pragma omp parallel + // Accumulate one (iat0, iat1, R) block at a time so OpenMP threads do + // not each retain a near-complete output map on dense k meshes. +#pragma omp parallel for collapse(2) schedule(dynamic) + for (size_t i0 = 0; i0 < list_A0_pair_R.size(); ++i0) { - std::map> local_datas; - -#pragma omp for schedule(dynamic) nowait - for (size_t ik = 0; ik != this->nks0; ++ik) + for (size_t i1 = 0; i1 < list_A1_pair_R.size(); ++i1) { - for (size_t i0 = 0; i0 < list_A0_pair_R.size(); ++i0) + const TA iat0 = list_A0_pair_R[i0]; + const auto it_outer = Vq.find(iat0); + if (it_outer == Vq.end()) { - for (size_t i1 = 0; i1 < list_A1_pair_R.size(); ++i1) + continue; + } + + const TA iat1 = list_A1_pair_R[i1].first; + const TC& cell1 = list_A1_pair_R[i1].second; + + bool has_value = false; + Tout data; + + for (size_t ik = 0; ik != this->nks0; ++ik) + { + const std::complex frac + = std::exp(-ModuleBase::TWO_PI * ModuleBase::IMAG_UNIT + * (this->kvec_c[ik] * (RI_Util::array3_to_Vector3(cell1) * ucell.latvec))) + * cfrac; + + const TAK index = std::make_pair(iat1, std::array{static_cast(ik)}); + + // check the Fourier transformed V(q) + // whether ccp_rmesh_times * Rcut >= rIJ + // skip some IJ pairs + const auto it_inner = it_outer->second.find(index); + if (it_inner == it_outer->second.end()) { - const TA iat0 = list_A0_pair_R[i0]; - const TA iat1 = list_A1_pair_R[i1].first; - const TC& cell1 = list_A1_pair_R[i1].second; - const std::complex frac - = std::exp(-ModuleBase::TWO_PI * ModuleBase::IMAG_UNIT - * (this->kvec_c[ik] * (RI_Util::array3_to_Vector3(cell1) * ucell.latvec))) - * cfrac; - - const TAK index = std::make_pair(iat1, std::array{static_cast(ik)}); - - // check the Fourier transformed V(q) - // whether ccp_rmesh_times * Rcut >= rIJ - // skip some IJ pairs - auto it_outer = Vq.find(iat0); - if (it_outer == Vq.end()) - continue; - - auto it_inner = it_outer->second.find(index); - if (it_inner == it_outer->second.end()) - continue; - - if (LRI_CV_Tools::exist(Vq.at(iat0).at(index))) - { - Tout Vq_tmp = LRI_CV_Tools::convert(LRI_CV_Tools::mul2(frac, Vq.at(iat0).at(index))); + continue; + } - if (!LRI_CV_Tools::exist(local_datas[iat0][list_A1_pair_R[i1]])) - local_datas[iat0][list_A1_pair_R[i1]] = Vq_tmp; - else - local_datas[iat0][list_A1_pair_R[i1]] - = local_datas.at(iat0).at(list_A1_pair_R[i1]) + Vq_tmp; + if (LRI_CV_Tools::exist(it_inner->second)) + { + Tout Vq_tmp = LRI_CV_Tools::convert(LRI_CV_Tools::mul2(frac, it_inner->second)); + if (!has_value) + { + data = std::move(Vq_tmp); + has_value = true; + } + else + { + data = data + Vq_tmp; } } } - } -#pragma omp critical(Ewald_Vq_set_Vs_dVs) - { - for (auto& outer_pair: local_datas) + + if (has_value) { - TA key0 = outer_pair.first; - for (auto& inner_pair: outer_pair.second) - { - TAC key1 = inner_pair.first; - Tout& value = inner_pair.second; - if (!LRI_CV_Tools::exist(datas[key0][key1])) - datas[key0][key1] = value; - else - datas[key0][key1] = datas.at(key0).at(key1) + value; - } +#pragma omp critical(Ewald_Vq_set_Vs_dVs) + { datas[iat0][list_A1_pair_R[i1]] = std::move(data); } } } } diff --git a/source/source_lcao/module_ri/exx_lri.h b/source/source_lcao/module_ri/exx_lri.h index 95f5e09839f..b89a3a649c0 100644 --- a/source/source_lcao/module_ri/exx_lri.h +++ b/source/source_lcao/module_ri/exx_lri.h @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include "module_exx_symmetry/symm_rotation.h" @@ -45,6 +47,21 @@ class Exx_Obj std::vector>> abfs_ccp; }; +// Runtime state is captured when an EXX object is created. This keeps the +// RI kernels independent from process-global input and output state. +struct ExxLriRuntime +{ + bool cal_force; + bool cal_stress; + bool realtime_tddft; + int nspin; + int rank; + const std::string& output_dir; + std::ofstream& log; +}; + +ExxLriRuntime make_exx_lri_runtime(); + template class Exx_LRI { @@ -61,8 +78,21 @@ class Exx_LRI std::pair>>>>; - - Exx_LRI(const Exx_Info_RI& info_in) :info(info_in) {} + using CoulombMap = std::map>>; + struct EwaldCoulombComponents + { + CoulombMap bare_periodic; + CoulombMap gaussian_real; + CoulombMap short_range; + CoulombMap long_range; + }; + + Exx_LRI(const Exx_Info_RI& info_in, const ExxLriRuntime& runtime_in) + : info(info_in), runtime(runtime_in) {} + // Split RPA_LRI sources construct cut/full-coulomb helpers from a local + // Exx_Info_RI copy; default the runtime the same way the esolver path does. + Exx_LRI(const Exx_Info_RI& info_in) + : Exx_LRI(info_in, make_exx_lri_runtime()) {} Exx_LRI operator=(const Exx_LRI&) = delete; Exx_LRI operator=(Exx_LRI&&); @@ -101,64 +131,79 @@ class Exx_LRI const std::vector>>& abfs_in = {}); void cal_exx_ions(const UnitCell& ucell, const bool write_cv = false); void cal_cut_coulomb_cs( - std::map>>& Vs_cut_IJR, - std::map>>& Cs, - const UnitCell& ucell, - const bool write_cv = false); - void cal_ewald_coulomb( - std::map>>& Vs_full_IJR, - std::map>>& Cs, - const UnitCell& ucell, - const bool write_cv = false); - void cal_exx_elec( - const std::vector>>>& Ds, - const UnitCell& ucell, - const Parallel_Orbitals& pv, - const ModuleSymmetry::Symmetry_rotation* p_symrot = nullptr); - // (nspin=4) real-space symmetry EXX: the spinor H(R) rotation couples the 4 spin channels via - // the SU(2) part U(isym), so the 4 channels must be rotated together (not one-per-outer-loop). - // Gathers the irreducible Hs of all 4 channels, calls Symmetry_rotation::restore_HR_nspin4, then - // finishes energy/gather per channel. Called from cal_exx_elec when p_symrot && nspin==4. - void cal_exx_elec_soc( - const std::vector>>>& Ds, - const UnitCell& ucell, - const std::vector, std::set>>& judge, - const ModuleSymmetry::Symmetry_rotation* p_symrot); - void cal_exx_force(const int& nat); - void cal_exx_stress(const double& omega, const double& lat0); - - void reset_Cs(const std::map>>& Cs_in) { this->exx_lri.set_Cs(Cs_in, this->info.C_threshold); } - void reset_Vs(const std::map>>& Vs_in) { this->exx_lri.set_Vs(Vs_in, this->info.V_threshold); } - //std::vector> get_abfs_nchis() const; - - std::vector< std::map>>> Hexxs; - double Eexx = 0.0; // initialize: in the HexxR-restart branch (init_chg=file) Eexx is - // never computed, and OperatorEXX still reads get_Eexx() for the NSCF - // energy report; leaving it uninitialized leaked an indeterminate value. - ModuleBase::matrix force_exx; - ModuleBase::matrix stress_exx; + std::map>>& Vs_cut_IJR, + std::map>>& Cs, + const UnitCell& ucell, + const bool write_cv = false); + void cal_ewald_coulomb( + std::map>>& Vs_full_IJR, + std::map>>& Cs, + const UnitCell& ucell, + const bool write_cv = false, + std::map>>* Vs_short_IJR = nullptr, + std::map>>* Vs_long_IJR = nullptr, + EwaldCoulombComponents* components = nullptr); + void cal_exx_elec( + const std::vector>>>& Ds, + const UnitCell& ucell, + const Parallel_Orbitals& pv, + const ModuleSymmetry::Symmetry_rotation* p_symrot = nullptr); + // (nspin=4) real-space symmetry EXX: the spinor H(R) rotation couples the 4 spin channels via + // the SU(2) part U(isym), so the 4 channels must be rotated together (not one-per-outer-loop). + // Gathers the irreducible Hs of all 4 channels, calls Symmetry_rotation::restore_HR_nspin4, then + // finishes energy/gather per channel. Called from cal_exx_elec when p_symrot && nspin==4. + void cal_exx_elec_soc( + const std::vector>>>& Ds, + const UnitCell& ucell, + const std::vector, std::set>>& judge, + const ModuleSymmetry::Symmetry_rotation* p_symrot); + void cal_exx_force(const int& nat); + void cal_exx_stress(const double& omega, const double& lat0); + void cal_exx_dHs( + const std::vector>>>& Ds, + const UnitCell& ucell, + const Parallel_Orbitals& pv); + + void reset_Cs(const std::map>>& Cs_in) { this->exx_lri.set_Cs(Cs_in, this->info.C_threshold); } + void reset_Vs(const std::map>>& Vs_in) { this->exx_lri.set_Vs(Vs_in, this->info.V_threshold); } + int abfs_Lmax() const { return abfs_Lmax_; } + const Exx_Info_RI& get_info_ri() const { return info; } + // Return the ABF shell multiplicities for each atom type. + std::vector> get_abfs_nchis() const; + + std::vector< std::map>>> Hexxs; + std::array>>>>, 3> dHexxs; + double Eexx; + ModuleBase::matrix force_exx; + ModuleBase::matrix stress_exx; void post_process_Hexx(std::map>>& Hexxs_io) const; double post_process_Eexx(const double& Eexx_in) const; - int abfs_Lmax() const { return abfs_Lmax_; } - const Exx_Info_RI& get_info_ri() const { return info; } - private: - Exx_Info_RI info; - int abfs_Lmax_ = 0; - MPI_Comm mpi_comm; - const K_Vectors *p_kv = nullptr; - std::shared_ptr MGT; - std::vector orb_cutoff_; - - std::vector>> lcaos; - std::vector>> abfs; - //std::vector>> abfs_ccp; - std::map> exx_objs; - //LRI_CV cv; - RI::Exx exx_lri; - CoulombSettings coulomb_settings; + // Keep an owned copy: RPA_LRI may construct Exx_LRI from a temporary + // local Exx_Info_RI when the cut/full Coulomb settings are adjusted. + // Storing a reference here leaves a dangling reference after that caller + // returns and can corrupt the subsequent RI matrix construction. + Exx_Info_RI info; + const ExxLriRuntime runtime; + int abfs_Lmax_ = 0; + MPI_Comm mpi_comm; + const K_Vectors *p_kv = nullptr; + std::shared_ptr MGT; + std::vector orb_cutoff_; + + std::vector>> lcaos; + std::vector>> abfs; + std::map> exx_objs; + RI::Exx exx_lri; + std::map>>>> coulomb_settings; + bool use_rotated_n0_long_range = false; + ModuleBase::Element_Basis_Index::IndexPermutation abfs_old_to_new_per_type; + std::vector abfs_long_prefix_size_per_type; friend class RPA_LRI; friend class RPA_LRI, Tdata>; diff --git a/source/source_lcao/module_ri/exx_lri.hpp b/source/source_lcao/module_ri/exx_lri.hpp index 618c58cef6f..b078c474a5f 100644 --- a/source/source_lcao/module_ri/exx_lri.hpp +++ b/source/source_lcao/module_ri/exx_lri.hpp @@ -10,12 +10,13 @@ #include "source_cell/unitcell.h" #include "ri_2d_comm.h" #include "ri_util.h" -#include "exx_lri_detail.h" +#include "source_lcao/module_ri/exx_rotate_abfs.h" #include "source_lcao/module_ri/exx_abfs_ctor_orbs.h" #include "source_lcao/module_ri/exx_abfs_io.h" #include "source_lcao/module_ri/conv_coulomb_pot_k.h" #include "source_base/tool_title.h" #include "source_base/timer.h" +#include "source_lcao/module_ri/serialization_cereal.h" #include "source_lcao/module_ri/mix_dmk_2d.h" #include "source_basis/module_ao/parallel_orbitals.h" #include "source_io/module_parameter/parameter.h" @@ -24,13 +25,1053 @@ #include #include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include #if defined(__GLIBC__) #include #endif +inline ExxLriRuntime make_exx_lri_runtime() +{ + return {PARAM.inp.cal_force, + PARAM.inp.cal_stress, + PARAM.inp.esolver_type == "tddft", + PARAM.inp.nspin, + GlobalV::MY_RANK, + PARAM.globalv.global_out_dir, + GlobalV::ofs_running}; +} + +namespace ExxLriDetail +{ +using CoulombParam + = std::map>>; + +inline void trim_malloc_cache() +{ +#if defined(__GLIBC__) + malloc_trim(0); +#endif +} + +inline double default_spencer_rcut(const UnitCell& ucell, const K_Vectors& kv) +{ + return std::pow(0.75 * kv.get_nkstot_nospin() * ucell.omega / (ModuleBase::PI), 1.0 / 3.0); +} + +inline bool rotate_abfs_in_place_for_current_full_matrix(const Exx_Info_RI&) +{ + // The rotated-basis Ewald split uses the full rotated ABFS for the + // short-range channel and a separate N=0 truncation only for the + // Gaussian long-range channel. + return true; +} + +inline bool should_rotate_abfs_for_init(const Exx_Info_RI& info) +{ + return info.rotate_abfs && rotate_abfs_in_place_for_current_full_matrix(info); +} + +inline bool allow_rt_tddft_ewald_force_stress_bypass(const ExxLriRuntime& runtime) +{ + return runtime.realtime_tddft; +} + +inline void print_rt_tddft_ewald_force_stress_warning_once(const ExxLriRuntime& runtime) +{ + static bool warning_printed = false; + if (!warning_printed && runtime.rank == 0) + { + std::cout << "RT-TDDFT Ewald moment/split skips long-range EXX force/stress construction." + << std::endl; + warning_printed = true; + } +} + +inline CoulombParam build_center2_cut_coulomb_param(const CoulombParam& coulomb_param, + const UnitCell& ucell, + const K_Vectors& kv, + bool* synthesized_rcut = nullptr) +{ + CoulombParam center2_param = RI_Util::update_coulomb_param(coulomb_param, ucell, &kv); + const double fallback_rcut = default_spencer_rcut(ucell, kv); + bool used_fallback_rcut = false; + + for (auto& param_list: center2_param) + { + if (param_list.first != Conv_Coulomb_Pot_K::Coulomb_Type::Fock) + { + continue; + } + for (auto& param: param_list.second) + { + auto rcut_it = param.find("Rcut"); + if (rcut_it == param.end() || rcut_it->second.empty()) + { + param["Rcut"] = ModuleBase::GlobalFunc::TO_STRING(fallback_rcut); + used_fallback_rcut = true; + } + } + } + + if (synthesized_rcut != nullptr) + { + *synthesized_rcut = used_fallback_rcut; + } + return center2_param; +} + +inline double get_total_fock_alpha(const CoulombParam& coulomb_param) +{ + double alpha_sum = 0.0; + const auto fock_it = coulomb_param.find(Conv_Coulomb_Pot_K::Coulomb_Type::Fock); + if (fock_it == coulomb_param.end()) + { + return alpha_sum; + } + for (const auto& param: fock_it->second) + { + const auto alpha_it = param.find("alpha"); + if (alpha_it == param.end() || alpha_it->second.empty()) + { + continue; + } + alpha_sum += std::stod(alpha_it->second); + } + return alpha_sum; +} + +template +struct has_weighted_short_config : std::false_type +{ +}; + +template +struct has_weighted_short_config> : std::true_type +{ +}; + +template +struct has_set_weighted_short_config : std::false_type +{ +}; + +template +struct has_set_weighted_short_config< + T, + std::void_t().set_weighted_short_config( + std::declval()))>> : std::true_type +{ +}; + +template +struct has_weighted_short_stats_member : std::false_type +{ +}; + +template +struct has_weighted_short_stats_member().weighted_short_stats)>> + : std::true_type +{ +}; + +template +constexpr bool supports_weighted_short_v + = has_weighted_short_config::value && has_set_weighted_short_config::value + && has_weighted_short_stats_member::value; + +inline void print_weighted_short_old_libri_warning_once(const double threshold, const int rank) +{ + static bool warning_printed = false; + if (!warning_printed && rank == 0) + { + std::cout << "EXX weighted short requested with exx_vcd_threshold=" << threshold + << ", but the linked LibRI does not provide weighted-short screening. " + << "Falling back to the legacy unscreened EXX path." << std::endl; + warning_printed = true; + } +} + +template +typename std::enable_if, void>::type maybe_set_weighted_short_config( + Texx& exx, + const Exx_Info_RI& info, + const int) +{ + typename Texx::Weighted_Short_Config weighted_short_cfg; + weighted_short_cfg.weighted_short_threshold = info.V_cd_threshold; + weighted_short_cfg.weighted_short_stats_only = info.V_cd_stats_only; + weighted_short_cfg.weighted_short_only = info.V_cd_short_only; + exx.set_weighted_short_config(weighted_short_cfg); +} + +template +typename std::enable_if, void>::type maybe_set_weighted_short_config( + Texx&, + const Exx_Info_RI& info, + const int rank) +{ + if (info.V_cd_threshold > 0.0) + { + print_weighted_short_old_libri_warning_once(info.V_cd_threshold, rank); + } +} + +template +typename std::enable_if, void>::type maybe_print_weighted_short_stats( + const Texx& exx_channel, + const Exx_Info_RI& info, + const std::string& ds_suffix, + const std::string& v_suffix, + const int rank) +{ + if (rank == 0 && info.V_cd_threshold > 0.0) + { + const auto& vcd_stats = exx_channel.weighted_short_stats; + const double skip_pct = (vcd_stats.weighted_short_candidates == 0) + ? 0.0 + : 100.0 * static_cast(vcd_stats.weighted_short_skips) + / static_cast(vcd_stats.weighted_short_candidates); + std::cout << "EXX weighted short[" << ds_suffix << "|" << v_suffix << "] " + << "thr=" << info.V_cd_threshold + << ", stats_only=" << static_cast(info.V_cd_stats_only) + << ", candidates=" << vcd_stats.weighted_short_candidates + << ", skips=" << vcd_stats.weighted_short_skips + << ", skip_pct=" << skip_pct + << ", max_score=" << vcd_stats.weighted_short_max_score + << std::endl; + } +} + +template +typename std::enable_if, void>::type maybe_print_weighted_short_stats( + const Texx&, + const Exx_Info_RI&, + const std::string&, + const std::string&, + const int) +{ +} + +inline std::vector> build_rotation_rows(const std::vector& moments, + const double threshold) +{ + const std::size_t n = moments.size(); + std::vector> rows; + rows.reserve(n); + + if (n == 0) + { + return rows; + } + + const double norm2 = std::inner_product(moments.begin(), moments.end(), moments.begin(), 0.0); + const double norm = std::sqrt(norm2); + const double ortho_tol = std::max(threshold, 10.0 * std::numeric_limits::epsilon()); + if (norm <= threshold) + { + rows.assign(n, std::vector(n, 0.0)); + for (std::size_t i = 0; i != n; ++i) + { + rows[i][i] = 1.0; + } + return rows; + } + + rows.emplace_back(n, 0.0); + for (std::size_t i = 0; i != n; ++i) + { + rows[0][i] = moments[i] / norm; + } + + for (std::size_t basis = 0; basis != n && rows.size() != n; ++basis) + { + std::vector candidate(n, 0.0); + candidate[basis] = 1.0; + + for (const auto& row: rows) + { + const double dot = std::inner_product(candidate.begin(), candidate.end(), row.begin(), 0.0); + for (std::size_t i = 0; i != n; ++i) + { + candidate[i] -= dot * row[i]; + } + } + + const double candidate_norm2 + = std::inner_product(candidate.begin(), candidate.end(), candidate.begin(), 0.0); + if (candidate_norm2 <= ortho_tol * ortho_tol) + { + continue; + } + + const double candidate_norm = std::sqrt(candidate_norm2); + for (double& value: candidate) + { + value /= candidate_norm; + } + rows.push_back(std::move(candidate)); + } + + if (rows.size() != n) + { + throw std::runtime_error("Failed to build a complete ABFS rotation basis."); + } + return rows; +} + +inline Numerical_Orbital_Lm combine_orbital_block( + const std::vector& original_block, + const std::vector& coeffs, + const int output_index, + const bool cal_force) +{ + if (original_block.empty()) + { + throw std::runtime_error("Cannot combine an empty ABFS block."); + } + if (original_block.size() != coeffs.size()) + { + throw std::runtime_error("ABFS rotation coefficients do not match the orbital block size."); + } + + const Numerical_Orbital_Lm& ref = original_block.front(); + const int nr = ref.getNr(); + std::vector psi(nr, 0.0); + for (std::size_t iorb = 0; iorb != original_block.size(); ++iorb) + { + const Numerical_Orbital_Lm& orb = original_block[iorb]; + if (orb.getNr() != nr || orb.getL() != ref.getL() || orb.getType() != ref.getType()) + { + throw std::runtime_error("ABFS rotation requires a consistent radial grid within each (T, L) block."); + } + for (int ir = 0; ir != nr; ++ir) + { + psi[ir] += coeffs[iorb] * orb.getPsi(ir); + } + } + + const Numerical_Orbital_Lm& out_ref = original_block.at(output_index); + Numerical_Orbital_Lm rotated; + rotated.set_orbital_info(out_ref.getLabel(), + out_ref.getType(), + out_ref.getL(), + out_ref.getChi(), + nr, + ref.getRab(), + ref.getRadial(), + Numerical_Orbital_Lm::Psi_Type::Psi, + ModuleBase::GlobalFunc::VECTOR_TO_PTR(psi), + out_ref.getNk(), + out_ref.getDk(), + out_ref.getDruniform(), + false, + true, + cal_force); + return rotated; +} + +inline void rotate_abfs_by_multipole(std::vector>>& abfs, + const double threshold, + const ExxLriRuntime& runtime) +{ + const auto multipole = Exx_Abfs::Construct_Orbs::get_multipole(abfs); + if (runtime.rank == 0) + { + std::cout << "\nRotated ABFS multipole summary" << std::endl; + std::cout << "threshold for displayed zero moments = " << threshold << std::endl; + std::cout << std::setprecision(16); + } + for (std::size_t T = 0; T != abfs.size(); ++T) + { + for (std::size_t L = 0; L != abfs[T].size(); ++L) + { + if (abfs[T][L].empty()) + { + continue; + } + + if (abfs[T][L].size() <= 1) + { + if (runtime.rank == 0) + { + std::cout << "Atom type " << T << ", L " << L << ", N 0" + << ", multipole before rotation: " << multipole[T][L][0] + << ", multipole after rotation: " << multipole[T][L][0] << std::endl; + } + continue; + } + + const std::vector> rows = build_rotation_rows(multipole[T][L], threshold); + const std::vector original_block = abfs[T][L]; + for (std::size_t N = 0; N != abfs[T][L].size(); ++N) + { + const double rotated_moment_raw + = std::inner_product(rows[N].begin(), rows[N].end(), multipole[T][L].begin(), 0.0); + const double rotated_moment + = (std::abs(rotated_moment_raw) <= threshold) ? 0.0 : rotated_moment_raw; + if (runtime.rank == 0) + { + std::cout << "Atom type " << T << ", L " << L << ", N " << N + << ", multipole before rotation: " << multipole[T][L][N] + << ", multipole after rotation: " << rotated_moment << std::endl; + } + abfs[T][L][N] = combine_orbital_block(original_block, rows[N], N, runtime.cal_force); + } + } + } +} + +inline std::vector>> prepare_abfs( + const UnitCell& ucell, + const LCAO_Orbitals& orb, + const std::vector>>& lcaos, + const Exx_Info_RI& info, + const double pca_threshold, + const std::vector& files_abfs, + const ExxLriRuntime& runtime) +{ + std::vector>> abfs + = Exx_Abfs::Construct_Orbs::abfs_same_atom( + ucell, + orb, + lcaos, + info.kmesh_times, + pca_threshold); + if (!files_abfs.empty()) + { + abfs = Exx_Abfs::IO::construct_abfs(abfs, orb, files_abfs, info.kmesh_times); + } + Exx_Abfs::Construct_Orbs::filter_empty_orbs(abfs); + if (should_rotate_abfs_for_init(info)) + { + rotate_abfs_by_multipole(abfs, info.multip_moments_threshold, runtime); + } + return abfs; +} + +struct AuxLongPrefixPermutation +{ + std::vector> old_to_new_by_type; + std::vector long_prefix_size_by_type; +}; + +inline AuxLongPrefixPermutation build_long_prefix_permutation( + const std::vector>>& abfs, + const double multipole_threshold) +{ + const ModuleBase::Element_Basis_Index::Range range = ModuleBase::Element_Basis_Index::construct_range(abfs); + const ModuleBase::Element_Basis_Index::IndexLNM index = ModuleBase::Element_Basis_Index::construct_index(range); + const auto multipoles = Exx_Abfs::Construct_Orbs::get_multipole(abfs); + + AuxLongPrefixPermutation permutation; + permutation.old_to_new_by_type.resize(abfs.size()); + permutation.long_prefix_size_by_type.resize(abfs.size(), 0); + + for (std::size_t T = 0; T != abfs.size(); ++T) + { + const std::size_t full_size = index[T].count_size; + permutation.old_to_new_by_type[T].assign(full_size, 0); + + std::vector old_order; + old_order.reserve(full_size); + std::vector pushed(full_size, false); + + for (std::size_t L = 0; L != abfs[T].size(); ++L) + { + if (abfs[T][L].empty()) + { + continue; + } + for (std::size_t N = 0; N != abfs[T][L].size(); ++N) + { + if (std::abs(multipoles[T][L][N]) <= multipole_threshold) + { + continue; + } + for (std::size_t M = 0; M != 2 * L + 1; ++M) + { + const std::size_t old_index = index[T][L][N][M]; + old_order.push_back(old_index); + pushed[old_index] = true; + } + } + } + permutation.long_prefix_size_by_type[T] = old_order.size(); + + for (std::size_t L = 0; L != abfs[T].size(); ++L) + { + for (std::size_t N = 0; N != abfs[T][L].size(); ++N) + { + for (std::size_t M = 0; M != 2 * L + 1; ++M) + { + const std::size_t old_index = index[T][L][N][M]; + if (!pushed[old_index]) + { + old_order.push_back(old_index); + } + } + } + } + + if (old_order.size() != full_size) + { + throw std::runtime_error("Failed to construct the rotated-ABFS long-prefix permutation."); + } + + for (std::size_t new_index = 0; new_index != old_order.size(); ++new_index) + { + permutation.old_to_new_by_type[T][old_order[new_index]] = new_index; + } + } + + return permutation; +} + +template +inline RI::Tensor permute_aux_tensor_rows( + const RI::Tensor& tensor_in, + const std::vector& old_to_new) +{ + if (tensor_in.empty()) + { + return tensor_in; + } + if (tensor_in.shape.empty() || tensor_in.shape[0] != old_to_new.size()) + { + throw std::runtime_error("Auxiliary-row permutation does not match tensor shape."); + } + + auto shape_out = tensor_in.shape; + RI::Tensor tensor_out(shape_out); + const std::size_t slice_size = tensor_in.get_shape_all() / tensor_in.shape[0]; + for (std::size_t old_index = 0; old_index != old_to_new.size(); ++old_index) + { + std::copy_n(tensor_in.ptr() + old_index * slice_size, + slice_size, + tensor_out.ptr() + old_to_new[old_index] * slice_size); + } + return tensor_out; +} + +template +inline RI::Tensor permute_aux_tensor_matrix( + const RI::Tensor& tensor_in, + const std::vector& row_old_to_new, + const std::vector& col_old_to_new) +{ + if (tensor_in.empty()) + { + return tensor_in; + } + if (tensor_in.shape.size() != 2 + || tensor_in.shape[0] != row_old_to_new.size() + || tensor_in.shape[1] != col_old_to_new.size()) + { + throw std::runtime_error("Auxiliary-matrix permutation does not match tensor shape."); + } + + RI::Tensor tensor_out({tensor_in.shape[0], tensor_in.shape[1]}); + for (std::size_t old_row = 0; old_row != row_old_to_new.size(); ++old_row) + { + const std::size_t new_row = row_old_to_new[old_row]; + for (std::size_t old_col = 0; old_col != col_old_to_new.size(); ++old_col) + { + tensor_out(new_row, col_old_to_new[old_col]) = tensor_in(old_row, old_col); + } + } + return tensor_out; +} + +template +inline void permute_aux_tensor_map_rows_by_type( + const UnitCell& ucell, + const std::vector>& old_to_new_per_type, + std::map>, RI::Tensor>>& tensors_io) +{ + for (auto& tensors_I: tensors_io) + { + const int type_I = ucell.iat2it[tensors_I.first]; + const auto& permutation = old_to_new_per_type.at(type_I); + for (auto& tensors_JR: tensors_I.second) + { + tensors_JR.second = permute_aux_tensor_rows(tensors_JR.second, permutation); + } + } +} + +template +inline void permute_aux_tensor_map_matrices_by_type( + const UnitCell& ucell, + const std::vector>& old_to_new_per_type, + std::map>, RI::Tensor>>& tensors_io) +{ + for (auto& tensors_I: tensors_io) + { + const int type_I = ucell.iat2it[tensors_I.first]; + const auto& row_permutation = old_to_new_per_type.at(type_I); + for (auto& tensors_JR: tensors_I.second) + { + const int type_J = ucell.iat2it[tensors_JR.first.first]; + const auto& col_permutation = old_to_new_per_type.at(type_J); + tensors_JR.second = permute_aux_tensor_matrix(tensors_JR.second, row_permutation, col_permutation); + } + } +} + +template +inline RI::Tensor extract_aux_tensor_row_prefix( + const RI::Tensor& tensor_in, + const std::size_t prefix_size) +{ + if (tensor_in.empty()) + { + return tensor_in; + } + if (tensor_in.shape.empty() || tensor_in.shape[0] < prefix_size) + { + throw std::runtime_error("Auxiliary-row prefix does not match tensor shape."); + } + + auto shape_out = tensor_in.shape; + shape_out[0] = prefix_size; + RI::Tensor tensor_out(shape_out); + const std::size_t slice_size = tensor_in.get_shape_all() / tensor_in.shape[0]; + std::copy_n(tensor_in.ptr(), prefix_size * slice_size, tensor_out.ptr()); + return tensor_out; +} + +template +inline RI::Tensor extract_aux_tensor_matrix_prefix( + const RI::Tensor& tensor_in, + const std::size_t row_prefix_size, + const std::size_t col_prefix_size) +{ + if (tensor_in.empty()) + { + return tensor_in; + } + if (tensor_in.shape.size() != 2 + || tensor_in.shape[0] < row_prefix_size + || tensor_in.shape[1] < col_prefix_size) + { + throw std::runtime_error("Auxiliary-matrix prefix does not match tensor shape."); + } + + RI::Tensor tensor_out({row_prefix_size, col_prefix_size}); + for (std::size_t row = 0; row != row_prefix_size; ++row) + { + std::copy_n(tensor_in.ptr() + row * tensor_in.shape[1], + col_prefix_size, + tensor_out.ptr() + row * col_prefix_size); + } + return tensor_out; +} + +template +inline std::map>, RI::Tensor>> +extract_aux_tensor_map_rows_prefix_by_type( + const UnitCell& ucell, + const std::vector& prefix_size_per_type, + const std::map>, RI::Tensor>>& tensors_in) +{ + std::map>, RI::Tensor>> tensors_out; + for (const auto& tensors_I: tensors_in) + { + const int type_I = ucell.iat2it[tensors_I.first]; + const std::size_t prefix_size = prefix_size_per_type.at(type_I); + for (const auto& tensors_JR: tensors_I.second) + { + tensors_out[tensors_I.first][tensors_JR.first] + = extract_aux_tensor_row_prefix(tensors_JR.second, prefix_size); + } + } + return tensors_out; +} + +template +inline std::map>, RI::Tensor>> +extract_aux_tensor_map_matrices_prefix_by_type( + const UnitCell& ucell, + const std::vector& prefix_size_per_type, + const std::map>, RI::Tensor>>& tensors_in) +{ + std::map>, RI::Tensor>> tensors_out; + for (const auto& tensors_I: tensors_in) + { + const int type_I = ucell.iat2it[tensors_I.first]; + const std::size_t row_prefix_size = prefix_size_per_type.at(type_I); + for (const auto& tensors_JR: tensors_I.second) + { + const int type_J = ucell.iat2it[tensors_JR.first.first]; + const std::size_t col_prefix_size = prefix_size_per_type.at(type_J); + tensors_out[tensors_I.first][tensors_JR.first] + = extract_aux_tensor_matrix_prefix(tensors_JR.second, row_prefix_size, col_prefix_size); + } + } + return tensors_out; +} + +template +inline std::vector>> filter_ewald_exact_near_pairs_for_atom( + const UnitCell& ucell, + const int iat0, + const std::vector>>& list_A1_full, + const std::vector& abfs_cutoff) +{ + std::vector>> list_A1_near; + const int it0 = ucell.iat2it[iat0]; + const int ia0 = ucell.iat2ia[iat0]; + const ModuleBase::Vector3 tau0 = ucell.atoms[it0].tau[ia0]; + + for (const auto& jr: list_A1_full) + { + const int iat1 = jr.first; + const int it1 = ucell.iat2it[iat1]; + const int ia1 = ucell.iat2ia[iat1]; + const ModuleBase::Vector3 tau1 = ucell.atoms[it1].tau[ia1]; + const auto delta_R = -tau0 + tau1 + (RI_Util::array3_to_Vector3(jr.second) * ucell.latvec); + const double exact_near_rcut = abfs_cutoff[it0] + abfs_cutoff[it1]; + if (delta_R.norm() * ucell.lat0 < exact_near_rcut) + { + list_A1_near.push_back(jr); + } + } + return list_A1_near; +} + +template +inline std::map>, RI::Tensor>> +build_ewald_bare_coulomb_with_moment( + const Exx_Info_RI& info, + const CoulombParam& ewald_coulomb_param, + const UnitCell& ucell, + const std::vector>>& abfs, + const ModuleBase::Element_Basis_Index::IndexPermutation& abfs_old_to_new, + const std::pair, std::vector>>>>& list_As_Vs, + LRI_CV& cv, + const ExxLriRuntime& runtime, + const bool ignore_upper_coulomb_cutoff = false, + const bool force_moment_far_field = false, + const bool diagnostic_only = false) +{ + if (!info.coul_moment && !force_moment_far_field) + { + auto Vs_full = cv.cal_Vs(ucell, + list_As_Vs.first, + list_As_Vs.second[0], + {{"writable_Vws", true}}); + cv.Vws = LRI_CV_Tools::get_CVws(ucell, Vs_full); + return Vs_full; + } + if (!diagnostic_only + && (runtime.cal_force || runtime.cal_stress) + && !ExxLriDetail::allow_rt_tddft_ewald_force_stress_bypass(runtime)) + { + throw std::invalid_argument("exx_coul_moment for Ewald currently supports energy/SCF only."); + } + if (runtime.cal_force || runtime.cal_stress) + { + ExxLriDetail::print_rt_tddft_ewald_force_stress_warning_once(runtime); + } + const double moment_value_scale = ExxLriDetail::get_total_fock_alpha(ewald_coulomb_param); + if (std::abs(moment_value_scale) <= std::numeric_limits::epsilon()) + { + throw std::invalid_argument("Failed to determine the Ewald Fock alpha for moment bare Coulomb construction."); + } + + Moment_abfs moment_abfs(info); + moment_abfs.cal_multipole(abfs); + const std::vector abfs_cutoff = Exx_Abfs::Construct_Orbs::get_Rcut(abfs); + std::map>, RI::Tensor>> Vs_direct; + cv.Vws.clear(); + + for (const int iat0: list_As_Vs.first) + { + const auto list_A1_near = filter_ewald_exact_near_pairs_for_atom( + ucell, + iat0, + list_As_Vs.second[0], + abfs_cutoff); + if (list_A1_near.empty()) + { + continue; + } + auto Vs_near = cv.cal_Vs(ucell, + std::vector{iat0}, + list_A1_near, + {{"writable_Vws", false}}); + Vs_direct = Vs_direct.empty() ? std::move(Vs_near) : LRI_CV_Tools::add(Vs_direct, Vs_near); + } + + moment_abfs.cal_VR(ucell, + abfs, + list_As_Vs, + abfs_cutoff, + 0.0, + cv, + Vs_direct, + abfs_old_to_new, + false, + false, + false, + true, + moment_value_scale, + ignore_upper_coulomb_cutoff); + cv.Vws = LRI_CV_Tools::get_CVws(ucell, Vs_direct); + return Vs_direct; +} + +template +inline std::map>, RI::Tensor>> +build_explicit_ewald_bare_coulomb( + const UnitCell& ucell, + const std::vector>>& abfs, + const std::vector& keys, + LRI_CV& cv) +{ + std::map>, RI::Tensor>> result; + const std::vector abfs_cutoff = Exx_Abfs::Construct_Orbs::get_Rcut(abfs); + const auto grouped = EwaldVqDetail::group_tail_keys_by_atom(keys); + for (typename std::map>>>::const_iterator grouped_it + = grouped.begin(); + grouped_it != grouped.end(); + ++grouped_it) + { + const int iat0 = grouped_it->first; + const auto list_A1 = filter_ewald_exact_near_pairs_for_atom( + ucell, + iat0, + grouped_it->second, + abfs_cutoff); + if (list_A1.empty()) continue; + const auto part = cv.cal_Vs(ucell, + std::vector{iat0}, + list_A1, + {{"writable_Vws", false}}); + for (typename std::map>, RI::Tensor>>::const_iterator + part_it = part.begin(); + part_it != part.end(); + ++part_it) + { + result[part_it->first].insert(part_it->second.begin(), part_it->second.end()); + } + } + return result; +} + +template +struct EwaldRealspaceRange +{ + using TailDistribution + = std::pair, std::vector>>>>; + using TensorMap + = std::map>, RI::Tensor>>; + + std::array period{}; + TailDistribution distribution; + TensorMap bare_coulomb; +}; + +inline void print_ewald_tail_report(const Exx_Info_RI& info, + const EwaldVqDetail::TailMode mode, + const std::array& production_period, + const std::array& probe_period, + const EwaldVqDetail::TailStats& stats, + const EwaldVqDetail::TailDecision decision, + const int expansion, + const double exact_ccp_rmesh_times) +{ + const std::ios::fmtflags saved_flags = std::cout.flags(); + const std::streamsize saved_precision = std::cout.precision(); + std::cout << std::scientific << std::setprecision(12) + << "Ewald short-range tail:\n" + << " mode: " << EwaldVqDetail::tail_mode_name(mode) << '\n' + << " lambda: " << info.ewald_lambda << '\n' + << " full-range source: exx_ccp_rmesh_times=" << info.ccp_rmesh_times << '\n' + << " exact radial mesh factor: " << exact_ccp_rmesh_times << '\n' + << " production period: " << production_period[0] << ' ' << production_period[1] << ' ' + << production_period[2] << '\n' + << " probe period: " << probe_period[0] << ' ' << probe_period[1] << ' ' + << probe_period[2] << '\n' + << " production blocks: " << stats.production_blocks << '\n' + << " shell blocks: " << stats.shell_blocks << '\n' + << " max |Vbare-Vgauss|_F: " << stats.max_norm << '\n' + << " sum |Vbare-Vgauss|_F: " << stats.sum_norm << '\n' + << " relative max: " << EwaldVqDetail::relative_tail_max(stats) << '\n' + << " relative sum: " << EwaldVqDetail::relative_tail_sum(stats) << '\n' + << " max Hermitian residual: " << stats.hermitian_residual << '\n' + << " worst I,J,R: " << stats.worst_key.atom_i << ' ' << stats.worst_key.atom_j << ' ' + << stats.worst_key.cell[0] << ' ' << stats.worst_key.cell[1] << ' ' + << stats.worst_key.cell[2] << '\n' + << " worst distance/bare/Gaussian: " << stats.worst_distance << ' ' + << stats.worst_bare_norm << ' ' << stats.worst_gaussian_norm << '\n' + << " coverage complete: " << (stats.coverage_complete ? "true" : "false") << '\n' + << " expansion: " << expansion << '\n' + << " decision: " << EwaldVqDetail::tail_decision_name(decision) << std::endl; + std::cout.flags(saved_flags); + std::cout.precision(saved_precision); +} + +template +inline EwaldRealspaceRange select_ewald_realspace_range( + const Exx_Info_RI& info, + const CoulombParam& ewald_coulomb_param, + const UnitCell& ucell, + const std::vector>>& abfs, + const ModuleBase::Element_Basis_Index::IndexPermutation& abfs_old_to_new, + const std::vector& atoms, + const std::array& requested_period, + const MPI_Comm mpi_comm, + LRI_CV& cv, + Ewald_Vq& evq, + const ExxLriRuntime& runtime) +{ + EwaldRealspaceRange selected; + selected.period = requested_period; + const auto tail_mode = EwaldVqDetail::parse_tail_mode(info.ewald_tail_check); + if (EwaldVqDetail::uses_adaptive_realspace_range(tail_mode) + && (runtime.cal_force || runtime.cal_stress)) + { + throw std::invalid_argument( + "Adaptive Ewald tail enlargement currently supports energy/SCF only; " + "use exx_ewald_tail_check=warn for force or stress calculations."); + } + const EwaldVqDetail::TailTolerances tolerances{info.ewald_tail_abs_tol, + info.ewald_tail_rel_tol, + info.ewald_tail_sum_rel_tol}; + const std::array periodic + = info.ewald_dimension == 2 ? std::array{true, true, false} + : std::array{true, true, true}; + int mpi_rank = 0; + int mpi_size = 1; + MPI_Comm_rank(mpi_comm, &mpi_rank); + MPI_Comm_size(mpi_comm, &mpi_size); + + int expansions = 0; + while (true) + { + selected.distribution = RI::Distribute_Equally::distribute_atoms_periods( + mpi_comm, + atoms, + selected.period, + 2, + false); + selected.bare_coulomb = build_ewald_bare_coulomb_with_moment( + info, + ewald_coulomb_param, + ucell, + abfs, + abfs_old_to_new, + selected.distribution, + cv, + runtime); + evq.init_ions(ucell, selected.period); + + if (tail_mode == EwaldVqDetail::TailMode::Off) + { + break; + } + + const bool truncate_analytic_tail = tail_mode == EwaldVqDetail::TailMode::Enlarge + || tail_mode == EwaldVqDetail::TailMode::Strict; + const auto production_difference = evq.cal_realspace_difference( + ucell, + selected.distribution.first, + selected.distribution.second[0], + selected.bare_coulomb, + truncate_analytic_tail); + std::size_t local_production_blocks = 0; + double local_reference_norm = 0.0; + for (const auto& blocks_i : production_difference) + { + local_production_blocks += blocks_i.second.size(); + for (const auto& block : blocks_i.second) + { + local_reference_norm = std::max(local_reference_norm, block.second.norm(2.0)); + } + } + + const auto probe_period = EwaldVqDetail::expand_odd_period( + selected.period, + periodic, + info.ewald_tail_guard_cells); + const auto local_shell_keys = EwaldVqDetail::distribute_tail_shell_keys( + atoms, + selected.period, + probe_period, + mpi_rank, + mpi_size); + auto shell_bare = build_explicit_ewald_bare_coulomb( + ucell, + abfs, + local_shell_keys, + cv); + const auto stats = evq.cal_short_range_tail_stats( + ucell, + local_shell_keys, + shell_bare, + local_production_blocks, + local_reference_norm); + const auto decision = EwaldVqDetail::decide_tail( + stats, + tolerances, + tail_mode, + expansions, + info.ewald_tail_max_expansions); + if (mpi_rank == 0) + { + print_ewald_tail_report(info, + tail_mode, + selected.period, + probe_period, + stats, + decision, + expansions, + evq.get_exact_ccp_rmesh_times()); + } + + if (decision == EwaldVqDetail::TailDecision::Expand) + { + selected.period = probe_period; + ++expansions; + continue; + } + if (decision == EwaldVqDetail::TailDecision::Fail) + { + throw std::runtime_error( + "Ewald short-range tail did not converge within the configured expansion limit"); + } + break; + } + return selected; +} + +template +inline std::map>, RI::Tensor>> +filter_tensor_map_by_max_abs_threshold( + const std::map>, RI::Tensor>>& tensor_map, + const RI::Global_Func::To_Real_t& threshold) +{ + typename RI::RI_Tools::T_filter_func filter_func + = [](const RI::Tensor& tensor, const RI::Global_Func::To_Real_t& threshold_in) -> bool + { + return tensor.norm(std::numeric_limits::max()) > threshold_in; + }; + return RI::RI_Tools::filter(tensor_map, filter_func, threshold); +} +} + template void Exx_LRI::init(const MPI_Comm &mpi_comm_in, const UnitCell &ucell, @@ -48,21 +1089,17 @@ void Exx_LRI::init(const MPI_Comm &mpi_comm_in, this->lcaos = Exx_Abfs::Construct_Orbs::change_orbs( orb, this->info.kmesh_times ); Exx_Abfs::Construct_Orbs::filter_empty_orbs(this->lcaos); - if(abfs_in.empty()) + if (abfs_in.empty()) { - const std::vector>> - abfs_same_atom = Exx_Abfs::Construct_Orbs::abfs_same_atom(ucell, orb, this->lcaos, this->info.kmesh_times, this->info.pca_threshold ); - if(this->info.files_abfs.empty()) - { this->abfs = abfs_same_atom;} - else - { this->abfs = Exx_Abfs::IO::construct_abfs( abfs_same_atom, orb, this->info.files_abfs, this->info.kmesh_times ); } + this->abfs = ExxLriDetail::prepare_abfs( + ucell, orb, this->lcaos, this->info, this->info.pca_threshold, this->info.files_abfs, this->runtime); } else { this->abfs = abfs_in; + Exx_Abfs::Construct_Orbs::filter_empty_orbs(this->abfs); } - Exx_Abfs::Construct_Orbs::filter_empty_orbs(this->abfs); - Exx_Abfs::Construct_Orbs::print_orbs_size(ucell, this->abfs, GlobalV::ofs_running); + Exx_Abfs::Construct_Orbs::print_orbs_size(ucell, this->abfs, this->runtime.log); for( size_t T=0; T!=this->abfs.size(); ++T ) { @@ -70,7 +1107,41 @@ void Exx_LRI::init(const MPI_Comm &mpi_comm_in, } this->exx_objs.clear(); + this->abfs_old_to_new_per_type.clear(); + this->abfs_long_prefix_size_per_type.clear(); const auto settings = RI_Util::update_coulomb_settings(this->info.coulomb_param, ucell, this->p_kv); + const bool rotated_n0_requested + = this->info.rotate_abfs + && this->info.coul_moment + && settings.find(Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) != settings.end(); + this->use_rotated_n0_long_range = rotated_n0_requested; + if (this->use_rotated_n0_long_range) + { + if ((this->runtime.cal_force || this->runtime.cal_stress) + && !ExxLriDetail::allow_rt_tddft_ewald_force_stress_bypass(this->runtime)) + { + throw std::invalid_argument( + "Rotated-ABFS split Ewald currently supports energy/SCF only."); + } + if (this->runtime.cal_force || this->runtime.cal_stress) + { + ExxLriDetail::print_rt_tddft_ewald_force_stress_warning_once(this->runtime); + } + const auto permutation = ExxLriDetail::build_long_prefix_permutation(this->abfs, + this->info.multip_moments_threshold); + this->abfs_old_to_new_per_type = permutation.old_to_new_by_type; + this->abfs_long_prefix_size_per_type = permutation.long_prefix_size_by_type; + if (this->runtime.rank == 0) + { + std::cout << "Rotated ABFS long-prefix sizes by type:"; + for (std::size_t T = 0; T != this->abfs_long_prefix_size_per_type.size(); ++T) + { + std::cout << " T" << T << "=" << this->abfs_long_prefix_size_per_type[T]; + } + std::cout << std::endl; + } + } + this->MGT = std::make_shared(); this->refresh_coulomb(ucell, orb, settings); @@ -110,17 +1181,28 @@ void Exx_LRI::refresh_coulomb(const UnitCell& ucell, // In particular, reusing Matrix_Orbs maps would retain old entries inserted by init(). this->exx_objs.erase(settings_list.first); auto& object = this->exx_objs[settings_list.first]; + const auto tail_mode = EwaldVqDetail::parse_tail_mode(this->info.ewald_tail_check); + const double exact_ccp_rmesh_times + = settings_list.first == Conv_Coulomb_Pot_K::Coulomb_Method::Ewald + && EwaldVqDetail::uses_adaptive_realspace_range(tail_mode) + ? EwaldVqDetail::adaptive_exact_rmesh_times( + this->info.ccp_rmesh_times, + Exx_Abfs::Construct_Orbs::get_Rcut(this->lcaos), + Exx_Abfs::Construct_Orbs::get_Rcut(this->abfs)) + : this->info.ccp_rmesh_times; object.abfs_ccp = Conv_Coulomb_Pot_K::cal_orbs_ccp( - this->abfs, settings_list.second.second, this->info.ccp_rmesh_times); + this->abfs, settings_list.second.second, exact_ccp_rmesh_times); object.cv.set_orbitals(ucell, orb, this->lcaos, this->abfs, object.abfs_ccp, - this->info.kmesh_times, this->MGT, settings_list.second.first); + this->info.kmesh_times, this->MGT, settings_list.second.first, + this->abfs_old_to_new_per_type); object.cv.set_info_ri(&this->info); if (settings_list.first == Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) { - const int evq_abfs_Lmax = this->abfs_Lmax_; + object.evq.set_info_ri(&this->info); object.evq.init(ucell, orb, this->mpi_comm, this->p_kv, this->lcaos, this->abfs, settings_list.second.second, this->MGT, this->info.ccp_rmesh_times, - this->info.kmesh_times, evq_abfs_Lmax); + exact_ccp_rmesh_times, this->info.ewald_lambda, this->info.kmesh_times, + this->info.ewald_dimension, this->abfs_old_to_new_per_type); } } this->coulomb_settings = settings; @@ -129,12 +1211,11 @@ void Exx_LRI::refresh_coulomb(const UnitCell& ucell, } template -void Exx_LRI::init_spencer( - const MPI_Comm& mpi_comm_in, - const UnitCell& ucell, - const K_Vectors& kv_in, - const LCAO_Orbitals& orb, - const std::vector>>& abfs_in) +void Exx_LRI::init_spencer(const MPI_Comm& mpi_comm_in, + const UnitCell& ucell, + const K_Vectors& kv_in, + const LCAO_Orbitals& orb, + const std::vector>>& abfs_in) { ModuleBase::TITLE("Exx_LRI", "init_spencer"); ModuleBase::timer::start("Exx_LRI", "init_spencer"); @@ -148,36 +1229,37 @@ void Exx_LRI::init_spencer( if (abfs_in.empty()) { - const std::vector>> abfs_same_atom - = Exx_Abfs::Construct_Orbs::abfs_same_atom( - ucell, orb, this->lcaos, this->info.kmesh_times, this->info.pca_threshold); - if (this->info.files_abfs.empty()) - { this->abfs = abfs_same_atom; } - else - { this->abfs = Exx_Abfs::IO::construct_abfs(abfs_same_atom, orb, this->info.files_abfs, this->info.kmesh_times); } + this->abfs = ExxLriDetail::prepare_abfs( + ucell, orb, this->lcaos, this->info, this->info.pca_threshold, this->info.files_abfs, this->runtime); } else { this->abfs = abfs_in; + Exx_Abfs::Construct_Orbs::filter_empty_orbs(this->abfs); } - Exx_Abfs::Construct_Orbs::filter_empty_orbs(this->abfs); - Exx_Abfs::Construct_Orbs::print_orbs_size(ucell, this->abfs, GlobalV::ofs_running); + Exx_Abfs::Construct_Orbs::print_orbs_size(ucell, this->abfs, this->runtime.log); for (size_t T = 0; T != this->abfs.size(); ++T) { this->abfs_Lmax_ = std::max(this->abfs_Lmax_, static_cast(this->abfs[T].size()) - 1); } - this->exx_objs.clear(); + this->exx_objs.clear(); + this->abfs_old_to_new_per_type.clear(); + this->abfs_long_prefix_size_per_type.clear(); + this->use_rotated_n0_long_range = false; this->coulomb_settings.clear(); - this->coulomb_settings[Conv_Coulomb_Pot_K::Coulomb_Method::Center2] = std::make_pair( - true, ExxLriDetail::build_center2_cut_coulomb_param(this->info.coulomb_param, ucell, kv_in)); + this->coulomb_settings[Conv_Coulomb_Pot_K::Coulomb_Method::Center2] + = std::make_pair(true, + ExxLriDetail::build_center2_cut_coulomb_param( + this->info.coulomb_param, ucell, kv_in)); this->MGT = std::make_shared(); const auto center2_settings = this->coulomb_settings.find(Conv_Coulomb_Pot_K::Coulomb_Method::Center2); if (center2_settings == this->coulomb_settings.end()) - { throw std::invalid_argument("Exx_LRI::init_spencer failed to prepare Center2 settings."); } - + { + throw std::invalid_argument("Exx_LRI::init_spencer failed to prepare Center2 settings."); + } this->exx_objs[Conv_Coulomb_Pot_K::Coulomb_Method::Center2].abfs_ccp = Conv_Coulomb_Pot_K::cal_orbs_ccp_spencer( this->abfs, center2_settings->second.second, @@ -196,7 +1278,6 @@ void Exx_LRI::init_spencer( ModuleBase::timer::end("Exx_LRI", "init_spencer"); } - template void Exx_LRI::cal_exx_ions(const UnitCell& ucell, const bool write_cv) @@ -212,6 +1293,13 @@ void Exx_LRI::cal_exx_ions(const UnitCell& ucell, std::vector atoms(ucell.nat); for(int iat=0; iat all_atoms; + for (int iat = 0; iat < ucell.nat; ++iat) + { + all_atoms.insert(iat); + } + int mpi_size = 1; + MPI_Comm_size(this->mpi_comm, &mpi_size); std::map atoms_pos; for(int iat=0; iat::cal_exx_ions(const UnitCell& ucell, this->exx_lri.set_parallel(this->mpi_comm, atoms_pos, latvec, period); // std::max(3) for gamma_only, list_A2 should contain cell {-1,0,1}. In the future distribute will be neighbour. - const std::array period_Vs = LRI_CV_Tools::cal_latvec_range(1+this->info.ccp_rmesh_times, ucell, orb_cutoff_); - const std::pair, std::vector>>>> + std::array period_Vs + = LRI_CV_Tools::cal_latvec_range(1+this->info.ccp_rmesh_times, ucell, orb_cutoff_); + std::pair, std::vector>>>> list_As_Vs = RI::Distribute_Equally::distribute_atoms_periods(this->mpi_comm, atoms, period_Vs, 2, false); + std::map>> selected_ewald_bare; + const auto ewald_settings = this->coulomb_settings.find(Conv_Coulomb_Pot_K::Coulomb_Method::Ewald); + if (ewald_settings != this->coulomb_settings.end()) + { + auto selected_range = ExxLriDetail::select_ewald_realspace_range( + this->info, + ewald_settings->second.second, + ucell, + this->abfs, + this->abfs_old_to_new_per_type, + atoms, + period_Vs, + this->mpi_comm, + this->exx_objs[Conv_Coulomb_Pot_K::Coulomb_Method::Ewald].cv, + this->exx_objs[Conv_Coulomb_Pot_K::Coulomb_Method::Ewald].evq, + this->runtime); + period_Vs = selected_range.period; + list_As_Vs = std::move(selected_range.distribution); + selected_ewald_bare = std::move(selected_range.bare_coulomb); + } std::map>> Vs; + std::map>> Vs_long; std::map, Ndim>>> dVs; - const bool cal_dCV = PARAM.inp.cal_force || PARAM.inp.cal_stress; - for(const auto &settings_list : this->coulomb_settings) + for (const auto& settings_list : this->coulomb_settings) { - std::map>> - Vs_temp = this->exx_objs[settings_list.first].cv.cal_Vs(ucell, - list_As_Vs.first, list_As_Vs.second[0], + auto Vs_temp = (settings_list.first == Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) + ? selected_ewald_bare + : this->exx_objs[settings_list.first].cv.cal_Vs( + ucell, + list_As_Vs.first, + list_As_Vs.second[0], {{"writable_Vws",true}}); - this->exx_objs[settings_list.first].cv.Vws = LRI_CV_Tools::get_CVws(ucell,Vs_temp); + if (settings_list.first != Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) + { + this->exx_objs[settings_list.first].cv.Vws = LRI_CV_Tools::get_CVws(ucell, Vs_temp); + } + if (settings_list.first == Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) { - this->exx_objs[settings_list.first].evq.init_ions(ucell, period_Vs); - const auto &coulomb_param = settings_list.second.second; + if (this->info.coul_moment && this->runtime.rank == 0) + { + std::cout << "Construct Ewald bare Coulomb blocks directly in the current ABFS basis:" + << " near-field exact + far-field moment." + << std::endl; + } + std::map>> Vs_ewald; - for(const auto ¶m_list : coulomb_param) + std::map>> Vs_ewald_long; + for (const auto& param_list : settings_list.second.second) { std::map>> Vs_ewald_temp; - switch(param_list.first) + switch (param_list.first) { case Conv_Coulomb_Pot_K::Coulomb_Type::Fock: { - double chi = this->exx_objs[settings_list.first].evq.get_singular_chi(ucell, param_list.second, 2.0); - Vs_ewald_temp = this->exx_objs[settings_list.first].evq.cal_Vs(ucell, chi, Vs_temp); + const double chi + = this->exx_objs[settings_list.first].evq.get_singular_chi(ucell, param_list.second, 2.0); + if (this->use_rotated_n0_long_range) + { + std::map>> Vs_ewald_long_temp; + if (mpi_size > 1) + { + MPI_Barrier(this->mpi_comm); + auto Vs_bare_root + = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vs_temp, all_atoms, all_atoms); + MPI_Barrier(this->mpi_comm); + if (this->runtime.rank == 0) + { + Vs_ewald_temp + = this->exx_objs[settings_list.first].evq.cal_short_range_Vs_serial_full( + ucell, + Vs_bare_root, + period_Vs); + Vs_ewald_long_temp + = this->exx_objs[settings_list.first].evq.cal_long_range_Vs_gauss_serial_full( + ucell, + chi, + period_Vs); + } + } + else + { + Vs_ewald_temp = this->exx_objs[settings_list.first].evq.cal_short_range_Vs( + ucell, + list_As_Vs.first, + list_As_Vs.second[0], + Vs_temp); + Vs_ewald_long_temp + = this->exx_objs[settings_list.first].evq.cal_long_range_Vs_gauss( + ucell, + chi); + } + Vs_ewald_long = Vs_ewald_long.empty() ? std::move(Vs_ewald_long_temp) + : LRI_CV_Tools::add(Vs_ewald_long, Vs_ewald_long_temp); + } + else + { + if (mpi_size > 1) + { + MPI_Barrier(this->mpi_comm); + auto Vs_bare_root + = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vs_temp, all_atoms, all_atoms); + MPI_Barrier(this->mpi_comm); + if (this->runtime.rank == 0) + { + Vs_ewald_temp = this->exx_objs[settings_list.first].evq.cal_Vs_serial_full( + ucell, + chi, + Vs_bare_root, + period_Vs); + } + } + else + { + Vs_ewald_temp = this->exx_objs[settings_list.first].evq.cal_Vs(ucell, chi, Vs_temp); + } + } break; } default: - { - throw std::invalid_argument( std::string(__FILE__) + " line " + std::to_string(__LINE__) ); - } + throw std::invalid_argument(std::string(__FILE__) + " line " + std::to_string(__LINE__)); } - // Vs_temp cannot be covered here - Vs_ewald = Vs_ewald.empty() ? Vs_ewald_temp : LRI_CV_Tools::add(Vs_ewald, Vs_ewald_temp); + + Vs_ewald = Vs_ewald.empty() ? std::move(Vs_ewald_temp) : LRI_CV_Tools::add(Vs_ewald, Vs_ewald_temp); + } + + Vs_temp = std::move(Vs_ewald); + if (this->use_rotated_n0_long_range) + { + Vs_long = Vs_long.empty() ? std::move(Vs_ewald_long) : LRI_CV_Tools::add(Vs_long, Vs_ewald_long); } - Vs_temp = Vs_ewald; } - Vs = Vs.empty() ? Vs_temp : LRI_CV_Tools::add(Vs, Vs_temp); - if (cal_dCV) + Vs = Vs.empty() ? std::move(Vs_temp) : LRI_CV_Tools::add(Vs, Vs_temp); + + if (this->runtime.cal_force || this->runtime.cal_stress) { - std::map, Ndim>>> - dVs_temp = this->exx_objs[settings_list.first].cv.cal_dVs(ucell, - list_As_Vs.first, list_As_Vs.second[0], - {{"writable_dVws",true}}); - this->exx_objs[settings_list.first].cv.dVws = LRI_CV_Tools::get_dCVws(ucell,dVs_temp); - dVs = dVs.empty() ? dVs_temp : LRI_CV_Tools::add(dVs, dVs_temp); + auto dVs_temp = this->exx_objs[settings_list.first].cv.cal_dVs( + ucell, + list_As_Vs.first, + list_As_Vs.second[0], + {{"writable_dVws",true}}); + this->exx_objs[settings_list.first].cv.dVws = LRI_CV_Tools::get_dCVws(ucell, dVs_temp); + dVs = dVs.empty() ? std::move(dVs_temp) : LRI_CV_Tools::add(dVs, dVs_temp); } } - if (write_cv && GlobalV::MY_RANK == 0) - { LRI_CV_Tools::write_Vs_abf(Vs, PARAM.globalv.global_out_dir + "Vs"); } - this->exx_lri.set_Vs(std::move(Vs), this->info.V_threshold); - if (cal_dCV) + if (write_cv && this->runtime.rank == 0) + { + LRI_CV_Tools::write_Vs_abf(Vs, this->runtime.output_dir + "Vs"); + if (this->use_rotated_n0_long_range) + { + LRI_CV_Tools::write_Vs_abf(Vs_long, this->runtime.output_dir + "Vs_long_n0"); + } + } + if (mpi_size > 1 + && this->coulomb_settings.find(Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) != this->coulomb_settings.end()) + { + MPI_Barrier(this->mpi_comm); + auto Vs_root = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vs, all_atoms, all_atoms); + MPI_Barrier(this->mpi_comm); + if (this->runtime.rank != 0) + { + Vs_root.clear(); + } + Vs = std::move(Vs_root); + if (this->use_rotated_n0_long_range) + { + MPI_Barrier(this->mpi_comm); + auto Vs_long_root = RI_2D_Comm::comm_map2_first(this->mpi_comm, Vs_long, all_atoms, all_atoms); + MPI_Barrier(this->mpi_comm); + if (this->runtime.rank != 0) + { + Vs_long_root.clear(); + } + Vs_long = std::move(Vs_long_root); + } + } + if (this->use_rotated_n0_long_range) + { + Vs_long = ExxLriDetail::extract_aux_tensor_map_matrices_prefix_by_type( + ucell, + this->abfs_long_prefix_size_per_type, + Vs_long); + } + const double V_threshold_short = this->info.V_threshold; + this->exx_lri.set_Vs(std::move(Vs), V_threshold_short, this->use_rotated_n0_long_range ? "short" : ""); + if (this->use_rotated_n0_long_range) + { + this->exx_lri.set_Vs(std::move(Vs_long), this->info.V_threshold_long, "long"); + } + if(this->runtime.cal_force || this->runtime.cal_stress) { std::array>>, Ndim> dVs_order = LRI_CV_Tools::change_order(std::move(dVs)); this->exx_lri.set_dVs(std::move(dVs_order), this->info.V_grad_threshold); - if(PARAM.inp.cal_stress) + if(this->runtime.cal_stress) { std::array>>,3>,3> dVRs = LRI_CV_Tools::cal_dMRs(ucell,dVs_order); this->exx_lri.set_dVRs(std::move(dVRs), this->info.V_grad_R_threshold); @@ -297,40 +1525,72 @@ void Exx_LRI::cal_exx_ions(const UnitCell& ucell, list_As_Cs = RI::Distribute_Equally::distribute_atoms_periods(this->mpi_comm, atoms, period_Cs, 2, false); std::map>> Cs; + std::map>> Cs_long; std::map, 3>>> dCs; - for(const auto &settings_list : this->coulomb_settings) + for (const auto& settings_list : this->coulomb_settings) { - if(settings_list.second.first) - { - std::pair>>, - std::map, 3>>>> - Cs_dCs = this->exx_objs[settings_list.first].cv.cal_Cs_dCs( - ucell, - list_As_Cs.first, list_As_Cs.second[0], - { {"cal_dC",cal_dCV}, - {"writable_Cws",true}, {"writable_dCws",true}, {"writable_Vws",false}, {"writable_dVws",false}}); - std::map>> &Cs_temp = std::get<0>(Cs_dCs); - this->exx_objs[settings_list.first].cv.Cws = LRI_CV_Tools::get_CVws(ucell,Cs_temp); + if (settings_list.second.first) + { + auto Cs_dCs = this->exx_objs[settings_list.first].cv.cal_Cs_dCs( + ucell, + list_As_Cs.first, + list_As_Cs.second[0], + {{"cal_dC",this->runtime.cal_force || this->runtime.cal_stress}, + {"writable_Cws",true}, + {"writable_dCws",true}, + {"writable_Vws",false}, + {"writable_dVws",false}}); + auto& Cs_temp = std::get<0>(Cs_dCs); + this->exx_objs[settings_list.first].cv.Cws = LRI_CV_Tools::get_CVws(ucell, Cs_temp); Cs = Cs.empty() ? Cs_temp : LRI_CV_Tools::add(Cs, Cs_temp); - if (cal_dCV) + if (this->runtime.cal_force || this->runtime.cal_stress) { - std::map, 3>>> &dCs_temp = std::get<1>(Cs_dCs); - this->exx_objs[settings_list.first].cv.dCws = LRI_CV_Tools::get_dCVws(ucell,dCs_temp); + auto& dCs_temp = std::get<1>(Cs_dCs); + this->exx_objs[settings_list.first].cv.dCws = LRI_CV_Tools::get_dCVws(ucell, dCs_temp); dCs = dCs.empty() ? dCs_temp : LRI_CV_Tools::add(dCs, dCs_temp); } } } - if (write_cv && GlobalV::MY_RANK == 0) - { LRI_CV_Tools::write_Cs_ao(Cs, PARAM.globalv.global_out_dir + "Cs"); } - this->exx_lri.set_Cs(std::move(Cs), this->info.C_threshold); + if (this->use_rotated_n0_long_range) + { + auto Cs_screened_by_full_mask = RI::RI_Tools::cal_period(Cs, period); + if (mpi_size > 1) + { + Cs_screened_by_full_mask = this->exx_lri.lri.parallel->comm_tensors_map2( + {RI::Label::ab::a, RI::Label::ab::b}, + std::move(Cs_screened_by_full_mask)); + } + Cs_screened_by_full_mask = ExxLriDetail::filter_tensor_map_by_max_abs_threshold( + Cs_screened_by_full_mask, + this->info.C_threshold); + Cs_long = ExxLriDetail::extract_aux_tensor_map_rows_prefix_by_type( + ucell, + this->abfs_long_prefix_size_per_type, + Cs_screened_by_full_mask); + } + if (write_cv && this->runtime.rank == 0) + { + LRI_CV_Tools::write_Cs_ao(Cs, this->runtime.output_dir + "Cs"); + } + if (this->use_rotated_n0_long_range) + { + this->exx_lri.lri.set_tensors_map2( + Cs_long, + {RI::Label::ab::a, RI::Label::ab::b}, + {{"flag_period", false}, {"flag_comm", false}, {"flag_filter", false}}, + "Cs_long"); + this->exx_lri.flag_finish.Cs = true; + } + this->exx_lri.set_Cs(std::move(Cs), this->info.C_threshold, this->use_rotated_n0_long_range ? "short" : ""); + ExxLriDetail::maybe_set_weighted_short_config(this->exx_lri, this->info, this->runtime.rank); - if (cal_dCV) + if(this->runtime.cal_force || this->runtime.cal_stress) { std::array>>, Ndim> dCs_order = LRI_CV_Tools::change_order(std::move(dCs)); this->exx_lri.set_dCs(std::move(dCs_order), this->info.C_grad_threshold); - if(PARAM.inp.cal_stress) + if(this->runtime.cal_stress) { std::array>>,3>,3> dCRs = LRI_CV_Tools::cal_dMRs(ucell,dCs_order); this->exx_lri.set_dCRs(std::move(dCRs), this->info.C_grad_R_threshold); @@ -443,9 +1703,9 @@ void Exx_LRI::cal_exx_ions(const UnitCell& ucell, } } - if (write_cv && GlobalV::MY_RANK == 0) + if (write_cv && this->runtime.rank == 0) { - LRI_CV_Tools::write_Vs_abf(Vs_cut, PARAM.globalv.global_out_dir + "Vs_cut"); + LRI_CV_Tools::write_Vs_abf(Vs_cut, this->runtime.output_dir + "Vs_cut"); } this->exx_lri.set_Vs(std::move(Vs_cut), this->info.V_threshold); @@ -627,9 +1887,9 @@ void Exx_LRI::cal_ewald_coulomb( Vs_full = Vs.empty() ? Vs_temp : LRI_CV_Tools::add(Vs_full, Vs_temp); } - if (write_cv && GlobalV::MY_RANK == 0) + if (write_cv && this->runtime.rank == 0) { - LRI_CV_Tools::write_Vs_abf(Vs_full, PARAM.globalv.global_out_dir + "Vs_full"); + LRI_CV_Tools::write_Vs_abf(Vs_full, this->runtime.output_dir + "Vs_full"); } // this->exx_lri.set_Vs(std::move(Vs_full), this->info.V_threshold); @@ -702,9 +1962,9 @@ void Exx_LRI::cal_cut_coulomb_cs( list_As_Vs.second[0], {{"writable_Vws", true}}); center2_obj_it->second.cv.Vws = LRI_CV_Tools::get_CVws(ucell, Vs_cut); - if (write_cv && GlobalV::MY_RANK == 0) + if (write_cv && this->runtime.rank == 0) { - LRI_CV_Tools::write_Vs_abf(Vs_cut, PARAM.globalv.global_out_dir + "Vs_cut"); + LRI_CV_Tools::write_Vs_abf(Vs_cut, this->runtime.output_dir + "Vs_cut"); } this->exx_lri.set_Vs(Vs_cut, this->info.V_threshold); @@ -724,9 +1984,9 @@ void Exx_LRI::cal_cut_coulomb_cs( {"writable_dVws", false}}); Cs = std::get<0>(Cs_dCs); center2_obj_it->second.cv.Cws = LRI_CV_Tools::get_CVws(ucell, Cs); - if (write_cv && GlobalV::MY_RANK == 0) + if (write_cv && this->runtime.rank == 0) { - LRI_CV_Tools::write_Cs_ao(Cs, PARAM.globalv.global_out_dir + "Cs"); + LRI_CV_Tools::write_Cs_ao(Cs, this->runtime.output_dir + "Cs"); } this->exx_lri.set_Cs(Cs, this->info.C_threshold); @@ -734,11 +1994,13 @@ void Exx_LRI::cal_cut_coulomb_cs( } template -void Exx_LRI::cal_ewald_coulomb( - std::map>>& Vs_full, - std::map>>& Cs, - const UnitCell& ucell, - const bool write_cv) +void Exx_LRI::cal_ewald_coulomb(std::map>>& Vs_full, + std::map>>& Cs, + const UnitCell& ucell, + const bool write_cv, + std::map>>* Vs_short_IJR, + std::map>>* Vs_long_IJR, + EwaldCoulombComponents* components) { ModuleBase::TITLE("Exx_LRI", "cal_ewald_coulomb"); ModuleBase::timer::start("Exx_LRI", "cal_ewald_coulomb"); @@ -760,35 +2022,108 @@ void Exx_LRI::cal_ewald_coulomb( const std::array period = {this->p_kv->nmp[0], this->p_kv->nmp[1], this->p_kv->nmp[2]}; this->exx_lri.set_parallel(this->mpi_comm, atoms_pos, latvec, period); + if (components != nullptr) + { + components->bare_periodic.clear(); + components->gaussian_real.clear(); + components->short_range.clear(); + components->long_range.clear(); + } - const std::array period_Vs + const std::array period_Vs_requested = LRI_CV_Tools::cal_latvec_range(1 + this->info.ccp_rmesh_times, ucell, orb_cutoff_); + // Keep the complete real-space image set together so the later atom-pair + // gather preserves V(q) Hermiticity. const std::pair, std::vector>>>> list_As_Vs - = RI::Distribute_Equally::distribute_atoms_periods(this->mpi_comm, atoms, period_Vs, 2, false); + = RI::Distribute_Equally::distribute_atoms_periods(this->mpi_comm, atoms, period_Vs_requested, 2, false); for (const auto& settings_list : this->coulomb_settings) { - std::map>> Vs_temp - = this->exx_objs[settings_list.first].cv.cal_Vs( + std::array period_Vs = period_Vs_requested; + auto list_As_Vs + = RI::Distribute_Equally::distribute_atoms_periods(this->mpi_comm, atoms, period_Vs, 2, false); + std::map>> Vs_temp; + if (settings_list.first == Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) + { + auto selected_range = ExxLriDetail::select_ewald_realspace_range( + this->info, + settings_list.second.second, + ucell, + this->abfs, + this->abfs_old_to_new_per_type, + atoms, + period_Vs_requested, + this->mpi_comm, + this->exx_objs[settings_list.first].cv, + this->exx_objs[settings_list.first].evq, + this->runtime); + period_Vs = selected_range.period; + list_As_Vs = std::move(selected_range.distribution); + Vs_temp = std::move(selected_range.bare_coulomb); + } + else + { + Vs_temp = this->exx_objs[settings_list.first].cv.cal_Vs( ucell, list_As_Vs.first, list_As_Vs.second[0], {{"writable_Vws", true}}); - this->exx_objs[settings_list.first].cv.Vws = LRI_CV_Tools::get_CVws(ucell, Vs_temp); + } + if (settings_list.first != Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) + { + this->exx_objs[settings_list.first].cv.Vws = LRI_CV_Tools::get_CVws(ucell, Vs_temp); + } if (settings_list.first == Conv_Coulomb_Pot_K::Coulomb_Method::Ewald) { - this->exx_objs[settings_list.first].evq.init_ions(ucell, period_Vs); std::map>> Vs_ewald; + std::map>> Vs_ewald_short; + std::map>> Vs_ewald_long; for (const auto& param_list : settings_list.second.second) { std::map>> Vs_ewald_temp; switch (param_list.first) { case Conv_Coulomb_Pot_K::Coulomb_Type::Fock: - { - double chi = this->exx_objs[settings_list.first].evq.get_singular_chi(ucell, param_list.second, 2.0); - Vs_ewald_temp = this->exx_objs[settings_list.first].evq.cal_Vs(ucell, chi, Vs_temp); + { + double chi = this->exx_objs[settings_list.first].evq.get_singular_chi(ucell, param_list.second, 2.0); + Vs_ewald_temp = this->exx_objs[settings_list.first].evq.cal_Vs(ucell, chi, Vs_temp); + if (Vs_short_IJR != nullptr || Vs_long_IJR != nullptr) + { + auto Vs_split = this->exx_objs[settings_list.first].evq.cal_Vs_split(ucell, chi, Vs_temp); + Vs_ewald_short = Vs_ewald_short.empty() ? Vs_split.first + : LRI_CV_Tools::add(Vs_ewald_short, Vs_split.first); + Vs_ewald_long = Vs_ewald_long.empty() ? Vs_split.second + : LRI_CV_Tools::add(Vs_ewald_long, Vs_split.second); + } + if (components != nullptr) + { + auto bare_periodic = this->exx_objs[settings_list.first].evq.cal_bare_periodic_Vs( + ucell, + list_As_Vs.first, + list_As_Vs.second[0], + Vs_temp); + auto short_range = this->exx_objs[settings_list.first].evq.cal_short_range_Vs( + ucell, + list_As_Vs.first, + list_As_Vs.second[0], + Vs_temp); + auto long_range + = this->exx_objs[settings_list.first].evq.cal_long_range_Vs_gauss(ucell, chi); + auto gaussian_real = LRI_CV_Tools::minus(bare_periodic, short_range); + components->bare_periodic = components->bare_periodic.empty() + ? std::move(bare_periodic) + : LRI_CV_Tools::add(components->bare_periodic, bare_periodic); + components->gaussian_real = components->gaussian_real.empty() + ? std::move(gaussian_real) + : LRI_CV_Tools::add(components->gaussian_real, gaussian_real); + components->short_range = components->short_range.empty() + ? std::move(short_range) + : LRI_CV_Tools::add(components->short_range, short_range); + components->long_range = components->long_range.empty() + ? std::move(long_range) + : LRI_CV_Tools::add(components->long_range, long_range); + } break; } default: @@ -798,15 +2133,23 @@ void Exx_LRI::cal_ewald_coulomb( } Vs_ewald = Vs_ewald.empty() ? Vs_ewald_temp : LRI_CV_Tools::add(Vs_ewald, Vs_ewald_temp); } + if (Vs_short_IJR != nullptr) + { + *Vs_short_IJR = Vs_short_IJR->empty() ? Vs_ewald_short : LRI_CV_Tools::add(*Vs_short_IJR, Vs_ewald_short); + } + if (Vs_long_IJR != nullptr) + { + *Vs_long_IJR = Vs_long_IJR->empty() ? Vs_ewald_long : LRI_CV_Tools::add(*Vs_long_IJR, Vs_ewald_long); + } Vs_temp = Vs_ewald; } Vs_full = Vs_full.empty() ? Vs_temp : LRI_CV_Tools::add(Vs_full, Vs_temp); } - if (write_cv && GlobalV::MY_RANK == 0) + if (write_cv && this->runtime.rank == 0) { - LRI_CV_Tools::write_Vs_abf(Vs_full, PARAM.globalv.global_out_dir + "Vs_full"); + LRI_CV_Tools::write_Vs_abf(Vs_full, this->runtime.output_dir + "Vs_full"); } Cs.clear(); @@ -825,13 +2168,62 @@ void Exx_LRI::cal_exx_elec(const std::vector, std::set>> judge = RI_2D_Comm::get_2D_judge(ucell,pv); if(p_symrot) - { this->exx_lri.set_symmetry(true, p_symrot->get_irred_sector()); } + { + this->exx_lri.set_symmetry(true, p_symrot->get_irred_sector()); + } else - { this->exx_lri.set_symmetry(false, {}); } + { + this->exx_lri.set_symmetry(false, {}); + } + + double full_cal_hs_time = 0.0; + double short_cal_hs_time = 0.0; + double long_cal_hs_time = 0.0; + + auto run_exx_channel = + [&](RI::Exx& exx_channel, + const std::map>>& D_in, + const int spin_index, + const std::string& ds_suffix, + const std::string& c_suffix, + const std::string& v_suffix, + double& cal_hs_time_acc) -> std::pair>>, double> + { + exx_channel.set_Ds(D_in, this->info.dm_threshold, ds_suffix); + const auto cal_hs_t0 = std::chrono::steady_clock::now(); + exx_channel.cal_Hs({c_suffix, v_suffix, ds_suffix}); + const auto cal_hs_t1 = std::chrono::steady_clock::now(); + cal_hs_time_acc += std::chrono::duration(cal_hs_t1 - cal_hs_t0).count(); + ExxLriDetail::maybe_print_weighted_short_stats( + exx_channel, this->info, ds_suffix, v_suffix, this->runtime.rank); + + if (!p_symrot) + { + return std::make_pair( + RI::Communicate_Tensors_Map_Judge::comm_map2_first( + this->mpi_comm, + std::move(exx_channel.Hs), + std::get<0>(judge[spin_index]), + std::get<1>(judge[spin_index])), + std::real(exx_channel.energy)); + } + + auto Hs_a2D = exx_channel.post_2D.set_tensors_map2(exx_channel.Hs); + Hs_a2D = p_symrot->restore_HR(ucell.symm, ucell.atoms, ucell.iat2it, 'H', Hs_a2D); + exx_channel.energy = exx_channel.post_2D.cal_energy(exx_channel.post_2D.saves["Ds_" + ds_suffix], + exx_channel.post_2D.set_tensors_map2(Hs_a2D)); + return std::make_pair( + RI::Communicate_Tensors_Map_Judge::comm_map2_first( + this->mpi_comm, + std::move(Hs_a2D), + std::get<0>(judge[spin_index]), + std::get<1>(judge[spin_index])), + std::real(exx_channel.energy)); + }; // (nspin=4) the spinor H(R) rotation mixes the 4 spin channels, so they cannot be rotated // independently in the per-spin loop below; hand off to the SOC implementation. - if (p_symrot && PARAM.inp.nspin == 4) + if (p_symrot && this->runtime.nspin == 4) { this->cal_exx_elec_soc(Ds, ucell, judge, p_symrot); this->exx_lri.set_symmetry(false, {}); @@ -839,39 +2231,53 @@ void Exx_LRI::cal_exx_elec(const std::vectorHexxs.resize(PARAM.inp.nspin); + this->Hexxs.resize(this->runtime.nspin); this->Eexx = 0; - for(int is=0; isruntime.nspin; ++is) { - const std::string suffix = ((PARAM.inp.cal_force || PARAM.inp.cal_stress) ? std::to_string(is) : ""); - - this->exx_lri.set_Ds(Ds[is], this->info.dm_threshold, suffix); - this->exx_lri.cal_Hs({ "","",suffix }); - - if (!p_symrot) + const std::string suffix = ((this->runtime.cal_force || this->runtime.cal_stress) ? std::to_string(is) : ""); + auto short_channel = run_exx_channel(this->exx_lri, + Ds[is], + is, + suffix, + this->use_rotated_n0_long_range ? "short" : "", + this->use_rotated_n0_long_range ? "short" : "", + this->use_rotated_n0_long_range ? short_cal_hs_time : full_cal_hs_time); + if (this->use_rotated_n0_long_range) { - this->Hexxs[is] = RI::Communicate_Tensors_Map_Judge::comm_map2_first( - this->mpi_comm, std::move(this->exx_lri.Hs), std::get<0>(judge[is]), std::get<1>(judge[is])); + auto long_channel = run_exx_channel(this->exx_lri, + Ds[is], + is, + suffix + "_lr", + "long", + "long", + long_cal_hs_time); + this->Hexxs[is] = LRI_CV_Tools::add(short_channel.first, long_channel.first); + this->Eexx += short_channel.second + long_channel.second; } else { - // reduce but not repeat - auto Hs_a2D = this->exx_lri.post_2D.set_tensors_map2(this->exx_lri.Hs); - // rotate locally without repeat - Hs_a2D = p_symrot->restore_HR(ucell.symm, ucell.atoms, ucell.iat2it, 'H', Hs_a2D); - // cal energy using full Hs without repeat - this->exx_lri.energy = this->exx_lri.post_2D.cal_energy( - this->exx_lri.post_2D.saves["Ds_" + suffix], - this->exx_lri.post_2D.set_tensors_map2(Hs_a2D)); - // get repeated full Hs for abacus - this->Hexxs[is] = RI::Communicate_Tensors_Map_Judge::comm_map2_first( - this->mpi_comm, std::move(Hs_a2D), std::get<0>(judge[is]), std::get<1>(judge[is])); + this->Hexxs[is] = std::move(short_channel.first); + this->Eexx += short_channel.second; } - this->Eexx += std::real(this->exx_lri.energy); post_process_Hexx(this->Hexxs[is]); } this->Eexx = post_process_Eexx(this->Eexx); this->exx_lri.set_symmetry(false, {}); + if (this->runtime.rank == 0) + { + if (this->use_rotated_n0_long_range) + { + const double total_cal_hs_time = short_cal_hs_time + long_cal_hs_time; + std::cout << "EXX cal_Hs timing summary: short = " << short_cal_hs_time + << " s, long = " << long_cal_hs_time + << " s, total = " << total_cal_hs_time << " s" << std::endl; + } + else + { + std::cout << "EXX cal_Hs timing summary: full = " << full_cal_hs_time << " s" << std::endl; + } + } ModuleBase::timer::end("Exx_LRI", "cal_exx_elec"); } @@ -883,37 +2289,64 @@ void Exx_LRI::cal_exx_elec_soc( const ModuleSymmetry::Symmetry_rotation* p_symrot) { ModuleBase::TITLE("Exx_LRI", "cal_exx_elec_soc"); - this->Hexxs.resize(PARAM.inp.nspin); // nspin==4 - this->Eexx = 0; + if (Ds.size() != 4 || p_symrot == nullptr) + { + throw std::invalid_argument("Exx_LRI::cal_exx_elec_soc requires four spinor channels and symmetry metadata."); + } - // pass 1: compute the irreducible-sector Hs of all 4 spin channels. - // distinct suffix per channel keeps all 4 "Ds_*" saves alive for the energy in pass 3. - std::array>>, 4> Hs_irr; - std::array suffix; - for (int is = 0; is < 4; ++is) + this->Hexxs.assign(4, {}); + this->Eexx = 0.0; + + struct ChannelSpec + { + std::string cv_suffix; + std::string ds_tail; + }; + std::vector channels; + channels.push_back({this->use_rotated_n0_long_range ? "short" : "", ""}); + if (this->use_rotated_n0_long_range) { - suffix[is] = std::to_string(is); - this->exx_lri.set_Ds(Ds[is], this->info.dm_threshold, suffix[is]); - this->exx_lri.cal_Hs({ "","",suffix[is] }); - Hs_irr[is] = this->exx_lri.post_2D.set_tensors_map2(this->exx_lri.Hs); + channels.push_back({"long", "_lr"}); } - // pass 2: spinor-coupled rotation of the 4 channels from the irreducible sector to the full BZ - std::array>>, 4> Hs_full = - p_symrot->restore_HR_nspin4(ucell.symm, ucell.atoms, ucell.iat2it, 'H', Hs_irr); + for (const ChannelSpec& channel : channels) + { + std::array>>, 4> Hs_irreducible; + std::array ds_suffix; + for (int is = 0; is < 4; ++is) + { + ds_suffix[is] = std::to_string(is) + channel.ds_tail; + this->exx_lri.set_Ds(Ds[is], this->info.dm_threshold, ds_suffix[is]); + this->exx_lri.cal_Hs({channel.cv_suffix, channel.cv_suffix, ds_suffix[is]}); + ExxLriDetail::maybe_print_weighted_short_stats( + this->exx_lri, this->info, ds_suffix[is], channel.cv_suffix, this->runtime.rank); + Hs_irreducible[is] = this->exx_lri.post_2D.set_tensors_map2(this->exx_lri.Hs); + } + + auto Hs_full = p_symrot->restore_HR_nspin4( + ucell.symm, ucell.atoms, ucell.iat2it, 'H', Hs_irreducible); + for (int is = 0; is < 4; ++is) + { + this->exx_lri.energy = this->exx_lri.post_2D.cal_energy( + this->exx_lri.post_2D.saves["Ds_" + ds_suffix[is]], + this->exx_lri.post_2D.set_tensors_map2(Hs_full[is])); + auto Hs_channel = RI::Communicate_Tensors_Map_Judge::comm_map2_first( + this->mpi_comm, + std::move(Hs_full[is]), + std::get<0>(judge[is]), + std::get<1>(judge[is])); + this->Hexxs[is] = this->Hexxs[is].empty() + ? std::move(Hs_channel) + : LRI_CV_Tools::add(this->Hexxs[is], Hs_channel); + this->Eexx += std::real(this->exx_lri.energy); + } + } - // pass 3: per-channel energy (full Hs, no repeat), then gather the repeated full Hs for abacus - for (int is = 0; is < 4; ++is) + for (auto& Hexx : this->Hexxs) { - this->exx_lri.energy = this->exx_lri.post_2D.cal_energy( - this->exx_lri.post_2D.saves["Ds_" + suffix[is]], - this->exx_lri.post_2D.set_tensors_map2(Hs_full[is])); - this->Hexxs[is] = RI::Communicate_Tensors_Map_Judge::comm_map2_first( - this->mpi_comm, std::move(Hs_full[is]), std::get<0>(judge[is]), std::get<1>(judge[is])); - this->Eexx += std::real(this->exx_lri.energy); - post_process_Hexx(this->Hexxs[is]); + this->post_process_Hexx(Hexx); } - this->Eexx = post_process_Eexx(this->Eexx); + this->Eexx = this->post_process_Eexx(this->Eexx); } template @@ -931,7 +2364,7 @@ template double Exx_LRI::post_process_Eexx(const double& Eexx_in) const { ModuleBase::TITLE("Exx_LRI","post_process_Eexx"); - const double SPIN_multiple = std::map{ {1,2}, {2,1}, {4,1} }.at(PARAM.inp.nspin); // why? + const double SPIN_multiple = std::map{ {1,2}, {2,1}, {4,1} }.at(this->runtime.nspin); // why? const double frac = -SPIN_multiple; return frac * Eexx_in; } @@ -960,14 +2393,21 @@ void Exx_LRI::cal_exx_force(const int& nat) ModuleBase::timer::start("Exx_LRI", "cal_exx_force"); this->force_exx.create(nat, Ndim); - const int& nspin = PARAM.inp.nspin; + if (this->runtime.realtime_tddft && this->use_rotated_n0_long_range) + { + this->force_exx.zero_out(); + ExxLriDetail::print_rt_tddft_ewald_force_stress_warning_once(this->runtime); + ModuleBase::timer::end("Exx_LRI", "cal_exx_force"); + return; + } + const int& nspin = this->runtime.nspin; for(int is=0; isexx_lri.cal_force({"","",std::to_string(is),"",""}); for(std::size_t idim=0; idimexx_lri.force[idim]) { this->force_exx(force_item.first, idim) += std::real(force_item.second); - } } + } } } // SPIN_multiple cancels the one in `split_m2D_ktoR`, which are 0.5*0.5 at nspin=1. // but only u-u and d-d pairs of Ds has contribution, so here's 2 instead of 4. @@ -986,24 +2426,68 @@ void Exx_LRI::cal_exx_stress(const double& omega, const double& lat0) ModuleBase::timer::start("Exx_LRI", "cal_exx_stress"); this->stress_exx.create(Ndim, Ndim); - for(int is=0; isruntime.realtime_tddft && this->use_rotated_n0_long_range) + { + this->stress_exx.zero_out(); + ExxLriDetail::print_rt_tddft_ewald_force_stress_warning_once(this->runtime); + ModuleBase::timer::end("Exx_LRI", "cal_exx_stress"); + return; + } + for(int is=0; isruntime.nspin; ++is) { this->exx_lri.cal_stress({"","",std::to_string(is),"",""}); for(std::size_t idim0=0; idim0stress_exx(idim0,idim1) += std::real(this->exx_lri.stress(idim0,idim1)); - } } + } } } - const double SPIN_multiple = std::map{{1,2}, {2,1}, {4,1}}.at(PARAM.inp.nspin); // why? - const double frac = 2 * SPIN_multiple / omega * lat0; // why? + const double SPIN_multiple = std::map{{1,2}, {2,1}, {4,1}}.at(this->runtime.nspin); // why? + const double frac = 2 * SPIN_multiple / omega * lat0; // why? this->stress_exx *= frac; ModuleBase::timer::end("Exx_LRI", "cal_exx_stress"); } +template +void Exx_LRI::cal_exx_dHs( + const std::vector>>>& Ds, + const UnitCell& ucell, + const Parallel_Orbitals& pv) +{ + ModuleBase::TITLE("Exx_LRI", "cal_exx_dHs"); + ModuleBase::timer::start("Exx_LRI", "cal_exx_dHs"); +#ifdef __EXX_DEV + const auto judge = RI_2D_Comm::get_2D_judge(ucell, pv); + for (int is = 0; is < this->runtime.nspin; ++is) + { + this->exx_lri.set_Ds(Ds[is], this->info.dm_threshold, std::to_string(is)); + this->exx_lri.cal_dHs({"", "", std::to_string(is), "", ""}); + for (int ipos = 0; ipos < 3; ++ipos) + { + if (this->dHexxs[ipos].size() != static_cast(ucell.nat)) + this->dHexxs[ipos].resize(ucell.nat); + for (int iat = 0; iat < ucell.nat; ++iat) + { + if (this->dHexxs[ipos][iat].size() != static_cast(this->runtime.nspin)) + this->dHexxs[ipos][iat].resize(this->runtime.nspin); + auto dH = this->exx_lri.dHs[ipos][0][iat]; + dH = dH + this->exx_lri.dHs_HF[ipos][iat]; + this->dHexxs[ipos][iat][is] = RI::Communicate_Tensors_Map_Judge::comm_map2_first( + this->mpi_comm, std::move(dH), std::get<0>(judge[is]), std::get<1>(judge[is])); + this->post_process_Hexx(this->dHexxs[ipos][iat][is]); + } + } + } +#else + (void)Ds; + (void)ucell; + (void)pv; + ModuleBase::WARNING("Exx_LRI", "cal_exx_dHs requires LibRI with EXX_DEV; leaving dHexxs empty."); +#endif + ModuleBase::timer::end("Exx_LRI", "cal_exx_dHs"); +} -/* template std::vector> Exx_LRI::get_abfs_nchis() const { @@ -1017,6 +2501,5 @@ std::vector> Exx_LRI::get_abfs_nchis() const } return abfs_nchis; } -*/ #endif diff --git a/source/source_lcao/module_ri/exx_lri_interface.h b/source/source_lcao/module_ri/exx_lri_interface.h index 8b5bc61cf64..b8fb7c85751 100644 --- a/source/source_lcao/module_ri/exx_lri_interface.h +++ b/source/source_lcao/module_ri/exx_lri_interface.h @@ -47,7 +47,7 @@ class Exx_LRI_Interface /// @brief Constructor for Exx_LRI_Interface Exx_LRI_Interface(const Exx_Info_RI& info_ri, const Exx_Info_Global& info_global) { - this->exx_ptr = std::make_shared>(info_ri); + this->exx_ptr = std::make_shared>(info_ri, make_exx_lri_runtime()); this->info_global = info_global; this->hybrid_step_ = info_global.hybrid_step; } @@ -140,7 +140,7 @@ class Exx_LRI_Interface /// >0: not the first outer loop. contributeHk will do enerything normally. int two_level_step = 0; double etot_last_outer_loop = 0.0; - module_dm::DensityMatrix* dm_last_step; + module_dm::DensityMatrix* dm_last_step = nullptr; size_t hybrid_step() const { return hybrid_step_; } void set_hybrid_step(size_t s) { hybrid_step_ = s; } @@ -148,9 +148,7 @@ class Exx_LRI_Interface std::shared_ptr> exx_ptr; private: - Mix_DMk_2D mix_DMk_2D; - Exx_Info_Global info_global; size_t hybrid_step_ = 1; // non-owning ptr to Charge_Mixing captured in exx_beforescf, used to refresh the diff --git a/source/source_lcao/module_ri/exx_lri_interface.hpp b/source/source_lcao/module_ri/exx_lri_interface.hpp index b08514faad0..136489e9318 100644 --- a/source/source_lcao/module_ri/exx_lri_interface.hpp +++ b/source/source_lcao/module_ri/exx_lri_interface.hpp @@ -132,7 +132,7 @@ void Exx_LRI_Interface::exx_beforescf(const int istep, || (istep > 0) || (PARAM.inp.init_wfc == "file")) { - XC_Functional::set_xc_type(ucell.atoms[0].ncpp.xc_func); + XC_Functional::set_xc_type(XC_Functional::resolve_runtime_xc_type(ucell)); } else { @@ -209,7 +209,7 @@ void Exx_LRI_Interface::exx_eachiterinit(const int istep, { const void* cur_mixing = static_cast(this->p_chgmix_->get_mixing()); if (this->last_borrowed_mixing_ != nullptr && cur_mixing != this->last_borrowed_mixing_) - { flag_restart = true; } + { flag_restart = true; } this->last_borrowed_mixing_ = cur_mixing; this->mix_DMk_2D.set_mixing(this->p_chgmix_->get_mixing()); } @@ -234,10 +234,12 @@ void Exx_LRI_Interface::exx_eachiterinit(const int istep, { this->cal_exx_elec(Ds, ucell, pv); } }; - if(istep > 0 && flag_restart) - { cal(*this->dm_last_step); } - else - { cal(dm); } + const module_dm::DensityMatrix* dm_for_exx = &dm; + if (istep > 0 && flag_restart && this->dm_last_step != nullptr) + { + dm_for_exx = this->dm_last_step; + } + cal(*dm_for_exx); } } } @@ -399,7 +401,7 @@ bool Exx_LRI_Interface::exx_after_converge( else { // update exx and redo scf - XC_Functional::set_xc_type(ucell.atoms[0].ncpp.xc_func); + XC_Functional::set_xc_type(XC_Functional::resolve_runtime_xc_type(ucell)); iter = 0; std::cout << " Entering 2nd SCF, where EXX is updated" << std::endl; this->two_level_step++; @@ -424,7 +426,7 @@ bool Exx_LRI_Interface::exx_after_converge( this->etot_last_outer_loop = etot; // update exx and redo scf if (this->two_level_step == 0) - { XC_Functional::set_xc_type(ucell.atoms[0].ncpp.xc_func); } + { XC_Functional::set_xc_type(XC_Functional::resolve_runtime_xc_type(ucell)); } std::cout << " Updating EXX " << std::flush; timeval t_start; gettimeofday(&t_start, nullptr); diff --git a/source/source_lcao/module_ri/exx_rotate_abfs.h b/source/source_lcao/module_ri/exx_rotate_abfs.h index 23517fafe1f..eb4b2f50c79 100644 --- a/source/source_lcao/module_ri/exx_rotate_abfs.h +++ b/source/source_lcao/module_ri/exx_rotate_abfs.h @@ -3,12 +3,9 @@ #define EXX_ROTATE_ABFS_H #include "lri_cv.h" -#include "source_hamilt/module_xc/exx_info_ri.h" -// #include "module_xc/exx_info.h" -// #include "module_basis/module_ao/orb_atomic_lm.h" -#include "exx_lri.h" -// #include "module_ri/exx_lri.h" -// #include +#include "source_hamilt/module_xc/exx_info.h" +#include "source_base/element_basis_index.h" +#include "source_base/matrix.h" #include #include #include @@ -28,7 +25,7 @@ class Moment_abfs using TAq = std::pair; public: - Moment_abfs(Exx_Info_RI& info_in) : info(info_in) {}; + Moment_abfs(const Exx_Info_RI& info_in) : info(info_in) {}; ~Moment_abfs() {}; void cal_VR( const UnitCell& ucell, @@ -37,7 +34,14 @@ class Moment_abfs const std::vector& orb_cutoff, const double Rc, LRI_CV& cv, - std::map>>& Vs_cut); + std::map>>& Vs_cut, + const ModuleBase::Element_Basis_Index::IndexPermutation& orb_old_to_new = {}, + const bool allow_overwrite = false, + const bool apply_cutoff = true, + const bool write_vws = true, + const bool allow_insert = true, + const double value_scale = 1.0, + const bool ignore_upper_coulomb_cutoff = false); void discard0_VR( const UnitCell& ucell, const std::vector>>& orb_in, @@ -45,7 +49,8 @@ class Moment_abfs const std::vector& orb_cutoff, const double Rc, LRI_CV& cv, - std::map>>& Vs_cut); + std::map>>& Vs_cut, + const ModuleBase::Element_Basis_Index::IndexPermutation& orb_old_to_new = {}); void cal_multipole(const std::vector>>& orb_in); void rotate_abfs(std::vector>>& orb_in); double sum_triple_Y_YLM_real(int l1, @@ -68,9 +73,7 @@ class Moment_abfs private: // std::map>> VR; - // WARNING: reference to Exx_Info_RI. - // Must not outlive the Exx_Info_RI passed to the constructor. - Exx_Info_RI& info; + const Exx_Info_RI& info; }; #include "exx_rotate_abfs.hpp" diff --git a/source/source_lcao/module_ri/exx_rotate_abfs.hpp b/source/source_lcao/module_ri/exx_rotate_abfs.hpp index cac52ac72d9..826b36d9e5c 100644 --- a/source/source_lcao/module_ri/exx_rotate_abfs.hpp +++ b/source/source_lcao/module_ri/exx_rotate_abfs.hpp @@ -6,8 +6,19 @@ #include "exx_rotate_abfs.h" #include // For std::erfc function used in smooth truncation +#include #include +template +inline int real_m_to_mm_index(const int m) +{ + if (m == 0) + { + return 0; + } + return (m > 0) ? (2 * m - 1) : (-2 * m); +} + template void Moment_abfs::cal_multipole(const std::vector>>& orb_in) { @@ -42,61 +53,8 @@ void Moment_abfs::rotate_abfs(std::vectormultipole[T][L][N2] * this->multipole[T][L][N2]; - } - double norm = std::sqrt(square); - // change abfs - if (N == 0) - { - for (int N2 = 0; N2 != orb_in[T][L].size(); ++N2) - orb_lm_mod += orb_in[T][L][N2] * this->multipole[T][L][N2] / norm; - } - else - { - for (int N2 = 0; N2 != orb_in[T][L].size(); ++N2) - { - if (N2 == N) - { - orb_lm_mod += (1.0 - this->multipole[T][L][N] * this->multipole[T][L][N2] / square) - * orb_in[T][L][N2]; - } - else - { - orb_lm_mod - += (-this->multipole[T][L][N] * this->multipole[T][L][N2] / square) * orb_in[T][L][N2]; - } - } - } - orb_lm_old = orb_lm_mod; - // change moment - if (N == 0) - { - this->multipole[T][L][N] = norm; - std::cout << "Atom type " << T << ", L " << L << ", N " << N - << ", multipole after rotation: " << this->multipole[T][L][N] << std::endl; - } - else - { - this->multipole[T][L][N] = 0.0; - } - } - } - } - ModuleBase::timer::end("Rotate_abfs", "rotate_abfs"); + throw std::logic_error("Moment_abfs::rotate_abfs is deprecated. Use ExxLriDetail::rotate_abfs_by_multipole instead."); } template @@ -160,13 +118,13 @@ double Moment_abfs::sum_triple_Y_YLM_real(int l1, double sum = 0.0; const double tiny2 = 1e-10; const int L = l1 + l2; - const int idx1 = MGT.get_lm_index(l1, m1 + l1); - const int idx2 = MGT.get_lm_index(l2, m2 + l2); + const int idx1 = MGT.get_lm_index(l1, real_m_to_mm_index(m1)); + const int idx2 = MGT.get_lm_index(l2, real_m_to_mm_index(m2)); // cyl(m1,m2) = sum_M C(l1,l2,L,m1,m2,M) Y_LM(R) for (int M = -L; M <= L; ++M) { - const int idxL = MGT.get_lm_index(L, M + L); + const int idxL = MGT.get_lm_index(L, real_m_to_mm_index(M)); const double C = MGT.Gaunt_Coefficients(idx1, idx2, idxL); const double ylm_solid = rly.at(idxL); const double ylm_real = (distance > tiny2) ? ylm_solid / pow(distance, l1 + l2) : ylm_solid; @@ -188,7 +146,14 @@ void Moment_abfs::cal_VR( const std::vector& orb_cutoff, const double Rc, LRI_CV& cv, - std::map>>& Vs_cut) + std::map>>& Vs_cut, + const ModuleBase::Element_Basis_Index::IndexPermutation& orb_old_to_new, + const bool allow_overwrite, + const bool apply_cutoff, + const bool write_vws, + const bool allow_insert, + const double value_scale, + const bool ignore_upper_coulomb_cutoff) { ModuleBase::TITLE("Rotate_abfs", "cal_VR"); ModuleBase::timer::start("Rotate_abfs", "cal_VR"); @@ -196,7 +161,8 @@ void Moment_abfs::cal_VR( const double tiny1 = 1e-12; const ModuleBase::Element_Basis_Index::Range range = ModuleBase::Element_Basis_Index::construct_range(orb_in); // index: T: type, L: angular momentum, N: radial index, M: magnetic moment - const ModuleBase::Element_Basis_Index::IndexLNM index = ModuleBase::Element_Basis_Index::construct_index(range); + const ModuleBase::Element_Basis_Index::IndexLNM index + = ModuleBase::Element_Basis_Index::construct_index(range, orb_old_to_new); ORB_gaunt_table MGT0; int Lmax = 0; @@ -238,8 +204,8 @@ void Moment_abfs::cal_VR( // Rcut_lcao: bohr, ucell.lat0 = 1.8897 transform angstrom to bohr double Rcut_lcao = orb_cutoff[T1] + orb_cutoff[T2]; // Rcut_coul: bohr - double Rcut_coul = std::min(cv.cal_V_Rcut(T1, T2), cv.cal_V_Rcut(T2, T2)); - if (distance < Rcut_lcao || distance >= Rcut_coul) + double Rcut_coul = std::min(cv.get_V_Rcut(T1, T2), cv.get_V_Rcut(T2, T1)); + if (distance < Rcut_lcao || (!ignore_upper_coulomb_cutoff && distance >= Rcut_coul)) continue; const auto JR = std::make_pair(iat1, R); auto tmp_tensor = RI::Tensor({sizeA, sizeB}); @@ -257,10 +223,10 @@ void Moment_abfs::cal_VR( const double prefactor1 = std::pow(distance, L1 + L2 + 1); for (int M1 = -L1; M1 <= L1; ++M1) { - const int index_M1 = M1 + L1; + const int index_M1 = real_m_to_mm_index(M1); for (int M2 = -L2; M2 <= L2; ++M2) { - const int index_M2 = M2 + L2; + const int index_M2 = real_m_to_mm_index(M2); const double prefactor = std::pow(-1, L2) * std::pow(ModuleBase::TWO_PI, 1.5) / prefactor1; const double clmlm = this->cal_cl1l2(L1, L2); // For real spherical harmonics, m order is: 0, 0, 1, -1, 0, 1, -1, 2, -2, ... @@ -269,12 +235,9 @@ void Moment_abfs::cal_VR( // Determine N1 and N2 loop ranges based on rotate_abfs // When rotate_abfs=true: only N=0 has non-zero moment, calculate only N1=0 and N2=0 // When rotate_abfs=false: all moments are non-zero, calculate all N1, N2 - const int N1_max = this->info.rotate_abfs ? 1 : orb_in[T1][L1].size(); - const int N2_max = this->info.rotate_abfs ? 1 : orb_in[T2][L2].size(); - - for (int N1 = 0; N1 != N1_max; ++N1) + for (int N1 = 0; N1 != orb_in[T1][L1].size(); ++N1) { - for (int N2 = 0; N2 != N2_max; ++N2) + for (int N2 = 0; N2 != orb_in[T2][L2].size(); ++N2) { double mom1 = this->multipole[T1][L1][N1]; const double mom2 = this->multipole[T2][L2][N2]; @@ -313,85 +276,59 @@ void Moment_abfs::cal_VR( // width = 1.03: r = Rc*1.1 gives ~0.5% contribution // width = 1.02: r = Rc*1.05 gives ~0.08% contribution (nearly hard) // width = 1.01: essentially hard cutoff at Rc - const double width = 0.9; // Width factor - tune this for high states! - double cutoff_factor = 1.0; - // Uncomment to test hard cutoff (for debugging) - // cutoff_factor = (distance < Rc) ? 1.0 : 0.0; - // tmp_tensor(iA, iB) = value * cutoff_factor; - // continue; // Skip the erfc truncation below - - if (distance > 0.0 && width > 1.0) + if (apply_cutoff) { - // Log-space erfc truncation (FHI-aims implementation) - cutoff_factor = 0.5 * std::erfc(std::log(distance / Rc) / std::log(width)); + const double width = 0.9; // Width factor - tune this for high states! + if (distance > 0.0 && width > 1.0) + { + // Log-space erfc truncation (FHI-aims implementation) + cutoff_factor = 0.5 * std::erfc(std::log(distance / Rc) / std::log(width)); - // Debug output for high states (check if truncation is working) - static int debug_count = 0; - if (debug_count < 10 && distance > Rc * 0.9 && distance < Rc * 1.2) + } + else if (distance <= 0.0) { - std::cout << "DEBUG: distance=" << distance << " Rc=" << Rc - << " cutoff_factor=" << cutoff_factor << " value=" << value - << std::endl; - debug_count++; + // At r = 0, no truncation + cutoff_factor = 1.0; + } + else + { + // width <= 1.0: no smooth truncation, use hard cutoff + cutoff_factor = (distance < Rc) ? 1.0 : 0.0; } - } - else if (distance <= 0.0) - { - // At r = 0, no truncation - cutoff_factor = 1.0; - } - else - { - // width <= 1.0: no smooth truncation, use hard cutoff - cutoff_factor = (distance < Rc) ? 1.0 : 0.0; - // const double gamma = 5.0 / Rc; - // double x = gamma * distance; - // cutoff_factor = std::erfc(x); - // cutoff_factor = (cutoff_factor > 0) ? cutoff_factor : 0.0; } - tmp_tensor(iA, iB) = value * cutoff_factor; + tmp_tensor(iA, iB) = value * (value_scale * cutoff_factor); } } - // if (iat0 == 0 && iat0 == 0 && R[0] == 2 && R[1] == 2 && R[2] == 1 && iat0 == 0 && - // iat1 == 0) - // { - // std::cout << "iA: " << iA << ", iB: " << iB << ", L2: " << L2 << ", M2: " << M2 - // << ", V= " << value << ", prefactor:" << prefactor << ",mom1: " << mom1 - // << ", mom2: " << mom2 << ", clmlm: " << clmlm << ", ylm: " << ylm - // << std::endl; - // } - - // if (R[0] == 2 && R[1] == 2 && R[2] == 1 && iat0 == 0 && iat1 == 0) - // { - // out_pure_ri_tensor("Vs_tensor.txt", tmp_tensor, 0.0); - // } - // debug - // std::cout << "T1: " << T1 << ", L1: " << L1 << ", T2: " << T2 << ", L2: " << L2 - // << ", delta_R: " << delta_R[0] << ", " << delta_R[1] << ", " << delta_R[2] - // << ", distance: " << distance << ", outside V= " << value - // << ", prefactor:" << prefactor << ",mom1: " << mom1 << ", mom2: " << mom2 - // << ", clmlm: " << clmlm << ", ylm_real: " << ylm_real << std::endl; } } } } - Vws[T1][T2][delta_R] = tmp_tensor; + if (write_vws) + { + Vws[T1][T2][delta_R] = tmp_tensor; + } // I must contain all atoms in unit cell - auto& target_inner = Vs_cut.at(iat0); + auto& target_inner = Vs_cut[iat0]; if (target_inner.find(JR) == target_inner.end()) { - target_inner.emplace(JR, tmp_tensor); + if (allow_insert) + { + target_inner.emplace(JR, tmp_tensor); + } } - // otherwise, warning + // otherwise, overwrite when the exact block already exists else { + if (!allow_overwrite) + { + const auto J = JR.first; + const auto R = JR.second; + std::cout << "J=" << J << ", R= " << R[0] << ", " << R[1] << ", " << R[2] << std::endl; + ModuleBase::WARNING_QUIT("merge_VR", "JR already exists in Vs_cut[I], which is unexpected."); + } target_inner[JR] = tmp_tensor; - const auto J = JR.first; - const auto R = JR.second; - // std::cout << "J=" << J << ", R= " << R[0] << ", " << R[1] << ", " << R[2] << std::endl; - ModuleBase::WARNING_QUIT("merge_VR", "JR already exists in Vs_cut[I], which is unexpected."); } } } @@ -406,14 +343,16 @@ void Moment_abfs::discard0_VR( const std::vector& orb_cutoff, const double Rc, LRI_CV& cv, - std::map>>& Vs_cut) + std::map>>& Vs_cut, + const ModuleBase::Element_Basis_Index::IndexPermutation& orb_old_to_new) { ModuleBase::TITLE("Rotate_abfs", "discard0_VR"); ModuleBase::timer::start("Rotate_abfs", "discard0_VR"); const double tiny1 = 1e-12; const ModuleBase::Element_Basis_Index::Range range = ModuleBase::Element_Basis_Index::construct_range(orb_in); - const ModuleBase::Element_Basis_Index::IndexLNM index = ModuleBase::Element_Basis_Index::construct_index(range); + const ModuleBase::Element_Basis_Index::IndexLNM index + = ModuleBase::Element_Basis_Index::construct_index(range, orb_old_to_new); const auto list_A0 = list_r.first; const auto list_A1 = list_r.second[0]; @@ -442,7 +381,7 @@ void Moment_abfs::discard0_VR( const double distance = (distance_true >= tiny1) ? distance_true : distance_true + tiny1; double Rcut_lcao = orb_cutoff[T1] + orb_cutoff[T2]; - double Rcut_coul = std::min(cv.cal_V_Rcut(T1, T2), cv.cal_V_Rcut(T2, T2)); + double Rcut_coul = std::min(cv.get_V_Rcut(T1, T2), cv.get_V_Rcut(T2, T1)); // Skip if not in moment method range if (distance < Rcut_lcao || distance >= Rcut_coul) @@ -463,10 +402,10 @@ void Moment_abfs::discard0_VR( { for (int M1 = -L1; M1 <= L1; ++M1) { - const int index_M1 = M1 + L1; + const int index_M1 = real_m_to_mm_index(M1); for (int M2 = -L2; M2 <= L2; ++M2) { - const int index_M2 = M2 + L2; + const int index_M2 = real_m_to_mm_index(M2); // Set all N1 != 0 or N2 != 0 elements to zero for (int N1 = 1; N1 != orb_in[T1][L1].size(); ++N1) @@ -519,7 +458,7 @@ void Moment_abfs::discard0_VR( const double distance = (distance_true >= tiny1) ? distance_true : distance_true + tiny1; double Rcut_lcao = orb_cutoff[T1] + orb_cutoff[T2]; - double Rcut_coul = std::min(cv.cal_V_Rcut(T1, T2), cv.cal_V_Rcut(T2, T2)); + double Rcut_coul = std::min(cv.get_V_Rcut(T1, T2), cv.get_V_Rcut(T2, T1)); // Skip if not in moment method range if (distance < Rcut_lcao || distance >= Rcut_coul) @@ -534,10 +473,10 @@ void Moment_abfs::discard0_VR( { for (int M1 = -L1; M1 <= L1; ++M1) { - const int index_M1 = M1 + L1; + const int index_M1 = real_m_to_mm_index(M1); for (int M2 = -L2; M2 <= L2; ++M2) { - const int index_M2 = M2 + L2; + const int index_M2 = real_m_to_mm_index(M2); // Set all N1 != 0 or N2 != 0 elements to zero for (int N1 = 1; N1 != orb_in[T1][L1].size(); ++N1) diff --git a/source/source_lcao/module_ri/gaussian_abfs.cpp b/source/source_lcao/module_ri/gaussian_abfs.cpp index 262b74d7e56..45a0c06b21f 100644 --- a/source/source_lcao/module_ri/gaussian_abfs.cpp +++ b/source/source_lcao/module_ri/gaussian_abfs.cpp @@ -7,8 +7,11 @@ #define GAUSSIAN_ABFS_CPP #include "gaussian_abfs.h" +#include "gaussian_abfs_2d_integrals.h" #include +#include +#include // #include #include "lri_cv_tools.h" @@ -37,7 +40,10 @@ void Gaussian_Abfs::init(const UnitCell& ucell, this->tpiba = ucell.tpiba; this->lat0 = ucell.lat0; this->omega = ucell.omega; - const double eta = 35; + const ModuleBase::Vector3 a1_bohr = ucell.a1 * this->lat0; + const ModuleBase::Vector3 a2_bohr = ucell.a2 * this->lat0; + this->area_parallel_bohr2 = (a1_bohr ^ a2_bohr).norm(); + this->lz_bohr = this->omega / this->area_parallel_bohr2; std::vector> Gvec; Gvec.resize(3); Gvec[0].x = G.e11; @@ -51,8 +57,10 @@ void Gaussian_Abfs::init(const UnitCell& ucell, Gvec[2].x = G.e31; Gvec[2].y = G.e32; Gvec[2].z = G.e33; + this->gvecs_direct = Gvec; this->n_cells.resize(nks0); + this->n_supercells_2d.resize(nks0); this->qGvecs.resize(nks0); this->check_gamma.resize(nks0); this->ylm.resize(nks0); @@ -62,7 +70,7 @@ void Gaussian_Abfs::init(const UnitCell& ucell, for (size_t ik = 0; ik != nks0; ++ik) { ModuleBase::Vector3 qvec = this->kvec_c[ik]; - const double Gmax = std::sqrt(eta * this->lambda) + qvec.norm() * this->tpiba; + const double Gmax = EwaldGaussianCutoff::reciprocal_radius(this->lambda, Lmax) + qvec.norm() * this->tpiba; std::vector n_supercells = get_n_supercells(this->lat0, G, Gmax); int total_cells = std::accumulate(n_supercells.begin(), n_supercells.end(), 1, [](int a, int b) { return a * (2 * b + 1); @@ -90,6 +98,7 @@ void Gaussian_Abfs::init(const UnitCell& ucell, #pragma omp critical(Gaussian_Abfs_init) { this->n_cells[ik] = total_cells; + this->n_supercells_2d[ik] = {n_supercells[0], n_supercells[1]}; this->qGvecs[ik] = qGvec_ik; this->check_gamma[ik] = check_gamma_ik; this->ylm[ik] = ylm_ik; @@ -132,6 +141,22 @@ auto Gaussian_Abfs::get_Vq(const int& lp_max, return res; } +auto Gaussian_Abfs::get_Vq_2d(const int& lp_max, + const int& lq_max, + const size_t& ik, + const double& chi2d, + const ModuleBase::Vector3& tau, + const ModuleBase::realArray& gaunt) -> RI::Tensor> +{ + ModuleBase::TITLE("Gaussian_Abfs", "get_Vq_2d"); + ModuleBase::timer::start("Gaussian_Abfs", "get_Vq_2d"); + + auto res = this->DPcal_Vq_2d(lp_max, lq_max, ik, chi2d, tau, gaunt); + + ModuleBase::timer::end("Gaussian_Abfs", "get_Vq_2d"); + return res; +} + auto Gaussian_Abfs::get_dVq(const int& lp_max, const int& lq_max, // Maximum L for which to calculate interaction. const size_t& ik, @@ -255,12 +280,81 @@ auto Gaussian_Abfs::DPcal_Vq_dVq(const double& omega, return Vq_dVq; } +auto Gaussian_Abfs::DPcal_Vq_2d(const int& lp_max, + const int& lq_max, + const size_t& ik, + const double& chi2d, + const ModuleBase::Vector3& tau, + const ModuleBase::realArray& gaunt) -> RI::Tensor> +{ + const int Lmax = lp_max + lq_max; + const size_t vq_ndim0 = (lp_max + 1) * (lp_max + 1); + const size_t vq_ndim1 = (lq_max + 1) * (lq_max + 1); + RI::Tensor> Vq; + LRI_CV_Tools::init_elem(Vq, vq_ndim0, vq_ndim1); + + const int n_add_ksq = std::min(lp_max, lq_max); + std::vector>> lattice_sum(n_add_ksq + 1); + const double exponent = 1.0 / this->lambda; + const ModuleBase::Vector3 qvec = this->kvec_c[ik]; + + for (int i_add_ksq = 0; i_add_ksq != n_add_ksq + 1; ++i_add_ksq) + { + const double power = -2.0 + 2 * i_add_ksq; + const int this_Lmax = Lmax - 2 * i_add_ksq; + const bool exclude_Gamma = (qvec.norm() < 1e-10 && i_add_ksq == 0); + lattice_sum[i_add_ksq] + = this->get_lattice_sum_2d(this->tpiba, ik, power, exponent, exclude_Gamma, this_Lmax, tau); + } + + if (qvec.norm() < 1e-10) + { + const double tau_z_cart = tau.z * this->lat0; + const double regular = this->C_reg_2d(tau_z_cart, exponent); + const std::complex val = ModuleBase::PI * chi2d + regular; + const std::complex frac = 1.0 / std::sqrt(ModuleBase::FOUR_PI); + LRI_CV_Tools::add_elem(lattice_sum[0][0], val, frac * this->lz_bohr / ModuleBase::TWO_PI); + } + + for (int lp = 0; lp != lp_max + 1; ++lp) + { + const double norm_1 = double_factorial(2 * lp - 1) * std::sqrt(ModuleBase::PI * 0.5); + for (int lq = 0; lq != lq_max + 1; ++lq) + { + const double norm_2 = double_factorial(2 * lq - 1) * std::sqrt(ModuleBase::PI * 0.5); + const std::complex phase = std::pow(ModuleBase::IMAG_UNIT, lp - lq); + const std::complex cfac + = ModuleBase::FOUR_PI * phase * std::pow(ModuleBase::TWO_PI, 3) / (norm_1 * norm_2) / this->omega; + for (int L = std::abs(lp - lq); L <= lp + lq; L += 2) + { + const int i_add_ksq = (lp + lq - L) / 2; + for (int mp = 0; mp != 2 * lp + 1; ++mp) + { + const int lmp = lp * lp + mp; + for (int mq = 0; mq != 2 * lq + 1; ++mq) + { + const int lmq = lq * lq + mq; + for (int m = 0; m != 2 * L + 1; ++m) + { + const int lm = L * L + m; + const double triple_Y = gaunt(lmp, lmq, lm); + const std::complex fac = triple_Y * cfac; + LRI_CV_Tools::add_elem(Vq, lmp, lmq, lattice_sum[i_add_ksq][lm], fac); + } + } + } + } + } + } + + return Vq; +} + Numerical_Orbital_Lm Gaussian_Abfs::Gauss(const Numerical_Orbital_Lm& orb, const double& lambda) { Numerical_Orbital_Lm gaussian; const int angular_momentum_l = orb.getL(); - const double eta = 35; - const double rcut = std::sqrt(eta / lambda); + const double rcut = radial_cutoff(lambda, angular_momentum_l); const double dr = orb.get_rab().back(); int Nr = std::ceil(rcut / dr); if (Nr % 2 == 0) @@ -331,6 +425,112 @@ auto Gaussian_Abfs::get_lattice_sum(const double& tpiba, ->DPcal_lattice_sum>(tpiba, ik, power, exponent, exclude_Gamma, lmax, func_DPcal_phase); } +auto Gaussian_Abfs::get_lattice_sum_2d(const double& tpiba, + const size_t& ik, + const double& power, + const double& exponent, + const bool& exclude_Gamma, + const int& lmax, + const ModuleBase::Vector3& tau) -> std::vector> +{ + if (power < 0.0 && !exclude_Gamma && this->kvec_c[ik].norm() < 1e-10) + { + ModuleBase::WARNING_QUIT("Gaussian_Abfs::lattice_sum_2d", "Gamma point for power<0.0 cannot be evaluated!"); + } + + const int total_lm = (lmax + 1) * (lmax + 1); + std::vector> result(total_lm, std::complex{}); + const int n_super0 = this->n_supercells_2d[ik][0]; + const int n_super1 = this->n_supercells_2d[ik][1]; + const double conversion = this->lz_bohr / ModuleBase::TWO_PI; + const double tau_z_bohr = tau.z * this->lat0; + + for (int i0 = -n_super0; i0 <= n_super0; ++i0) + { + for (int i1 = -n_super1; i1 <= n_super1; ++i1) + { + const bool excluded_gamma_term = exclude_Gamma && i0 == 0 && i1 == 0; + + const ModuleBase::Vector3 qg_direct + = -(this->kvec_c[ik] + this->gvecs_direct[0] * static_cast(i0) + + this->gvecs_direct[1] * static_cast(i1)); + const ModuleBase::Vector3 qg_cart = qg_direct * tpiba; + const double q_parallel_sq = qg_cart.x * qg_cart.x + qg_cart.y * qg_cart.y; + const std::complex phase + = std::exp(ModuleBase::TWO_PI * ModuleBase::IMAG_UNIT * (qg_direct * tau)); + const double gaussian_parallel = std::exp(-exponent * q_parallel_sq); + + for (int L = 0; L != lmax + 1; ++L) + { + // Only L=0 is singular and L=1 has a direction-dependent q->0 + // limit. The L>=2 Coulomb channels have finite Gamma limits. + if (excluded_gamma_term && (std::abs(power + 2.0) >= 1e-12 || L < 2)) + { + continue; + } + for (int m = 0; m != 2 * L + 1; ++m) + { + const int lm = L * L + m; + const std::complex k_lm = this->K_LM_2d( + L, + m, + power, + ModuleBase::Vector3(qg_cart.x, qg_cart.y, 0.0), + tau_z_bohr, + exponent); + result[lm] += conversion * gaussian_parallel * phase * k_lm; + } + } + } + } + + return result; +} + +double Gaussian_Abfs::I2_2d(const double& q_parallel_abs, const double& tau_z, const double& beta) const +{ + return GaussianAbfs2D::i2(q_parallel_abs, tau_z, beta); +} + +double Gaussian_Abfs::C_reg_2d(const double& tau_z, const double& beta) const +{ + const double a = std::abs(tau_z); + const double sqrt_beta = std::sqrt(beta); + return ModuleBase::PI * a * (std::erfc(a / (2.0 * sqrt_beta)) - 1.0) + - 2.0 * std::sqrt(ModuleBase::PI * beta) * std::exp(-a * a / (4.0 * beta)); +} + +std::complex Gaussian_Abfs::K_LM_2d(const int& L, + const int& M, + const double& power, + const ModuleBase::Vector3& q_parallel_cart, + const double& tau_z, + const double& beta) const +{ + const double q_parallel_abs + = std::sqrt(q_parallel_cart.x * q_parallel_cart.x + q_parallel_cart.y * q_parallel_cart.y); + if (L == 0 && M == 0 && std::abs(power + 2.0) < 1e-12) + { + return this->I2_2d(q_parallel_abs, tau_z, beta) / std::sqrt(ModuleBase::FOUR_PI); + } + if (L == 1 && std::abs(power + 2.0) < 1e-12) + { + return GaussianAbfs2D::coulomb_l1(M, + q_parallel_cart.x, + q_parallel_cart.y, + tau_z, + beta); + } + + return GaussianAbfs2D::integral_lm(L, + M, + power, + q_parallel_cart.x, + q_parallel_cart.y, + tau_z, + beta); +} + auto Gaussian_Abfs::get_d_lattice_sum( const double& tpiba, const size_t& ik, @@ -384,12 +584,9 @@ auto Gaussian_Abfs::DPcal_lattice_sum( omp_out.begin(), \ LRI_CV_Tools::plus())) \ initializer(omp_priv = decltype(omp_orig)(omp_orig.size())) -// // auto start0 = std::chrono::system_clock::now(); -#pragma omp parallel for reduction(vec_plus : result) - for (int idx = 0; idx < total_cells; ++idx) - { + auto accumulate_cell = [&](const int idx, std::vector& out) { if (exclude_Gamma && this->check_gamma[ik][idx]) - continue; + return; ModuleBase::Vector3 vec = this->qGvecs[ik][idx]; const double vec_sq = vec.norm2() * tpiba * tpiba; @@ -405,10 +602,18 @@ auto Gaussian_Abfs::DPcal_lattice_sum( { const int lm = L * L + m; const double val_lm = val_l * this->ylm[ik](lm, idx); - result[lm] = result[lm] + RI::Global_Func::convert>(val_lm) * phase; + out[lm] = out[lm] + RI::Global_Func::convert>(val_lm) * phase; } } + }; + +// // auto start0 = std::chrono::system_clock::now(); +#pragma omp parallel for reduction(vec_plus : result) + for (int idx = 0; idx < total_cells; ++idx) + { + accumulate_cell(idx, result); } + // auto end0 = std::chrono::system_clock::now(); // auto duration0 = // std::chrono::duration_cast(end0 - start0); @@ -446,4 +651,4 @@ std::vector Gaussian_Abfs::get_n_supercells(const double& lat0, const Modul return n_supercells; } -#endif \ No newline at end of file +#endif diff --git a/source/source_lcao/module_ri/gaussian_abfs.h b/source/source_lcao/module_ri/gaussian_abfs.h index c48ce1db4ba..8813e5e0574 100644 --- a/source/source_lcao/module_ri/gaussian_abfs.h +++ b/source/source_lcao/module_ri/gaussian_abfs.h @@ -6,13 +6,19 @@ #ifndef GAUSSIAN_ABFS_H #define GAUSSIAN_ABFS_H +#include "conv_coulomb_pot_k.h" +#include "ewald_gaussian_cutoff.h" #include "source_basis/module_ao/orb_atomic_lm.h" +#include "source_basis/module_ao/orb_gaunt_table.h" //#include "source_basis/module_pw/pw_basis_k.h" #include "source_cell/klist.h" #include #include +#include +#include #include +#include #include class Gaussian_Abfs @@ -36,6 +42,11 @@ class Gaussian_Abfs C^{LM}_{lm,l'm'}: Gaunt coefficient */ public: + static double radial_cutoff(const double lambda, const int angular_momentum) + { + return EwaldGaussianCutoff::real_space_radius(lambda, angular_momentum); + } + void init(const UnitCell& ucell, const int& Lmax, const std::vector>& kvec_c, @@ -48,6 +59,12 @@ class Gaussian_Abfs const double& chi, // Singularity corrected value at q=0. const ModuleBase::Vector3& tau, const ModuleBase::realArray& gaunt); + RI::Tensor> get_Vq_2d(const int& lp_max, + const int& lq_max, + const size_t& ik, + const double& chi2d, + const ModuleBase::Vector3& tau, + const ModuleBase::realArray& gaunt); std::array>, 3> get_dVq( const int& lp_max, @@ -71,6 +88,13 @@ Calculate the lattice sum over a Gaussian: const bool& exclude_Gamma, // The R==0. can be excluded by this flag. const int& lmax, // Maximum angular momentum the sum is needed for. const ModuleBase::Vector3& tau); + std::vector> get_lattice_sum_2d(const double& tpiba, + const size_t& ik, + const double& power, + const double& exponent, + const bool& exclude_Gamma, + const int& lmax, + const ModuleBase::Vector3& tau); std::vector, 3>> get_d_lattice_sum( const double& tpiba, @@ -90,9 +114,13 @@ Calculate the lattice sum over a Gaussian: double tpiba; double lat0; double omega; + double area_parallel_bohr2; + double lz_bohr; std::vector> kvec_c; std::vector>> qGvecs; + std::vector> gvecs_direct; std::vector n_cells; + std::vector> n_supercells_2d; std::vector> check_gamma; std::vector ylm; template @@ -114,6 +142,20 @@ Calculate the lattice sum over a Gaussian: const ModuleBase::Vector3& tau, const ModuleBase::realArray& gaunt, const T_func_DPcal_lattice_sum& func_DPcal_lattice_sum); + RI::Tensor> DPcal_Vq_2d(const int& lp_max, + const int& lq_max, + const size_t& ik, + const double& chi2d, + const ModuleBase::Vector3& tau, + const ModuleBase::realArray& gaunt); + std::complex K_LM_2d(const int& L, + const int& M, + const double& power, + const ModuleBase::Vector3& q_parallel_cart, + const double& tau_z, + const double& beta) const; + double I2_2d(const double& q_parallel_abs, const double& tau_z, const double& beta) const; + double C_reg_2d(const double& tau_z, const double& beta) const; template std::vector DPcal_lattice_sum(const double& tpiba, @@ -130,4 +172,4 @@ Calculate the lattice sum over a Gaussian: static std::vector get_n_supercells(const double& lat0, const ModuleBase::Matrix3& G, const double& Gmax); }; -#endif \ No newline at end of file +#endif diff --git a/source/source_lcao/module_ri/gaussian_abfs_2d_integrals.h b/source/source_lcao/module_ri/gaussian_abfs_2d_integrals.h new file mode 100644 index 00000000000..a5e95cc3fca --- /dev/null +++ b/source/source_lcao/module_ri/gaussian_abfs_2d_integrals.h @@ -0,0 +1,269 @@ +#ifndef GAUSSIAN_ABFS_2D_INTEGRALS_H +#define GAUSSIAN_ABFS_2D_INTEGRALS_H + +#include +#include +#include +#include + +// Closed-form in-plane momentum integrals of the 2D Ewald Gaussian +// compensation for auxiliary functions. +// +// In the mixed representation (in-plane momentum q, real z) every needed +// integral reduces to z moments of a shifted Gaussian, which are analytic +// (physicists' Hermite polynomials), times in-plane factors that are +// polynomials in q arranged by real solid harmonics. The base Coulomb +// integrals i2/di2_dtau carry the erfc form of the z integration. Only even +// in-plane kernel powers q^p with p >= -2 occur; p = -2 (the Coulomb +// singularity) is handled by synthetic division onto the base integrals. +namespace GaussianAbfs2D +{ +constexpr double pi = 3.141592653589793238462643383279502884; + +// Base integral: the z integration of the Gaussian-compensated Coulomb at +// in-plane momentum q and center offset tau_z, +// i2 = (pi/2q) e^{beta q^2} [ e^{q|tau_z|} erfc(sqrt(beta) q + |tau_z|/2 sqrt(beta)) +// + e^{-q|tau_z|} erfc(sqrt(beta) q - |tau_z|/2 sqrt(beta)) ], +// which reduces to (pi/q) e^{beta q^2} erfc(sqrt(beta) q) at tau_z = 0. +inline double i2(const double q, const double tau_z, const double beta) +{ + if (q < 1.0e-12) + { + return 0.0; + } + + const double sqrt_beta = std::sqrt(beta); + const double x = sqrt_beta * q; + if (std::abs(tau_z) < 1.0e-14) + { + return pi / q * std::exp(x * x) * std::erfc(x); + } + + const double a = std::abs(tau_z); + const double term_plus = std::exp(q * a) * std::erfc(x + a / (2.0 * sqrt_beta)); + const double term_minus = std::exp(-q * a) * std::erfc(x - a / (2.0 * sqrt_beta)); + return pi / (2.0 * q) * std::exp(x * x) * (term_plus + term_minus); +} + +inline double di2_dtau(const double q, const double tau_z, const double beta) +{ + if (q < 1.0e-12 || std::abs(tau_z) < 1.0e-14) + { + return 0.0; + } + + const double a = std::abs(tau_z); + const double sqrt_beta = std::sqrt(beta); + const double x = sqrt_beta * q; + const double term_plus = std::exp(q * a) * std::erfc(x + a / (2.0 * sqrt_beta)); + const double term_minus = std::exp(-q * a) * std::erfc(x - a / (2.0 * sqrt_beta)); + const double derivative_a = 0.5 * pi * std::exp(x * x) * (term_plus - term_minus); + return tau_z < 0.0 ? -derivative_a : derivative_a; +} + +inline std::complex coulomb_l1(const int m, + const double qx, + const double qy, + const double tau_z, + const double beta) +{ + const double q = std::hypot(qx, qy); + const double coefficient = std::sqrt(3.0 / (4.0 * pi)); + if (m == 0) + { + return std::complex(0.0, -coefficient * di2_dtau(q, tau_z, beta)); + } + if (m == 1) + { + return -coefficient * qx * i2(q, tau_z, beta); + } + if (m == 2) + { + return -coefficient * qy * i2(q, tau_z, beta); + } + return 0.0; +} + +namespace detail +{ +inline double factorial(const int n) +{ + if (n < 0) throw std::invalid_argument("factorial argument must be non-negative"); + return std::tgamma(static_cast(n + 1)); +} + +inline double physicists_hermite(const int degree, const double x) +{ + if (degree < 0) throw std::invalid_argument("Hermite degree must be non-negative"); + if (degree == 0) return 1.0; + if (degree == 1) return 2.0 * x; + double hm2 = 1.0; + double hm1 = 2.0 * x; + for (int n = 2; n <= degree; ++n) + { + const double h = 2.0 * x * hm1 - 2.0 * static_cast(n - 1) * hm2; + hm2 = hm1; + hm1 = h; + } + return hm1; +} + +inline std::complex i_power(const int degree) +{ + switch ((degree % 4 + 4) % 4) + { + case 0: return {1.0, 0.0}; + case 1: return {0.0, 1.0}; + case 2: return {-1.0, 0.0}; + default: return {0.0, -1.0}; + } +} + +// z moment of the shifted Gaussian, <(z)^degree exp(-beta (z-tau_z)^2)>, +// expressed through physicists' Hermite polynomials; i^degree carries the +// Fourier convention of the mixed representation. +inline std::complex gaussian_fourier_moment(const int degree, + const double tau_z, + const double beta) +{ + if (beta <= 0.0) throw std::invalid_argument("Gaussian exponent must be positive"); + const double scaled_tau = tau_z / std::sqrt(beta); + const double radial_prefactor = std::pow(beta, -0.5 * static_cast(degree + 1)); + const double fourier_prefactor = std::sqrt(pi) / std::pow(2.0, degree); + return radial_prefactor * fourier_prefactor + * physicists_hermite(degree, 0.5 * scaled_tau) + * std::exp(-0.25 * scaled_tau * scaled_tau) * i_power(degree); +} + +inline std::vector> complex_solid_harmonic_coefficients(const int L, + const int m, + const double qx, + const double qy) +{ + if (L < 0 || m < 0 || m > L) throw std::invalid_argument("invalid solid-harmonic indices"); + std::vector> coefficients(static_cast(L + 1), + std::complex{}); + const double normalization + = std::sqrt(static_cast(2 * L + 1) / (4.0 * pi) + * factorial(L - m) / factorial(L + m)); + const double condon_shortley = (m % 2 == 0) ? 1.0 : -1.0; + const std::complex angular_prefactor + = normalization * condon_shortley * std::pow(std::complex(qx, qy), m); + const double q_sq = qx * qx + qy * qy; + for (int k = 0; k <= L / 2; ++k) + { + const int z_degree = L - 2 * k - m; + if (z_degree < 0) continue; + const double legendre_derivative + = ((k % 2 == 0) ? 1.0 : -1.0) * factorial(2 * L - 2 * k) + / (std::pow(2.0, L) * factorial(k) * factorial(L - k) * factorial(z_degree)); + for (int j = 0; j <= k; ++j) + { + const int degree = z_degree + 2 * j; + const double binomial = factorial(k) / (factorial(j) * factorial(k - j)); + coefficients[static_cast(degree)] + += angular_prefactor * legendre_derivative * binomial * std::pow(q_sq, k - j); + } + } + return coefficients; +} + +inline std::vector real_solid_harmonic_coefficients(const int L, + const int M, + const double qx, + const double qy) +{ + if (M < 0 || M > 2 * L) throw std::invalid_argument("invalid real solid-harmonic index"); + const int m = M == 0 ? 0 : (M + 1) / 2; + const auto complex_coefficients = complex_solid_harmonic_coefficients(L, m, qx, qy); + std::vector coefficients(complex_coefficients.size(), 0.0); + if (M == 0) + { + for (std::size_t i = 0; i < coefficients.size(); ++i) + coefficients[i] = complex_coefficients[i].real(); + } + else if (M % 2 == 1) + { + for (std::size_t i = 0; i < coefficients.size(); ++i) + coefficients[i] = std::sqrt(2.0) * complex_coefficients[i].real(); + } + else + { + for (std::size_t i = 0; i < coefficients.size(); ++i) + coefficients[i] = std::sqrt(2.0) * complex_coefficients[i].imag(); + } + return coefficients; +} +} // namespace detail + +// Master integral: in-plane kernel q^power times the (L,M) real solid +// harmonic of the auxiliary function, with the z dependence of the Gaussian +// integrated analytically. The solid harmonic is first expanded into +// coefficients indexed by z degree; then either +// power = -2: synthetic division of the polynomial by q^2 maps everything +// onto the Coulomb base integrals i2 / di2_dtau, or +// power >= 0 (even): binomial expansion of (q_parallel^2 + qz^2)^{p/2} +// maps onto the Gaussian z moments. +inline std::complex integral_lm(const int L, + const int M, + const double power, + const double qx, + const double qy, + const double tau_z, + const double beta) +{ + if (beta <= 0.0) throw std::invalid_argument("Gaussian exponent must be positive"); + const int integer_power = static_cast(std::lround(power)); + if (std::abs(power - static_cast(integer_power)) > 1.0e-12 + || integer_power < -2 || integer_power % 2 != 0) + { + throw std::invalid_argument("2D Gaussian kernel power must be an even integer >= -2"); + } + + const double q_sq = qx * qx + qy * qy; + const double q = std::sqrt(q_sq); + const auto solid = detail::real_solid_harmonic_coefficients(L, M, qx, qy); + std::vector> polynomial(solid.begin(), solid.end()); + + if (integer_power == -2) + { + std::vector> quotient(polynomial.size(), std::complex{}); + for (int degree = static_cast(polynomial.size()) - 1; degree >= 2; --degree) + { + const auto leading = polynomial[static_cast(degree)]; + quotient[static_cast(degree - 2)] += leading; + polynomial[static_cast(degree)] = 0.0; + polynomial[static_cast(degree - 2)] -= q_sq * leading; + } + std::complex result{}; + for (std::size_t degree = 0; degree < quotient.size(); ++degree) + result += quotient[degree] + * detail::gaussian_fourier_moment(static_cast(degree), tau_z, beta); + result += polynomial[0] * i2(q, tau_z, beta); + if (polynomial.size() > 1) + result += polynomial[1] * std::complex(0.0, -di2_dtau(q, tau_z, beta)); + return result; + } + + const int exponent = integer_power / 2; + std::vector> expanded( + polynomial.size() + static_cast(2 * exponent), std::complex{}); + for (std::size_t degree = 0; degree < polynomial.size(); ++degree) + { + for (int j = 0; j <= exponent; ++j) + { + const double binomial + = detail::factorial(exponent) / (detail::factorial(j) * detail::factorial(exponent - j)); + expanded[degree + static_cast(2 * j)] + += polynomial[degree] * binomial * std::pow(q_sq, exponent - j); + } + } + std::complex result{}; + for (std::size_t degree = 0; degree < expanded.size(); ++degree) + result += expanded[degree] + * detail::gaussian_fourier_moment(static_cast(degree), tau_z, beta); + return result; +} +} // namespace GaussianAbfs2D + +#endif diff --git a/source/source_lcao/module_ri/lri_cv.h b/source/source_lcao/module_ri/lri_cv.h index 12359d3b5e3..33238259388 100644 --- a/source/source_lcao/module_ri/lri_cv.h +++ b/source/source_lcao/module_ri/lri_cv.h @@ -9,7 +9,7 @@ #include "matrix_orbs11.h" #include "matrix_orbs21.h" #include "source_basis/module_ao/orb_atomic_lm.h" -#include "abfs_vector3_order.h" +#include "source_lcao/module_ri/abfs_vector3_order.h" #include "source_base/element_basis_index.h" #include "source_hamilt/module_xc/exx_info_ri.h" @@ -44,7 +44,8 @@ class LRI_CV const std::vector>> &abfs_ccp_in, const double &kmesh_times, std::shared_ptr MGT, - const bool& init_C); + const bool& init_C, + const ModuleBase::Element_Basis_Index::IndexPermutation &abfs_old_to_new = {}); inline std::map>> cal_Vs( const UnitCell &ucell, @@ -65,7 +66,8 @@ class LRI_CV const std::vector &list_A1, const std::map &flags); // "cal_dC", "writable_Cws", "writable_dCws", "writable_Vws", "writable_dVws" - size_t get_index_abfs_size(const size_t &iat){return this->index_abfs[iat].count_size; } + size_t get_index_abfs_size(const size_t &iat){return this->index_abfs[iat].count_size; } + inline double get_V_Rcut(const int it0, const int it1) { return this->cal_V_Rcut(it0, it1); } private: std::vector>> lcaos; diff --git a/source/source_lcao/module_ri/lri_cv.hpp b/source/source_lcao/module_ri/lri_cv.hpp index 49f197816c0..b26a8853702 100644 --- a/source/source_lcao/module_ri/lri_cv.hpp +++ b/source/source_lcao/module_ri/lri_cv.hpp @@ -10,10 +10,10 @@ #include "lri_cv_tools.h" #include "exx_abfs_ctor_orbs.h" #include "ri_util.h" -#include "source_basis/module_ao/elem_basis_idx_orb.h" -#include "source_base/tool_title.h" -#include "source_base/timer.h" -#include "source_hamilt/module_xc/exx_info_ri.h" +#include "../../source_basis/module_ao/elem_basis_idx_orb.h" +#include "../../source_base/tool_title.h" +#include "../../source_base/timer.h" +#include "source_hamilt/module_xc/exx_info.h" // use GlobalC::exx_info #include #include @@ -45,7 +45,8 @@ void LRI_CV::set_orbitals( const std::vector>> &abfs_ccp_in, const double &kmesh_times, std::shared_ptr MGT, - const bool& init_C) + const bool& init_C, + const ModuleBase::Element_Basis_Index::IndexPermutation &abfs_old_to_new) { ModuleBase::TITLE("LRI_CV", "set_orbitals"); ModuleBase::timer::start("LRI_CV", "set_orbitals"); @@ -63,7 +64,7 @@ void LRI_CV::set_orbitals( const ModuleBase::Element_Basis_Index::Range range_abfs = ModuleBase::Element_Basis_Index::construct_range( abfs ); - this->index_abfs = ModuleBase::Element_Basis_Index::construct_index( range_abfs ); + this->index_abfs = ModuleBase::Element_Basis_Index::construct_index( range_abfs, abfs_old_to_new ); this->m_abfs_abfs.MGT = this->m_abfslcaos_lcaos.MGT = MGT; this->m_abfs_abfs.init( @@ -75,9 +76,9 @@ void LRI_CV::set_orbitals( ucell, orb, kmesh_times); this->m_abfs_abfs.init_radial_table(); - if (init_C) { - this->m_abfslcaos_lcaos.init_radial_table(); - } + if (init_C) { + this->m_abfslcaos_lcaos.init_radial_table(); + } ModuleBase::timer::end("LRI_CV", "set_orbitals"); } diff --git a/source/source_lcao/module_ri/lri_cv_tools.h b/source/source_lcao/module_ri/lri_cv_tools.h index d5b0cd4f68f..1c48c945a53 100644 --- a/source/source_lcao/module_ri/lri_cv_tools.h +++ b/source/source_lcao/module_ri/lri_cv_tools.h @@ -7,7 +7,7 @@ #define LRI_CV_TOOLS_H #include "invmat_ri.h" -#include "abfs_vector3_order.h" +#include "source_lcao/module_ri/abfs_vector3_order.h" #include "source_lcao/module_ri/abfs.h" #include @@ -80,6 +80,10 @@ inline std::map>> minus( template extern std::map> minus(std::map>& v1, std::map>& v2); +template +extern std::map> minus_common_keys( + const std::map>& v1, + const std::map>& v2); template extern std::vector> add(const std::vector>& v1, @@ -143,8 +147,6 @@ using TLRI = std::map>>; template TLRI read_Cs_ao(const std::string& file_path, const double& threshold = 1e-10); template -TLRI read_Cs_ao_all(const std::string& path, const double& threshold = 1e-10); -template void write_Cs_ao(const TLRI& Vs, const std::string& file_path); template TLRI read_Vs_abf(const std::string& file_path, const double& threshold = 1e-10); diff --git a/source/source_lcao/module_ri/lri_cv_tools.hpp b/source/source_lcao/module_ri/lri_cv_tools.hpp index 1f96ec8c33d..0da51149918 100644 --- a/source/source_lcao/module_ri/lri_cv_tools.hpp +++ b/source/source_lcao/module_ri/lri_cv_tools.hpp @@ -6,7 +6,7 @@ #ifndef LRI_CV_TOOLS_HPP #define LRI_CV_TOOLS_HPP -#include "source_base/mathzone.h" +#include "../../source_base/mathzone.h" #include "invmat_ri.h" #include "lri_cv_tools.h" #include "ri_util.h" @@ -229,6 +229,36 @@ std::vector> return v; } +template +std::map> LRI_CV_Tools::minus_common_keys( + const std::map>& v1, + const std::map>& v2) { + std::map> dv; + for (typename std::map>::const_iterator it_map1 = v1.begin(); + it_map1 != v1.end(); + ++it_map1) { + const TkeyA& keyA = it_map1->first; + const auto it_map2 = v2.find(keyA); + if (it_map2 == v2.end()) { + dv[keyA] = it_map1->second; + continue; + } + const std::map& map1 = it_map1->second; + for (typename std::map::const_iterator it_value1 = map1.begin(); + it_value1 != map1.end(); + ++it_value1) { + const TkeyB& keyB = it_value1->first; + const auto it_value2 = it_map2->second.find(keyB); + if (it_value2 == it_map2->second.end()) { + dv[keyA][keyB] = it_value1->second; + continue; + } + dv[keyA][keyB] = it_value1->second - it_value2->second; + } + } + return dv; +} + template std::map>> LRI_CV_Tools::add( std::map>>& v1, @@ -255,21 +285,17 @@ template std::map> LRI_CV_Tools::add(std::map>& v1, std::map>& v2) { - assert(v1.size() == v2.size()); using namespace RI::Map_Operator; using namespace RI::Array_Operator; - std::map> dv; - auto it1 = v1.begin(); - auto it2 = v2.begin(); - while (it1 != v1.end() && it2 != v2.end()) { - assert(it1->first == it2->first); - const TkeyA& keyA = it1->first; - const std::map& map1 = it1->second; - const std::map& map2 = it2->second; - dv[keyA] = map1 + map2; - ++it1; - ++it2; + std::map> dv = v1; + for (const auto& key_map_pair: v2) { + const auto dv_it = dv.find(key_map_pair.first); + if (dv_it == dv.end()) { + dv.emplace(key_map_pair.first, key_map_pair.second); + } else { + dv_it->second = dv_it->second + key_map_pair.second; + } } return dv; } diff --git a/source/source_lcao/module_ri/matrix_orbs11.cpp b/source/source_lcao/module_ri/matrix_orbs11.cpp index 2673535cddb..07f8cdcc1ba 100644 --- a/source/source_lcao/module_ri/matrix_orbs11.cpp +++ b/source/source_lcao/module_ri/matrix_orbs11.cpp @@ -36,7 +36,7 @@ void Matrix_Orbs11::init( const double dr = orb.get_dR(); const double dk = orb.get_dk(); - const int kmesh = orb.get_kmesh() * kmesh_times + 1; + const int kmesh = static_cast(orb.get_kmesh() * kmesh_times) | 1; const double rmax = Exx_Abfs::Construct_Orbs::get_Rmax(orb_A) + Exx_Abfs::Construct_Orbs::get_Rmax(orb_B); diff --git a/source/source_lcao/module_ri/matrix_orbs21.cpp b/source/source_lcao/module_ri/matrix_orbs21.cpp index 963d7786b42..ddf8a371770 100644 --- a/source/source_lcao/module_ri/matrix_orbs21.cpp +++ b/source/source_lcao/module_ri/matrix_orbs21.cpp @@ -38,7 +38,7 @@ void Matrix_Orbs21::init( const double dr = orb.get_dR(); const double dk = orb.get_dk(); - const int kmesh = orb.get_kmesh() * kmesh_times + 1; + const int kmesh = static_cast(orb.get_kmesh() * kmesh_times) | 1; const double rmax = std::min({Exx_Abfs::Construct_Orbs::get_Rmax(orb_A1), Exx_Abfs::Construct_Orbs::get_Rmax(orb_A2)}) + Exx_Abfs::Construct_Orbs::get_Rmax(orb_B); diff --git a/source/source_lcao/module_ri/matrix_orbs22.cpp b/source/source_lcao/module_ri/matrix_orbs22.cpp index aa9cd012719..903968b5d1e 100644 --- a/source/source_lcao/module_ri/matrix_orbs22.cpp +++ b/source/source_lcao/module_ri/matrix_orbs22.cpp @@ -40,7 +40,7 @@ void Matrix_Orbs22::init( const double dr = orb.get_dR(); const double dk = orb.get_dk(); - const int kmesh = orb.get_kmesh() * kmesh_times + 1; + const int kmesh = static_cast(orb.get_kmesh() * kmesh_times) | 1; const double rmax = std::min({Exx_Abfs::Construct_Orbs::get_Rmax(orb_A1), Exx_Abfs::Construct_Orbs::get_Rmax(orb_A2)}) + std::min({Exx_Abfs::Construct_Orbs::get_Rmax(orb_B1), Exx_Abfs::Construct_Orbs::get_Rmax(orb_B2)}); diff --git a/source/source_lcao/module_ri/mix_matrix.cpp b/source/source_lcao/module_ri/mix_matrix.cpp new file mode 100644 index 00000000000..e8f4706fc18 --- /dev/null +++ b/source/source_lcao/module_ri/mix_matrix.cpp @@ -0,0 +1,164 @@ +//======================= +// AUTHOR : Peize Lin +// DATE : 2023-05-09 +//======================= + +#ifndef MIX_DATA_HPP +#define MIX_DATA_HPP + +#include "mix_matrix.h" +#include "source_base/matrix.h" +#include "source_base/complexmatrix.h" +#include "source_base/tool_title.h" + +template<> +void Mix_Matrix::mix(const ModuleBase::matrix& data_in, const bool flag_restart) +{ + if(separate_loop) + { + this->mixing = new Base_Mixing::Plain_Mixing(this->mixing_beta); + } + + if(flag_restart) + { + this->data_out = data_in; + this->mixing->init_mixing_data(this->matrix_data, data_in.nc*data_in.nr, sizeof(*data_in.c)); + } + else + { + this->mixing->push_data(this->matrix_data, data_out.c, data_in.c, nullptr, false); + this->mixing->mix_data(this->matrix_data, data_out.c); + } + + if(separate_loop) + { + delete this->mixing; + this->mixing = nullptr; + } +} +template<> +void Mix_Matrix::mix(const ModuleBase::ComplexMatrix& data_in, const bool flag_restart) +{ + if (separate_loop) + { + this->mixing = new Base_Mixing::Plain_Mixing(this->mixing_beta); + } + + if (flag_restart) + { + this->data_out = data_in; + this->mixing->init_mixing_data(this->matrix_data, data_in.nc * data_in.nr, sizeof(*data_in.c)); + } + else + { + this->mixing->push_data(this->matrix_data, data_out.c, data_in.c, nullptr, false); + this->mixing->mix_data(this->matrix_data, data_out.c); + } + + if (separate_loop) + { + delete this->mixing; + this->mixing = nullptr; + } +} + +template<> +void Mix_Matrix>::mix(const std::vector& data_in, const bool flag_restart) +{ + if (separate_loop) + { + this->mixing = new Base_Mixing::Plain_Mixing(this->mixing_beta); + } + + if (flag_restart) + { + this->data_out = data_in; + this->mixing->init_mixing_data(this->matrix_data, data_in.size(), sizeof(*data_in.data())); + } + else + { + this->mixing->push_data(this->matrix_data, data_out.data(), data_in.data(), nullptr, false); + this->mixing->mix_data(this->matrix_data, data_out.data()); + } + + if (separate_loop) + { + delete this->mixing; + this->mixing = nullptr; + } +} +template<> +void Mix_Matrix>>::mix(const std::vector>& data_in, const bool flag_restart) +{ + if (separate_loop) + { + this->mixing = new Base_Mixing::Plain_Mixing(this->mixing_beta); + } + + if (flag_restart) + { + this->data_out = data_in; + this->mixing->init_mixing_data(this->matrix_data, data_in.size(), sizeof(*data_in.data())); + } + else + { + this->mixing->push_data(this->matrix_data, data_out.data(), data_in.data(), nullptr, false); + this->mixing->mix_data(this->matrix_data, data_out.data()); + } + + if (separate_loop) + { + delete this->mixing; + this->mixing = nullptr; + } +} + +/* +// mix for ct::Tensor if useful in the future. comment out for UT-coverage +template<> +void Mix_Matrix::mix(const ct::Tensor& data_in, const bool flag_restart) +{ + ModuleBase::TITLE("Mix_Matrix", "mix"); + if (separate_loop) + { + this->mixing = new Base_Mixing::Plain_Mixing(this->mixing_beta); + } + + if (flag_restart) + { + this->data_out = data_in; + this->mixing->init_mixing_data(this->matrix_data, data_in.NumElements(), sizeof(data_in.SizeOfType(data_in.data_type()))); + } + else + { + switch (data_in.data_type()) + { + case ct::DataType::DT_DOUBLE: + this->mixing->push_data(this->matrix_data, data_out.data(), data_in.data(), nullptr, false); + this->mixing->mix_data(this->matrix_data, data_out.data()); + break; + case ct::DataType::DT_COMPLEX_DOUBLE: + this->mixing->push_data(this->matrix_data, data_out.data>(), data_in.data>(), nullptr, false); + this->mixing->mix_data(this->matrix_data, data_out.data>()); + break; + default: + throw std::invalid_argument("Mix_Matrix: data type not supported"); + break; + } + } + + if (separate_loop) + { + delete this->mixing; + this->mixing = nullptr; + } +} +*/ + +template class Mix_Matrix; +template class Mix_Matrix; +template class Mix_Matrix>; +template class Mix_Matrix>>; +// template class Mix_Matrix; + +#endif diff --git a/source/source_lcao/module_ri/mix_matrix.h b/source/source_lcao/module_ri/mix_matrix.h new file mode 100644 index 00000000000..e841e41462f --- /dev/null +++ b/source/source_lcao/module_ri/mix_matrix.h @@ -0,0 +1,53 @@ +//======================= +// AUTHOR : Peize Lin +// DATE : 2023-05-09 +//======================= + +#ifndef MIX_MATRIX_H +#define MIX_MATRIX_H + +#include + +#include "source_base/module_mixing/mixing.h" +#include "source_base/module_mixing/plain_mixing.h" +template +class Mix_Matrix +{ + public: + /** + * @brief init Mix_Matrix with Mixing pointer + * + * @param mixing_in Mixing pointer + */ + void init(Base_Mixing::Mixing* mixing_in) + { + this->mixing = mixing_in; + if (this->mixing == nullptr) + this->separate_loop = true; + } + /** + * @brief Get the data out object + * + * @return const Tdata& + */ + const Tdata& get_data_out() const + { + return this->data_out; + } + + /** + * @brief Mixes the input data according to the set mixing mode. + * @param data_in Input data to be mixed. + * @param flag_restart Flag indicating whether restart mixing. + */ + void mix(const Tdata& data_in, const bool flag_restart); + + double mixing_beta = 1.0; + + private: + Tdata data_out; + Base_Mixing::Mixing* mixing = nullptr; + Base_Mixing::Mixing_Data matrix_data; + bool separate_loop = false; +}; +#endif \ No newline at end of file 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 2adbb5561b2..47b55395a06 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 @@ -10,6 +10,8 @@ class UnitCell; namespace ModuleSymmetry { + class SymmetryRotationTestAccess; + /// Real-space (RI::Tensor / HContainer) H(R) and RI-coefficient symmetry restoration for /// EXX/RPA, built on top of the LibRI-independent k-space restoration in /// ModuleSymmetry::Symmetry_rotation_k (source_cell/module_symmetry/symm_rotation_k.h), @@ -37,6 +39,13 @@ namespace ModuleSymmetry const std::map, RI::Tensor>>& HR_irreduceble)const; /// @param iat2it map from global atom index to its species index /// @param iat2ia map from global atom index to its index within the species + /// Restore scalar auxiliary-basis H(R) blocks from the irreducible sector. + /// The ABF rotation is independent of the AO rotation and also applies complex + /// conjugation for antiunitary magnetic operations. + template + std::map, RI::Tensor>> restore_HR_abf( + const Symmetry& symm, const Atom* atoms, const std::vector& iat2it, + const std::map, RI::Tensor>>& HR_irreducible)const; template // HContainer type void restore_HR( const Symmetry& symm, const Atom* atoms, @@ -61,6 +70,8 @@ namespace ModuleSymmetry //-------------------------------------------------------------------------------- private: + friend class SymmetryRotationTestAccess; + //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- @@ -88,6 +99,9 @@ namespace ModuleSymmetry RI::Tensor set_rotation_matrix(const Atom& a, const int& isym)const; template RI::Tensor set_rotation_matrix_abf(const int& type, const int& isym)const; + template + RI::Tensor rotate_atompair_serial_abf(const RI::Tensor& A, const int isym, + const int& type1, const int& type2, bool output = false)const; /// RI::Tensor mirror of rotmat_Slm_ (which is stored as ModuleBase::ComplexMatrix, shared /// with the LibRI-free k-space code), rebuilt lazily and cached across the many diff --git a/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation_r.hpp b/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation_r.hpp index 86a20e6364f..f4ed2f14937 100644 --- a/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation_r.hpp +++ b/source/source_lcao/module_ri/module_exx_symmetry/symm_rotation_r.hpp @@ -2,6 +2,8 @@ #include "source_lcao/module_ri/ri_util.h" #include "source_base/timer.h" #include +#include +#include #include #include namespace ModuleSymmetry @@ -45,6 +47,12 @@ namespace ModuleSymmetry inline std::complex conj_elem(const std::complex& v) { return std::conj(v); } inline std::complex conj_elem(const std::complex& v) { return std::conj(v); } + template + inline bool has_valid_matrix_shape(const RI::Tensor& tensor) + { + return tensor.shape.size() == 2 && tensor.shape[0] > 0 && tensor.shape[1] > 0; + } + template inline void print_tensor(const RI::Tensor& t, const std::string& name, const double& threshold = 0.0) { @@ -280,6 +288,114 @@ namespace ModuleSymmetry return HR_full; } + template + RI::Tensor Symmetry_rotation::rotate_atompair_serial_abf(const RI::Tensor& A, + const int isym, const int& type1, const int& type2, const bool output)const + { + assert(this->reduce_Cs_); + if (!has_valid_matrix_shape(A)) + { + throw std::runtime_error("rotate_atompair_serial_abf: expected a non-empty rank-2 tensor."); + } + const RI::Tensor T1 = this->set_rotation_matrix_abf(type1, isym); + const RI::Tensor T2 = (type1 == type2) + ? T1 + : this->set_rotation_matrix_abf(type2, isym); + if (A.shape[0] != T1.shape[0] || A.shape[1] != T2.shape[0]) + { + throw std::runtime_error("rotate_atompair_serial_abf: tensor shape does not match the ABF rotation matrices."); + } + + RI::Tensor TAT(A.shape); + RI::Sym::T1_HR_T2(TAT.ptr(), A.ptr(), T1, T2); + // Scalar auxiliary-basis blocks have no spin indices. An antiunitary + // magnetic operation therefore adds complex conjugation after the + // spatial ABF rotation, without the spinor sigma_y channel map. + if (isym >= this->nsym_) + { + for (int i = 0; i < TAT.shape[0]; ++i) + { + for (int j = 0; j < TAT.shape[1]; ++j) + { + TAT(i, j) = ModuleSymmetry::conj_elem(TAT(i, j)); + } + } + } + if (output) + { + print_tensor(A, "A_abf"); + print_tensor(T1, "T1_abf"); + print_tensor(T2, "T2_abf"); + print_tensor(TAT, "TAT_abf"); + } + return TAT; + } + + template + std::map, RI::Tensor>> Symmetry_rotation::restore_HR_abf( + const Symmetry& symm, const Atom* atoms, const std::vector& iat2it, + const std::map, RI::Tensor>>& HR_irreducible)const + { + (void)symm; + (void)atoms; + ModuleBase::TITLE("Symmetry_rotation", "restore_HR_abf"); + ModuleBase::timer::start("Symmetry_rotation", "restore_HR_abf"); + assert(this->reduce_Cs_); + + std::map, RI::Tensor>> HR_full; + for (const auto& tmp1 : HR_irreducible) + { + const int& irap1 = tmp1.first; + for (const auto& tmp2 : tmp1.second) + { + if (!has_valid_matrix_shape(tmp2.second)) + { + std::ostringstream oss; + oss << "restore_HR_abf: invalid tensor shape for irreducible atom pair (" + << irap1 << "," << tmp2.first.first << "), R=(" + << tmp2.first.second[0] << "," << tmp2.first.second[1] << "," + << tmp2.first.second[2] << ")."; + throw std::runtime_error(oss.str()); + } + const int& irap2 = tmp2.first.first; + const TapR irapR = {{irap1, irap2}, tmp2.first.second}; + const auto star_it = this->irs_.sector_stars_.find(irapR); + if (star_it == this->irs_.sector_stars_.end()) + { + std::cout << "Warning: not found for ABF restore: irreducible atom pair = (" + << irap1 << "," << irap2 << "), irR=(" << tmp2.first.second[0] + << "," << tmp2.first.second[1] << "," << tmp2.first.second[2] << ")\n"; + continue; + } + const int type1 = iat2it[irap1]; + const int type2 = iat2it[irap2]; + for (const auto& isym_apR : star_it->second) + { + const int isym = isym_apR.first; + const TapR& apR = isym_apR.second; + const int ap1 = apR.first.first; + const int ap2 = apR.first.second; + const TC& R = apR.second; + const std::pair target_key = {ap2, R}; + if (HR_full[ap1].count(target_key) != 0) + { + std::ostringstream oss; + oss << "restore_HR_abf: duplicate target key (ap1=" << ap1 + << ", ap2=" << ap2 << ", R=(" << R[0] << "," << R[1] << "," + << R[2] << ")) produced by more than one symmetry operation;" + << " refusing to silently overwrite."; + throw std::runtime_error(oss.str()); + } + HR_full[ap1][target_key] + = rotate_atompair_serial_abf(tmp2.second, isym, type1, type2); + } + } + } + + ModuleBase::timer::end("Symmetry_rotation", "restore_HR_abf"); + return HR_full; + } + template inline void set_block(const int starti, const int startj, const RI::Tensor>& block, RI::Tensor& obj_tensor) @@ -350,6 +466,14 @@ namespace ModuleSymmetry template RI::Tensor Symmetry_rotation::set_rotation_matrix_abf(const int& type, const int& isym)const { + if (type < 0 || type >= static_cast(this->abfs_l_nchi_.size())) + { + throw std::runtime_error("ABF rotation requested with an invalid atom type index."); + } + if (isym < 0 || isym >= static_cast(this->rotmat_Slm_.size())) + { + throw std::runtime_error("ABF rotation requested with an invalid symmetry index."); + } int nabfs = 0; for (int l = 0;l < this->abfs_l_nchi_[type].size();++l) {nabfs += this->abfs_l_nchi_[type][l] * (2 * l + 1); } @@ -357,6 +481,16 @@ namespace ModuleSymmetry int iw = 0; for (int L = 0;L < this->abfs_l_nchi_[type].size();++L) { + if (L >= static_cast(this->rotmat_Slm_[isym].size()) + || this->rotmat_Slm_[isym][L].nr <= 0 + || this->rotmat_Slm_[isym][L].nc <= 0) + { + std::ostringstream oss; + oss << "Missing ABF rotation block for symmetry " << isym + << ", atom type " << type << ", angular momentum L=" << L + << ". Recompute rotation matrices with the finalized ABF Lmax before restoring ABF tensors."; + throw std::runtime_error(oss.str()); + } int nm = 2 * L + 1; for (int N = 0;N < this->abfs_l_nchi_[type][L];++N) { @@ -405,4 +539,4 @@ namespace ModuleSymmetry } } -} \ No newline at end of file +} 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 015c4fc4a16..c3733327565 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 @@ -9,3 +9,15 @@ AddTest( ../../../../source_cell/parallel_kpoints.cpp ) +AddTest( + TARGET MODULE_RI_EXX_SYMMETRY_abf_rotation + LIBS base ${math_libs} device symmetry neighbor parameter + SOURCES abf_rotation_test.cpp ../symm_rotation.cpp + ../../../../source_basis/module_ao/parallel_orbitals.cpp +) +AddTest( + TARGET MODULE_RI_EXX_SYMMETRY_nspin4_restore + LIBS base ${math_libs} device symmetry neighbor parameter + SOURCES nspin4_restore_test.cpp ../symm_rotation.cpp + ../../../../source_basis/module_ao/parallel_orbitals.cpp +) diff --git a/source/source_lcao/module_ri/module_exx_symmetry/test/abf_rotation_test.cpp b/source/source_lcao/module_ri/module_exx_symmetry/test/abf_rotation_test.cpp new file mode 100644 index 00000000000..7b4c898cd64 --- /dev/null +++ b/source/source_lcao/module_ri/module_exx_symmetry/test/abf_rotation_test.cpp @@ -0,0 +1,306 @@ +#include "mpi.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "symm_rotation_test_access.h" +using namespace std::complex_literals; // for the `1i` literal used below +#define DOUBLETHRESHOLD 1e-8 + +/* + +Focused mathematical tests for scalar-ABF unitary/antiunitary restore. + +Convention (derived from LibRI RI::Sym::T1_HR_T2, Symmetry_Rotation.h): + + H' = T1^\dagger * H * T2 + +For scalar ABF the rotation matrices T are real (real spherical-harmonic +basis; rotmat_Slm_ entries have zero imaginary part), so T^\dagger = T^T. + +rotate_atompair_serial_abf: + unitary (isym < nsym_): TAT = T1^T * A * T2 + antiunitary (isym >= nsym_): TAT = conj(T1^T * A * T2) + = T1^T * conj(A) * T2 (T real) + +Test matrices: T = rotation by 90 deg about z in the real Y_l^1 basis +(m = -1,0,1 order): + + T = [ 0 -1 0 ] + [ 1 0 0 ] + [ 0 0 1 ] + + A (real 3x3) = [ 1 2 3 ] A_c (complex) = [ 1+i 2 3 ] + [ 4 5 6 ] [ 4 5-i 6 ] + [ 7 8 9 ] [ 7 8 9+i ] + + T^T * A * T = + [ 5 -4 6 ] + [ -2 1 -3 ] + [ 8 -7 9 ] + + T^T * A_c * T = + [ 5+0i -4+0i 6+0i ] + [ -2+0i 1+0i -3+0i ] + [ 8+0i -7+0i 9+1i ] + + (the entry (2,2) of A_c, 9+i, maps onto itself under this rotation) + +*/ + +// mocks: dummy constructors for classes pulled in by the headers +pseudo::pseudo() {} +pseudo::~pseudo() {} +Atom::Atom() {} +Atom::~Atom() {} +Atom_pseudo::Atom_pseudo() {} +Atom_pseudo::~Atom_pseudo() {} +UnitCell::UnitCell() {} +UnitCell::~UnitCell() {} +Magnetism::Magnetism() {} +Magnetism::~Magnetism() {} +SepPot::SepPot() {} +SepPot::~SepPot() {} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} + +namespace +{ +// real 3x3 rotation about z by 90 deg, l=1 real-spherical-harmonic basis +ModuleBase::ComplexMatrix make_T_rot90() +{ + ModuleBase::ComplexMatrix T(3, 3); + const double m[3][3] = {{0, -1, 0}, {1, 0, 0}, {0, 0, 1}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + T(i, j) = std::complex(m[i][j], 0.0); + return T; +} + +RI::Tensor make_A_real() +{ + RI::Tensor A({3, 3}); + const double v[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + A(i, j) = v[i][j]; + return A; +} + +RI::Tensor> make_A_complex() +{ + RI::Tensor> A({3, 3}); + const std::complex v[3][3] = { + {1.0 + 1i, 2.0, 3.0}, {4.0, 5.0 - 1i, 6.0}, {7.0, 8.0, 9.0 + 1i}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + A(i, j) = v[i][j]; + return A; +} + +// manual T^T * A * T (T real) +RI::Tensor expected_unitary() +{ + RI::Tensor E({3, 3}); + const double v[3][3] = {{5, -4, 6}, {-2, 1, -3}, {8, -7, 9}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + E(i, j) = v[i][j]; + return E; +} + +// manual T^T * A_c * T +RI::Tensor> expected_unitary_complex() +{ + RI::Tensor> E({3, 3}); + const std::complex v[3][3] = { + {5.0 - 1i, -4.0, 6.0}, {-2.0, 1.0 + 1i, -3.0}, {8.0, -7.0, 9.0 + 1i}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + E(i, j) = v[i][j]; + return E; +} + +template +bool tensor_close(const RI::Tensor& a, const RI::Tensor& b, double tol) +{ + if (a.shape.size() != b.shape.size()) + return false; + for (size_t d = 0; d < a.shape.size(); ++d) + if (a.shape[d] != b.shape[d]) + return false; + for (size_t i = 0; i < a.shape[0]; ++i) + for (size_t j = 0; j < a.shape[1]; ++j) + if (std::abs(a(i, j) - b(i, j)) > tol) + return false; + return true; +} +} // namespace + +class AbfRotationTest : public testing::Test +{ +protected: + void SetUp() override + { + // type 0: one l=1 ABF channel (3 functions); type 1: one l=0 (1 function) + symrot.set_Cs_rotation({{0, 1}, {1, 0}}); + ModuleSymmetry::SymmetryRotationTestAccess::set_operation_counts(symrot, 1, 1); + auto& rotmat = symrot.get_rotmat_Slm(); + rotmat.resize(2); + for (int isym = 0; isym < 2; ++isym) + { + rotmat[isym].resize(2); + // L=0 block: identity (real) + rotmat[isym][0] = ModuleBase::ComplexMatrix(1, 1); + rotmat[isym][0](0, 0) = 1.0; + // L=1 block: rotation by 90 deg about z (real in real-Y basis) + rotmat[isym][1] = make_T_rot90(); + } + } + ModuleSymmetry::Symmetry_rotation symrot; +}; + +TEST_F(AbfRotationTest, UnitaryRealTensor) +{ + // isym=0 < nsym_: TAT = T1^T * A * T2 (T real) + RI::Tensor A = make_A_real(); + RI::Tensor TAT = ModuleSymmetry::SymmetryRotationTestAccess::rotate_atompair_abf(symrot, A, 0, 0, 0); + EXPECT_TRUE(tensor_close(TAT, expected_unitary(), DOUBLETHRESHOLD)); +} + +TEST_F(AbfRotationTest, AntiunitaryRealTensor) +{ + // isym=1 >= nsym_: TAT = conj(T1^T * A * T2); for real A,T this equals + // the unitary result (conjugation is the identity on reals) - but the + // result must remain real, and must equal the manual expected matrix. + RI::Tensor A = make_A_real(); + RI::Tensor TAT = ModuleSymmetry::SymmetryRotationTestAccess::rotate_atompair_abf(symrot, A, 1, 0, 0); + EXPECT_TRUE(tensor_close(TAT, expected_unitary(), DOUBLETHRESHOLD)); + // explicitly verify no stray imaginary part is introduced + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + EXPECT_EQ(TAT(i, j), TAT(i, j)); // placeholder, real dtype +} + +TEST_F(AbfRotationTest, UnitaryComplexTensor) +{ + // complex A: unitary TAT = T1^T * A * T2, manual expected values + RI::Tensor> A = make_A_complex(); + RI::Tensor> TAT + = ModuleSymmetry::SymmetryRotationTestAccess::rotate_atompair_abf(symrot, A, 0, 0, 0); + EXPECT_TRUE(tensor_close(TAT, expected_unitary_complex(), DOUBLETHRESHOLD)); +} + +TEST_F(AbfRotationTest, AntiunitaryComplexTensor) +{ + // antiunitary: TAT = conj(T1^T * A * T2); manual expected = conj(unitary) + RI::Tensor> A = make_A_complex(); + RI::Tensor> TAT + = ModuleSymmetry::SymmetryRotationTestAccess::rotate_atompair_abf(symrot, A, 1, 0, 0); + RI::Tensor> E = expected_unitary_complex(); + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + EXPECT_NEAR(std::abs(TAT(i, j) - std::conj(E(i, j))), 0.0, DOUBLETHRESHOLD); + // contrast with the non-conjugated result: entry (2,2) must flip sign of Im + EXPECT_NEAR(TAT(2, 2).imag(), -1.0, DOUBLETHRESHOLD); +} + +TEST_F(AbfRotationTest, DifferentTypesT1T2) +{ + // type1=0 (l=1, T1 = 3x3 rot90), type2=1 (l=0, T2 = 1x1 identity) + // A is 3x1; expected = T1^T * A + RI::Tensor A({3, 1}); + A(0, 0) = 1; A(1, 0) = 2; A(2, 0) = 3; + RI::Tensor TAT = ModuleSymmetry::SymmetryRotationTestAccess::rotate_atompair_abf(symrot, A, 0, 0, 1); + // T^T * [1,2,3]^T = [2, -1, 3]^T + EXPECT_NEAR(TAT(0, 0), 2.0, DOUBLETHRESHOLD); + EXPECT_NEAR(TAT(1, 0), -1.0, DOUBLETHRESHOLD); + EXPECT_NEAR(TAT(2, 0), 3.0, DOUBLETHRESHOLD); +} + +TEST_F(AbfRotationTest, RestoreHRAbfStarMapping) +{ + // end-to-end restore_HR_abf: one irreducible entry (iat0,iat0,R=0) whose + // star contains two members: isym=0 -> (ap1=0,ap2=0,R=0) and + // isym=1 (antiunitary) -> (ap1=1,ap2=1,R=(1,0,0)). + std::vector iat2it = {0, 0}; + + std::map, RI::Tensor>> HR_irr; + RI::Tensor A = make_A_real(); + HR_irr[0][{0, {0, 0, 0}}] = A; + + ModuleSymmetry::TapR irapR = {{0, 0}, {0, 0, 0}}; + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[irapR] = { + {0, {{0, 0}, {0, 0, 0}}}, + {1, {{1, 1}, {1, 0, 0}}}, + }; + + ModuleSymmetry::Symmetry symm; + Atom atoms[2]; + auto HR_full = symrot.restore_HR_abf(symm, atoms, iat2it, HR_irr); + + // member 0: unitary -> T^T A T + EXPECT_TRUE(tensor_close(HR_full[0][{0, {0, 0, 0}}], expected_unitary(), DOUBLETHRESHOLD)); + // member 1: antiunitary -> conj(T^T A T) == T^T A T (real) + EXPECT_TRUE(tensor_close(HR_full[1][{1, {1, 0, 0}}], expected_unitary(), DOUBLETHRESHOLD)); + // no extra keys + EXPECT_EQ(HR_full.size(), 2u); + EXPECT_EQ(HR_full[0].size(), 1u); + EXPECT_EQ(HR_full[1].size(), 1u); +} + +TEST_F(AbfRotationTest, RestoreHRAbfInvalidShapeThrows) +{ + // an invalid (1-D) tensor must raise instead of being silently skipped + std::vector iat2it = {0}; + + std::map, RI::Tensor>> HR_irr; + RI::Tensor bad({3}); // not a valid matrix + HR_irr[0][{0, {0, 0, 0}}] = bad; + + ModuleSymmetry::TapR irapR = {{0, 0}, {0, 0, 0}}; + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[irapR] = {{0, {{0, 0}, {0, 0, 0}}}}; + + ModuleSymmetry::Symmetry symm; + Atom atoms[1]; + EXPECT_THROW(symrot.restore_HR_abf(symm, atoms, iat2it, HR_irr), std::runtime_error); +} + +TEST_F(AbfRotationTest, RestoreHRAbfDuplicateKeyThrows) +{ + // two star members mapping onto the same target key must raise + std::vector iat2it = {0}; + + std::map, RI::Tensor>> HR_irr; + RI::Tensor A = make_A_real(); + HR_irr[0][{0, {0, 0, 0}}] = A; + + ModuleSymmetry::TapR irapR = {{0, 0}, {0, 0, 0}}; + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[irapR] = { + {0, {{0, 0}, {0, 0, 0}}}, + {1, {{0, 0}, {0, 0, 0}}}, // duplicate target + }; + + ModuleSymmetry::Symmetry symm; + Atom atoms[1]; + EXPECT_THROW(symrot.restore_HR_abf(symm, atoms, iat2it, HR_irr), std::runtime_error); +} + +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); + MPI_Finalize(); + return result; +} diff --git a/source/source_lcao/module_ri/module_exx_symmetry/test/nspin4_restore_test.cpp b/source/source_lcao/module_ri/module_exx_symmetry/test/nspin4_restore_test.cpp new file mode 100644 index 00000000000..8ae0138e166 --- /dev/null +++ b/source/source_lcao/module_ri/module_exx_symmetry/test/nspin4_restore_test.cpp @@ -0,0 +1,396 @@ +#include "mpi.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "symm_rotation_test_access.h" +#define DOUBLETHRESHOLD 1e-8 + +/* + +Focused tests for restore_HR_nspin4: four spinor channels of the real-space +EXX H(R), short/long Coulomb channels each restored once, no channel +cross-talk for U = I, correct SU(2) mixing for non-trivial U, and the +antiunitary sigma_y (.)^* sigma_y channel remap (real AND complex inputs). + +COVERAGE BOUNDARY (honest): this suite tests the restore_HR_nspin4 helper +directly. The production caller chain Exx_LRI::cal_exx_elec_soc (per-Coulomb- +channel single restore, short/long accumulation) is NOT unit-tested here; it +depends on the full SCF/EXX environment and requires separate +SCF-level cross-feature validation. + +Conventions (from symmetry_rotation_R.hpp restore_HR_nspin4): + + step 1: each of the 4 irreducible channels is orbitally rotated with + rotate_atompair_serial(mode='H') -> T1^\dagger H T2 (T real here). + step 2: SU(2) mixing with U (Su2 = array): + Hout[a*2+b] = sum_{c,d} conj(U[c*2+a]) * U[d*2+b] * G[c*2+d] + step 3: antiunitary (isym >= nsym_): + out[0]=conj(in[3]); out[1]=-conj(in[2]); out[2]=-conj(in[1]); + out[3]=conj(in[0]) + +Test matrices (nw = 3; T = rot-90 about z, real Y_l^1 basis): + + T = [ 0 -1 0 ] A0 = [ 1 2 3 ] (channel 00) + [ 1 0 0 ] [ 4 5 6 ] + [ 0 0 1 ] [ 7 8 9 ] + + T^T * A0 * T = [ 5 -4 6 ] + [ -2 1 -3 ] + [ 8 -7 9 ] + + A1 (channel 01) = 2*A0 ; A2 (channel 10) = 3*A0 ; A3 (channel 11) = 4*A0 + (real; conjugation then acts as identity, keeping the remap check exact) + + Non-trivial SU(2): U = {a,b,-b,a} with a=b=1/sqrt(2) (real, 90 deg): + Hout[00] = 0.5*(G00 - G01 - G10 + G11) + Hout[01] = 0.5*(G00 + G01 - G10 - G11) +*/ + +// mocks +pseudo::pseudo() {} +pseudo::~pseudo() {} +Atom::Atom() {} +Atom::~Atom() {} +Atom_pseudo::Atom_pseudo() {} +Atom_pseudo::~Atom_pseudo() {} +UnitCell::UnitCell() {} +UnitCell::~UnitCell() {} +Magnetism::Magnetism() {} +Magnetism::~Magnetism() {} +SepPot::SepPot() {} +SepPot::~SepPot() {} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} + +namespace +{ +ModuleBase::ComplexMatrix make_T_rot90() +{ + ModuleBase::ComplexMatrix T(3, 3); + const double m[3][3] = {{0, -1, 0}, {1, 0, 0}, {0, 0, 1}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + T(i, j) = std::complex(m[i][j], 0.0); + return T; +} + +RI::Tensor> make_chan(const int scale) +{ + RI::Tensor> A({3, 3}); + const double v[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + A(i, j) = std::complex(scale * v[i][j], 0.0); + return A; +} + +// complex channel input: real part = scale*v, imaginary part = (scale+1)*0.25 +// for the diagonal, (scale+1)*0.5*i off-diagonal - all four channels differ +// in both real and imaginary parts. +RI::Tensor> make_chan_complex(const int scale) +{ + RI::Tensor> A({3, 3}); + const double v[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + A(i, j) = std::complex(scale * v[i][j], + (i == j ? 0.25 : 0.5) * (scale + 1)); + return A; +} + +// manual T^T * A * T for the scaled channel +RI::Tensor> expected_rotated(const int scale) +{ + RI::Tensor> E({3, 3}); + const double v[3][3] = {{5, -4, 6}, {-2, 1, -3}, {8, -7, 9}}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + E(i, j) = std::complex(scale * v[i][j], 0.0); + return E; +} + +template +bool tensor_close(const RI::Tensor& a, const RI::Tensor& b, double tol) +{ + if (a.shape.size() != b.shape.size()) + return false; + for (size_t d = 0; d < a.shape.size(); ++d) + if (a.shape[d] != b.shape[d]) + return false; + for (size_t i = 0; i < a.shape[0]; ++i) + for (size_t j = 0; j < a.shape[1]; ++j) + if (std::abs(a(i, j) - b(i, j)) > tol) + return false; + return true; +} +} // namespace + +class Nspin4RestoreTest : public testing::Test +{ +protected: + void SetUp() override + { + symrot.set_Cs_rotation({}); + ModuleSymmetry::SymmetryRotationTestAccess::set_operation_counts(symrot, 1, 1); + auto& rotmat = symrot.get_rotmat_Slm(); + rotmat.resize(2); + for (int isym = 0; isym < 2; ++isym) + { + rotmat[isym].resize(2); + rotmat[isym][0] = ModuleBase::ComplexMatrix(1, 1); + rotmat[isym][0](0, 0) = 1.0; + rotmat[isym][1] = make_T_rot90(); + } + // identity SU(2) on both operations by default + auto& spin_rotations = ModuleSymmetry::SymmetryRotationTestAccess::spin_rotations(symrot); + spin_rotations.resize(2); + for (int isym = 0; isym < 2; ++isym) + spin_rotations[isym] = ModuleSymmetry::SpinRotation::Su2{1.0, 0.0, 0.0, 1.0}; + + atoms[0].nw = 3; + atoms[1].nw = 3; + atoms[0].label = "H"; + atoms[1].label = "H"; + // one l=1 shell: iw2l = {1,1,1} so set_rotation_matrix builds the 3x3 block + atoms[0].iw2l = {1, 1, 1}; + atoms[1].iw2l = {1, 1, 1}; + + } + + std::array, RI::Tensor>>>, 4> + make_irr_input() + { + std::array, RI::Tensor>>>, 4> in; + for (int is = 0; is < 4; ++is) + in[is][0][{0, {0, 0, 0}}] = make_chan(is + 1); + return in; + } + + ModuleSymmetry::Symmetry_rotation symrot; + Atom atoms[2]; + std::vector iat2it = {0, 0}; +}; + +TEST_F(Nspin4RestoreTest, FourChannelsIndependentForIdentitySU2) +{ + // star with a single unitary member: 4 channels restored independently, + // each equal to T^T * A_channel * T - no spin mixing, no cross-talk. + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = {{0, {{0, 0}, {0, 0, 0}}}}; + + auto in = make_irr_input(); + ModuleSymmetry::Symmetry symm; + auto out = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in); + + for (int is = 0; is < 4; ++is) + { + EXPECT_EQ(out[is].size(), 1u); + EXPECT_TRUE(tensor_close(out[is][0][{0, {0, 0, 0}}], expected_rotated(is + 1), DOUBLETHRESHOLD)); + } +} + +TEST_F(Nspin4RestoreTest, OffDiagonalChannelsNonzero) +{ + // all four channels (including 01 and 10) are nonzero and distinct in + // both the input and the restored output. + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = {{0, {{0, 0}, {0, 0, 0}}}}; + auto in = make_irr_input(); + ModuleSymmetry::Symmetry symm; + auto out = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in); + for (int is = 0; is < 4; ++is) + { + const auto& H = out[is][0][{0, {0, 0, 0}}]; + bool nonzero = false; + for (size_t i = 0; i < 3 && !nonzero; ++i) + for (size_t j = 0; j < 3 && !nonzero; ++j) + nonzero = std::abs(H(i, j)) > 1e-6; + EXPECT_TRUE(nonzero) << "channel " << is << " became zero"; + } +} + +TEST_F(Nspin4RestoreTest, NonTrivialSU2Mixing) +{ + // U = {a,b,-b,a} with a=b=1/sqrt(2). From + // Hout[a*2+b] = sum_{c,d} conj(U[c*2+a]) * U[d*2+b] * G[c*2+d]: + // Hout[00] = 0.5*(G00 - G01 - G10 + G11) + // Hout[01] = 0.5*(G00 + G01 - G10 - G11) + ModuleSymmetry::SymmetryRotationTestAccess::spin_rotations(symrot)[0] = ModuleSymmetry::SpinRotation::Su2{M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2}; + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = {{0, {{0, 0}, {0, 0, 0}}}}; + + auto in = make_irr_input(); + ModuleSymmetry::Symmetry symm; + auto out = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in); + + // G[is] = T^T * (scale is+1) * A0 * T = (is+1) * expected_rotated(1) + RI::Tensor> G0 = expected_rotated(1); + RI::Tensor> G1 = expected_rotated(2); + RI::Tensor> G2 = expected_rotated(3); + RI::Tensor> G3 = expected_rotated(4); + RI::Tensor> E00({3, 3}); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + E00(i, j) = 0.5 * (G0(i, j) - G1(i, j) - G2(i, j) + G3(i, j)); + EXPECT_TRUE(tensor_close(out[0][0][{0, {0, 0, 0}}], E00, DOUBLETHRESHOLD)); + RI::Tensor> E01({3, 3}); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + E01(i, j) = 0.5 * (G0(i, j) + G1(i, j) - G2(i, j) - G3(i, j)); + EXPECT_TRUE(tensor_close(out[1][0][{0, {0, 0, 0}}], E01, DOUBLETHRESHOLD)); +} + +TEST_F(Nspin4RestoreTest, AntiunitarySigmaYChannelRemap) +{ + // star member isym=1 is antiunitary (>= nsym_): after the (identity) + // SU(2) step, channels remap as out[0]=conj(in[3]), out[1]=-conj(in[2]), + // out[2]=-conj(in[1]), out[3]=conj(in[0]). + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = { + {0, {{0, 0}, {0, 0, 0}}}, + {1, {{1, 1}, {1, 0, 0}}}, + }; + + auto in = make_irr_input(); + ModuleSymmetry::Symmetry symm; + auto out = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in); + + // member 0 (unitary): independent channels + for (int is = 0; is < 4; ++is) + EXPECT_TRUE(tensor_close(out[is][0][{0, {0, 0, 0}}], expected_rotated(is + 1), DOUBLETHRESHOLD)); + // member 1 (antiunitary): sigma_y K remap, real input -> conjugation identity + RI::Tensor> neg3 = expected_rotated(3); + RI::Tensor> neg2 = expected_rotated(2); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + { + neg3(i, j) = -neg3(i, j); + neg2(i, j) = -neg2(i, j); + } + EXPECT_TRUE(tensor_close(out[0][1][{1, {1, 0, 0}}], expected_rotated(4), DOUBLETHRESHOLD)); // 00 <- +conj(11) + EXPECT_TRUE(tensor_close(out[1][1][{1, {1, 0, 0}}], neg3, DOUBLETHRESHOLD)); // 01 <- -conj(10) + EXPECT_TRUE(tensor_close(out[2][1][{1, {1, 0, 0}}], neg2, DOUBLETHRESHOLD)); // 10 <- -conj(01) + EXPECT_TRUE(tensor_close(out[3][1][{1, {1, 0, 0}}], expected_rotated(1), DOUBLETHRESHOLD)); // 11 <- +conj(00) +} + +TEST_F(Nspin4RestoreTest, AntiunitarySigmaYComplexInputs) +{ + // complex inputs: the antiunitary branch must conjugate elementwise AND + // apply the channel remap with sign flips. With U = I: + // out[0] = conj(rot(A3)), out[1] = -conj(rot(A2)), + // out[2] = -conj(rot(A1)), out[3] = conj(rot(A0)) + // where rot(Ak) = T^T * A_k * T is the orbitally rotated channel. + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = { + {0, {{0, 0}, {0, 0, 0}}}, + {1, {{1, 1}, {1, 0, 0}}}, + }; + + std::array, RI::Tensor>>>, 4> in; + for (int is = 0; is < 4; ++is) + in[is][0][{0, {0, 0, 0}}] = make_chan_complex(is + 1); + + ModuleSymmetry::Symmetry symm; + auto out = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in); + + // unitary member (isym=0): channels pass through unchanged (no conj) + for (int is = 0; is < 4; ++is) + { + // rot(Ak) computed manually: T^T A T with T = rot-90; for A_k with + // entries a_ij: (T^T A T)_00 = a_11, (T^T A T)_01 = -a_10, + // (T^T A T)_02 = a_12, (T^T A T)_10 = -a_01, (T^T A T)_11 = a_00, + // (T^T A T)_12 = -a_02, (T^T A T)_20 = a_21, (T^T A T)_21 = -a_20, + // (T^T A T)_22 = a_22 + RI::Tensor> E({3, 3}); + const auto& A = in[is][0][{0, {0, 0, 0}}]; + E(0, 0) = A(1, 1); E(0, 1) = -A(1, 0); E(0, 2) = A(1, 2); + E(1, 0) = -A(0, 1); E(1, 1) = A(0, 0); E(1, 2) = -A(0, 2); + E(2, 0) = A(2, 1); E(2, 1) = -A(2, 0); E(2, 2) = A(2, 2); + const bool ok_u = tensor_close(out[is][0][{0, {0, 0, 0}}], E, DOUBLETHRESHOLD); + EXPECT_TRUE(ok_u) << "unitary member channel " << is; + } + // antiunitary member (isym=1): conjugate + remap + sign + for (int is = 0; is < 4; ++is) + { + // out[0] <- +conj(rot(A3)); out[1] <- -conj(rot(A2)); + // out[2] <- -conj(rot(A1)); out[3] <- +conj(rot(A0)) + const int src[4] = {3, 2, 1, 0}; + const bool neg[4] = {false, true, true, false}; + // rotate channel src[is] (input index src[is] -> make_chan_complex(src[is]+1)) + const auto& Asrc = in[src[is]][0][{0, {0, 0, 0}}]; + RI::Tensor> rotc({3, 3}); + rotc(0, 0) = Asrc(1, 1); rotc(0, 1) = -Asrc(1, 0); rotc(0, 2) = Asrc(1, 2); + rotc(1, 0) = -Asrc(0, 1); rotc(1, 1) = Asrc(0, 0); rotc(1, 2) = -Asrc(0, 2); + rotc(2, 0) = Asrc(2, 1); rotc(2, 1) = -Asrc(2, 0); rotc(2, 2) = Asrc(2, 2); + RI::Tensor> E({3, 3}); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + E(i, j) = neg[is] ? -std::conj(rotc(i, j)) : std::conj(rotc(i, j)); + const bool ok_a = tensor_close(out[is][1][{1, {1, 0, 0}}], E, DOUBLETHRESHOLD); + EXPECT_TRUE(ok_a) << "antiunitary member channel " << is; + } + // explicit sanity: out[3] <- +conj(rot(A0)) (src[3]=0, neg[3]=false). + // rot(A0)(0,0) = A0(1,1) = 5 + 0.5i -> conj -> 5 - 0.5i + const auto& out3 = out[3][1][{1, {1, 0, 0}}]; + EXPECT_NEAR(out3(0, 0).real(), 5.0, DOUBLETHRESHOLD); + EXPECT_NEAR(out3(0, 0).imag(), -0.5, DOUBLETHRESHOLD); +} + +TEST_F(Nspin4RestoreTest, ShortAndLongRestoreOnceEach) +{ + // short and long Coulomb channels are restored by two separate calls; + // each call restores all four spin channels exactly once. Verify by + // running two independent calls and checking per-call output size. + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = {{0, {{0, 0}, {0, 0, 0}}}}; + auto in_short = make_irr_input(); + auto in_long = make_irr_input(); + // long channel carries different values to prove no cross-talk between calls + for (int is = 0; is < 4; ++is) + in_long[is][0][{0, {0, 0, 0}}] = make_chan(10 * (is + 1)); + + ModuleSymmetry::Symmetry symm; + auto out_short = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in_short); + auto out_long = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in_long); + + for (int is = 0; is < 4; ++is) + { + EXPECT_TRUE(tensor_close(out_short[is][0][{0, {0, 0, 0}}], expected_rotated(is + 1), DOUBLETHRESHOLD)); + EXPECT_TRUE(tensor_close(out_long[is][0][{0, {0, 0, 0}}], expected_rotated(10 * (is + 1)), DOUBLETHRESHOLD)); + } +} + +TEST_F(Nspin4RestoreTest, HermiticityPreservedForUnitaryMember) +{ + // Hermitian input channel 00 -> Hermitian output under the unitary member + // (T real orthogonal, U=I: conjugation-free path). + RI::Tensor> H({3, 3}); + H(0, 0) = std::complex(2, 0); H(0, 1) = std::complex(1, 1); H(0, 2) = std::complex(3, -2); + H(1, 0) = std::conj(H(0, 1)); H(1, 1) = std::complex(4, 0); H(1, 2) = std::complex(0.5, 0.25); + H(2, 0) = std::conj(H(0, 2)); H(2, 1) = std::conj(H(1, 2)); H(2, 2) = std::complex(6, 0); + + ModuleSymmetry::SymmetryRotationTestAccess::sector_stars(symrot)[{{0, 0}, {0, 0, 0}}] = {{0, {{0, 0}, {0, 0, 0}}}}; + std::array, RI::Tensor>>>, 4> in; + in[0][0][{0, {0, 0, 0}}] = H; + + ModuleSymmetry::Symmetry symm; + auto out = symrot.restore_HR_nspin4(symm, atoms, iat2it, 'H', in); + + const auto& Hout = out[0][0][{0, {0, 0, 0}}]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + EXPECT_NEAR(std::abs(Hout(i, j) - std::conj(Hout(j, i))), 0.0, DOUBLETHRESHOLD); +} + +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); + MPI_Finalize(); + return result; +} diff --git a/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test.cpp b/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test.cpp index 73ca3b68752..882dacddf1d 100644 --- a/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test.cpp +++ b/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test.cpp @@ -2,6 +2,7 @@ #include "../symm_rotation.h" #include "source_cell/unitcell.h" #include "gtest/gtest.h" +#include #define DOUBLETHRESHOLD 1e-8 /* @@ -128,6 +129,25 @@ TEST_F(SymmetryRotationTest, RotMat) } } +TEST_F(SymmetryRotationTest, RotMatHighLIdentityFinite) +{ + ModuleBase::Matrix3 identity(1, 0, 0, 0, 1, 0, 0, 0, 1); + symrot.cal_rotmat_Slm(&identity, 8, -1); + ModuleBase::ComplexMatrix& rotmat = symrot.get_rotmat_Slm()[0][8]; + const int dim = 2 * 8 + 1; + for (int i = 0; i < dim; ++i) + { + for (int j = 0; j < dim; ++j) + { + EXPECT_TRUE(std::isfinite(rotmat(i, j).real())); + EXPECT_TRUE(std::isfinite(rotmat(i, j).imag())); + const double target = (i == j) ? 1.0 : 0.0; + EXPECT_NEAR(rotmat(i, j).real(), target, 1e-8); + EXPECT_NEAR(rotmat(i, j).imag(), 0.0, 1e-8); + } + } +} + TEST_F(SymmetryRotationTest, GetReturnLattice) { ModuleBase::Vector3 posd_a1(1. / 3., 1. / 3., 0.2); diff --git a/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test_access.h b/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test_access.h new file mode 100644 index 00000000000..3e2d695742d --- /dev/null +++ b/source/source_lcao/module_ri/module_exx_symmetry/test/symm_rotation_test_access.h @@ -0,0 +1,39 @@ +#ifndef ABACUS_SOURCE_LCAO_MODULE_RI_MODULE_EXX_SYMMETRY_TEST_SYMM_ROTATION_TEST_ACCESS_H +#define ABACUS_SOURCE_LCAO_MODULE_RI_MODULE_EXX_SYMMETRY_TEST_SYMM_ROTATION_TEST_ACCESS_H + +#include "../symm_rotation.h" + +namespace ModuleSymmetry +{ + // Keeps synthetic symmetry state confined to focused rotation unit tests. + class SymmetryRotationTestAccess + { + public: + static void set_operation_counts(Symmetry_rotation& rotation, const int nsym, const int nanti) + { + rotation.nsym_ = nsym; + rotation.nanti_ = nanti; + } + + static std::vector& spin_rotations(Symmetry_rotation& rotation) + { + return rotation.spin_U_; + } + + static std::map>& sector_stars(Symmetry_rotation& rotation) + { + return rotation.irs_.sector_stars_; + } + + template + static RI::Tensor rotate_atompair_abf(const Symmetry_rotation& rotation, + const RI::Tensor& input, + const int isym, + const int type1, + const int type2) + { + return rotation.rotate_atompair_serial_abf(input, isym, type1, type2); + } + }; +} +#endif diff --git a/source/source_lcao/module_ri/ri_2d_comm.hpp b/source/source_lcao/module_ri/ri_2d_comm.hpp index 2d8a6294b08..86829cfa69d 100644 --- a/source/source_lcao/module_ri/ri_2d_comm.hpp +++ b/source/source_lcao/module_ri/ri_2d_comm.hpp @@ -367,7 +367,7 @@ void RI_2D_Comm::add_HexxR( // This is the real-space (DM(R)) counterpart of split_m2D_ktoR (which converts DM(k)), // and the inverse of add_HexxR (which converts Ds -> HContainer); it therefore reuses the // exact same atom/orbital/spin-block index mapping as add_HexxR. -// dm_container is the DM(R) vector returned by DensityMatrix::get_dmr_vec(): +// dm_container is the DM(R) vector returned by DensityMatrix::get_DMR_vector(): // nspin==1 : size 1 (container 0 -> spin-block 0) // nspin==2 : size 2 (container is -> spin-block is) // nspin==4 : size 1 (container 0 holds the 2x2 npol blocks -> spin-blocks 0,1,2,3) diff --git a/source/source_lcao/module_ri/singular_value.cpp b/source/source_lcao/module_ri/singular_value.cpp index 3f81bb662d9..9cff69f8203 100644 --- a/source/source_lcao/module_ri/singular_value.cpp +++ b/source/source_lcao/module_ri/singular_value.cpp @@ -17,6 +17,7 @@ #include #include +#include #include namespace Singular_Value @@ -235,6 +236,91 @@ double cal_massidda(const UnitCell& ucell, return val_extra; } +double fq_massidda_2d(const UnitCell& ucell, const std::array& nmp, const double& eta) +{ + ModuleBase::Vector3 b1(ucell.G.e11 / nmp[0], ucell.G.e12 / nmp[0], ucell.G.e13 / nmp[0]); + ModuleBase::Vector3 b2(ucell.G.e21 / nmp[1], ucell.G.e22 / nmp[1], ucell.G.e23 / nmp[1]); + b1 = b1 * ucell.tpiba; + b2 = b2 * ucell.tpiba; + + const double cutoff = std::sqrt(35.0 / eta); + const int n0 = static_cast(std::ceil(cutoff / b1.norm())); + const int n1 = static_cast(std::ceil(cutoff / b2.norm())); + + double fq = 0.0; + for (int i0 = -n0; i0 <= n0; ++i0) + { + for (int i1 = -n1; i1 <= n1; ++i1) + { + if (i0 == 0 && i1 == 0) + { + continue; + } + const ModuleBase::Vector3 qg = b1 * static_cast(i0) + b2 * static_cast(i1); + const double q = std::sqrt(qg.x * qg.x + qg.y * qg.y); + fq += std::exp(-eta * q * q) / q; + } + } + + return fq; +} + +double cal_massidda_2d(const UnitCell& ucell, + const std::array& nmp, + const double& eta, + const int& niter, + const double& eps) +{ + ModuleBase::TITLE("Singular_Value", "cal_massidda_2d"); + ModuleBase::timer::start("Singular_Value", "cal_massidda_2d"); + + if (nmp[2] != 1) + { + ModuleBase::WARNING_QUIT("Singular_Value::cal_massidda_2d", "2D Ewald requires a single k point along z"); + } + + const ModuleBase::Vector3 a1_bohr = ucell.a1 * ucell.lat0; + const ModuleBase::Vector3 a2_bohr = ucell.a2 * ucell.lat0; + const double area_parallel = (a1_bohr ^ a2_bohr).norm(); + const int nks = nmp[0] * nmp[1]; + + auto cal_chi = [&ucell, &nmp, &area_parallel, &nks](const double& eta_now) { + const double fq_sum = fq_massidda_2d(ucell, nmp, eta_now); + const double fq_int = area_parallel / (4.0 * std::sqrt(ModuleBase::PI * eta_now)); + return fq_int * nks - fq_sum; + }; + + const int max_iter = std::max(niter, 16); + int tot_iter = 0; + double val_extra_old = 0.5 * std::numeric_limits::max(); + double eta_old = eta; + double val_old = cal_chi(eta_old); + double val_extra = val_old; + for (int iter = 0; iter != max_iter; ++iter) + { + const double eta_new = eta_old * 0.5; + const double val_new = cal_chi(eta_new); + const double dval = (val_new - val_old) / (eta_new - eta_old); + val_extra = val_new + dval * (0.0 - eta_new); + if (std::abs(val_extra - val_extra_old) < eps) + { + break; + } + eta_old = eta_new; + val_old = val_new; + val_extra_old = val_extra; + tot_iter += 1; + } + + if (tot_iter == max_iter) + { + ModuleBase::WARNING_QUIT("Singular_Value::cal_massidda_2d", "not converged!"); + } + + ModuleBase::timer::end("Singular_Value", "cal_massidda_2d"); + return val_extra; +} + double Iter_Integral(const ModuleBase::Matrix3& G, const T_cal_fq_type& func_cal_fq, const std::array& nq_arr, @@ -300,4 +386,4 @@ double Iter_Integral(const ModuleBase::Matrix3& G, } } // namespace Singular_Value -#endif \ No newline at end of file +#endif diff --git a/source/source_lcao/module_ri/singular_value.h b/source/source_lcao/module_ri/singular_value.h index 8318b807273..d0bb3d8c2f0 100644 --- a/source/source_lcao/module_ri/singular_value.h +++ b/source/source_lcao/module_ri/singular_value.h @@ -11,6 +11,7 @@ #include "source_cell/klist.h" #include +#include #include namespace Singular_Value @@ -38,6 +39,11 @@ double cal_massidda(const UnitCell& ucell, const double& start_lambda, const int& niter, const double& eps); +double cal_massidda_2d(const UnitCell& ucell, + const std::array& nmp, + const double& eta, + const int& niter, + const double& eps); double solve_chi(const ModuleBase::Matrix3& G, const std::vector>& kvec_c, @@ -75,6 +81,7 @@ double fq_massidda(const double& tpiba, const int& qdiv, const double& lambda, const int& lmax); +double fq_massidda_2d(const UnitCell& ucell, const std::array& nmp, const double& eta); }; // namespace Singular_Value -#endif \ No newline at end of file +#endif diff --git a/source/source_lcao/module_ri/test_code/scalapack_test.cpp b/source/source_lcao/module_ri/test_code/scalapack_test.cpp index bbc8453835b..6dc33e2c8d2 100644 --- a/source/source_lcao/module_ri/test_code/scalapack_test.cpp +++ b/source/source_lcao/module_ri/test_code/scalapack_test.cpp @@ -1,7 +1,7 @@ #ifdef __MPI #include #endif -#include "source_base/scalapack-connector.h" +#include "source_base/module_external/scalapack_connector.h" #include "source_base/global_function.h" void test_pblas() diff --git a/source/source_lcao/module_ri/unittests/CMakeLists.txt b/source/source_lcao/module_ri/unittests/CMakeLists.txt index 8ab5affa5f7..c32baf0d43e 100644 --- a/source/source_lcao/module_ri/unittests/CMakeLists.txt +++ b/source/source_lcao/module_ri/unittests/CMakeLists.txt @@ -25,3 +25,67 @@ AddTest( ) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/support" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") +add_executable(MODULE_RI_ewald_tail_utils_test test_ewald_tail_utils.cpp) +install(TARGETS MODULE_RI_ewald_tail_utils_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RI_ewald_tail_utils_test + COMMAND ./MODULE_RI_ewald_tail_utils_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_executable(MODULE_RI_gaussian_abfs_cutoff_test test_gaussian_abfs_cutoff.cpp) +install(TARGETS MODULE_RI_gaussian_abfs_cutoff_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RI_gaussian_abfs_cutoff_test + COMMAND ./MODULE_RI_gaussian_abfs_cutoff_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_executable(MODULE_RI_gaussian_abfs_2d_integrals_test test_gaussian_abfs_2d_integrals.cpp) +install(TARGETS MODULE_RI_gaussian_abfs_2d_integrals_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RI_gaussian_abfs_2d_integrals_test + COMMAND ./MODULE_RI_gaussian_abfs_2d_integrals_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_executable(MODULE_RI_ewald_gaussian_cutoff_test test_ewald_gaussian_cutoff.cpp) +install(TARGETS MODULE_RI_ewald_gaussian_cutoff_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RI_ewald_gaussian_cutoff_test + COMMAND ./MODULE_RI_ewald_gaussian_cutoff_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_executable( + MODULE_RI_direct_2d_coulomb_test + test_direct_2d_coulomb.cpp + ../direct_2d_coulomb.cpp +) +target_include_directories(MODULE_RI_direct_2d_coulomb_test PRIVATE ${CMAKE_SOURCE_DIR}/source) +target_link_libraries(MODULE_RI_direct_2d_coulomb_test PRIVATE base device parameter abacus::linalg_libs) +install(TARGETS MODULE_RI_direct_2d_coulomb_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RI_direct_2d_coulomb_test + COMMAND ./MODULE_RI_direct_2d_coulomb_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_executable( + MODULE_RI_direct_3d_coulomb_test + test_direct_3d_coulomb.cpp + ../direct_2d_coulomb.cpp +) +target_include_directories(MODULE_RI_direct_3d_coulomb_test PRIVATE ${CMAKE_SOURCE_DIR}/source) +target_link_libraries(MODULE_RI_direct_3d_coulomb_test PRIVATE base device parameter abacus::linalg_libs) +install(TARGETS MODULE_RI_direct_3d_coulomb_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RI_direct_3d_coulomb_test + COMMAND ./MODULE_RI_direct_3d_coulomb_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +if(ENABLE_MPI) + add_executable(MODULE_RI_ewald_mpi_distribution_test test_ewald_mpi_distribution.cpp) + target_link_libraries(MODULE_RI_ewald_mpi_distribution_test PRIVATE MPI::MPI_CXX) + install(TARGETS MODULE_RI_ewald_mpi_distribution_test DESTINATION ${CMAKE_BINARY_DIR}/tests) + add_test( + NAME MODULE_RI_ewald_mpi_distribution_parallel_test + COMMAND mpirun -np 4 ./MODULE_RI_ewald_mpi_distribution_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) +endif() diff --git a/source/source_lcao/module_ri/unittests/test_direct_2d_coulomb.cpp b/source/source_lcao/module_ri/unittests/test_direct_2d_coulomb.cpp new file mode 100644 index 00000000000..0e48ac916c9 --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_direct_2d_coulomb.cpp @@ -0,0 +1,181 @@ +#include "../direct_2d_coulomb.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + throw std::runtime_error(message); + } +} + +double relative_frobenius(const Direct2dCoulomb::DenseMatrix& lhs, + const Direct2dCoulomb::DenseMatrix& rhs) +{ + require(lhs.dimension == rhs.dimension, "matrix dimensions differ"); + double numerator = 0.0; + double denominator = 0.0; + for (std::size_t i = 0; i < lhs.values.size(); ++i) + { + numerator += std::norm(lhs.values[i] - rhs.values[i]); + denominator += std::norm(rhs.values[i]); + } + return std::sqrt(numerator / denominator); +} + +void require_positive_quadratic_forms(const Direct2dCoulomb::DenseMatrix& matrix) +{ + const std::vector>> probes = { + {{1.0, 0.0}, {0.0, 0.0}}, + {{0.0, 0.0}, {1.0, 0.0}}, + {{1.0, 0.0}, {1.0, 0.0}}, + {{1.0, 0.0}, {0.0, 1.0}}, + {{0.25, -0.75}, {-0.4, 0.2}}, + }; + for (const auto& vector: probes) + { + std::complex value = 0.0; + for (std::size_t i = 0; i < matrix.dimension; ++i) + { + for (std::size_t j = 0; j < matrix.dimension; ++j) + { + value += std::conj(vector[i]) * matrix(i, j) * vector[j]; + } + } + require(std::abs(value.imag()) < 1.0e-11, "quadratic form is not real"); + require(value.real() > -1.0e-11, "quadratic form is negative"); + } +} +} // namespace + +int main() +{ + try + { + const auto quadrature = Direct2dCoulomb::gauss_legendre_interval(32, -3.0, 3.0); + double weight_sum = 0.0; + for (const auto& node: quadrature) + { + require(node.weight_bohr > 0.0, "kz quadrature contains a non-positive weight"); + weight_sum += node.weight_bohr; + } + require(std::abs(weight_sum - 6.0) < 1.0e-13, + "kz quadrature does not integrate a constant"); + + ModuleBase::Matrix3 reciprocal; + reciprocal.e11 = 1.0; + reciprocal.e22 = 1.0; + reciprocal.e33 = 0.1; + const ModuleBase::Vector3 q_direct(0.125, 0.0, 0.0); + constexpr double tpiba = 1.0; + constexpr double ecut_ry = 36.0; + const auto lines = Direct2dCoulomb::enumerate_reciprocal_lines( + reciprocal, q_direct, tpiba, ecut_ry); + require(!lines.empty(), "2D reciprocal-line enumeration is empty"); + for (const auto& line: lines) + { + require(std::abs(line.q_plus_g_parallel_cart.z) < 1.0e-15, + "2D reciprocal-line enumeration retained a Gz image"); + require(line.kz_max_bohr > 0.0, "2D reciprocal line has no kz interval"); + } + + constexpr std::size_t radial_size = 1201; + constexpr double dk = 0.01; + std::vector radial_s(radial_size); + std::vector radial_p(radial_size); + for (std::size_t i = 0; i < radial_size; ++i) + { + const double k = dk * static_cast(i); + radial_s[i] = std::exp(-0.3 * k * k); + radial_p[i] = k * std::exp(-0.25 * k * k); + } + const std::vector basis = { + {{0, dk, radial_s.data(), radial_s.size()}, 0, 0, {0.0, 0.0, 0.0}}, + {{1, dk, radial_p.data(), radial_p.size()}, 0, 1, {0.17, 0.11, 0.23}}, + }; + + constexpr double omega = 100.0; + constexpr double lz_bohr = 10.0; + const auto points64 = Direct2dCoulomb::expand_mixed_fourier_points(lines, tpiba, 64); + const auto points128 = Direct2dCoulomb::expand_mixed_fourier_points(lines, tpiba, 128); + const auto points256 = Direct2dCoulomb::expand_mixed_fourier_points(lines, tpiba, 256); + const auto matrix64 = Direct2dCoulomb::build_gram_matrix(basis, points64, omega, lz_bohr, 1.0); + const auto matrix128 = Direct2dCoulomb::build_gram_matrix(basis, points128, omega, lz_bohr, 1.0); + const auto matrix256 = Direct2dCoulomb::build_gram_matrix(basis, points256, omega, lz_bohr, 1.0); + require(matrix128.hermitian_residual < 1.0e-12, "direct Gram matrix is not Hermitian"); + require_positive_quadratic_forms(matrix128.matrix); + require(relative_frobenius(matrix128.matrix, matrix256.matrix) < 2.0e-10, + "128-point kz quadrature is not converged"); + require(relative_frobenius(matrix64.matrix, matrix256.matrix) < 2.0e-6, + "64-point kz quadrature is not within the WS2-tested tolerance"); + + const std::array nmp{12, 12, 1}; + constexpr double massidda_chi = 3.25; + const auto gamma_plane = Direct2dCoulomb::gamma_plane_quadrature( + reciprocal, tpiba, nmp, 8, massidda_chi); + double gamma_weight_sum = 0.0; + double gamma_inverse_q_average = 0.0; + for (const auto& node: gamma_plane) + { + require(node.weight > 0.0, "Gamma-plane quadrature contains a non-positive weight"); + require(node.q_bohr > 0.0, "Gamma-plane quadrature contains q=0"); + gamma_weight_sum += node.weight; + gamma_inverse_q_average += node.weight / node.q_bohr; + } + require(std::abs(gamma_weight_sum - 1.0) < 2.0e-14, + "Gamma-plane quadrature is not normalized"); + require(std::abs(gamma_inverse_q_average - massidda_chi) < 2.0e-13, + "Gamma-plane quadrature does not reproduce the Massidda average"); + + Direct2dCoulomb::MethodMetadata metadata; + metadata.method = "direct_mixed_fourier"; + metadata.ecut_ry = 110.0; + metadata.kz_order = 64; + metadata.gamma_order = 8; + metadata.nq = 144; + metadata.naux = 807; + metadata.source_revision = "test-revision"; + const std::string formatted = Direct2dCoulomb::format_method_metadata(metadata); + const std::string expected_metadata = + "# ABACUS reader-v1 strict 2D Coulomb method\n" + "version = 1\n" + "method = direct_mixed_fourier\n" + "ecut_ry = 110\n" + "kz_order = 64\n" + "gamma_order = 8\n" + "nq = 144\n" + "naux = 807\n" + "source_revision = test-revision\n"; + require(formatted == expected_metadata, "method metadata is not deterministic"); + require(formatted.find("method = direct_mixed_fourier") != std::string::npos, + "metadata omits method"); + require(formatted.find("ecut_ry = 110") != std::string::npos, + "metadata omits cutoff"); + require(formatted.find("kz_order = 64") != std::string::npos, + "metadata omits kz order"); + require(formatted.find("gamma_order = 8") != std::string::npos, + "metadata omits Gamma order"); + require(formatted.find("nq = 144") != std::string::npos, + "metadata omits q count"); + require(formatted.find("naux = 807") != std::string::npos, + "metadata omits auxiliary dimension"); + + std::cout << "direct strict-2D Coulomb tests passed" << std::endl; + } + catch (const std::exception& error) + { + std::cerr << error.what() << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/source/source_lcao/module_ri/unittests/test_direct_3d_coulomb.cpp b/source/source_lcao/module_ri/unittests/test_direct_3d_coulomb.cpp new file mode 100644 index 00000000000..b01e14d17a7 --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_direct_3d_coulomb.cpp @@ -0,0 +1,156 @@ +#include "../direct_2d_coulomb.h" + +#include "source_base/constants.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + throw std::runtime_error(message); + } +} + +void require_close(const double actual, + const double expected, + const double tolerance, + const char* message) +{ + if (std::abs(actual - expected) > tolerance) + { + throw std::runtime_error(message); + } +} + +void require_positive_quadratic_forms(const Direct2dCoulomb::DenseMatrix& matrix) +{ + const std::vector>> probes = { + {{1.0, 0.0}, {0.0, 0.0}}, + {{0.0, 0.0}, {1.0, 0.0}}, + {{1.0, 0.0}, {1.0, 0.0}}, + {{1.0, 0.0}, {0.0, 1.0}}, + {{0.25, -0.75}, {-0.4, 0.2}}, + }; + for (const auto& vector: probes) + { + std::complex value = 0.0; + for (std::size_t i = 0; i < matrix.dimension; ++i) + { + for (std::size_t j = 0; j < matrix.dimension; ++j) + { + value += std::conj(vector[i]) * matrix(i, j) * vector[j]; + } + } + require(std::abs(value.imag()) < 1.0e-11, "3D quadratic form is not real"); + require(value.real() > -1.0e-11, "3D quadratic form is negative"); + } +} +} // namespace + +int main() +{ + try + { + ModuleBase::Matrix3 reciprocal; + reciprocal.e11 = 1.0; + reciprocal.e22 = 1.0; + reciprocal.e33 = 1.0; + constexpr double tpiba = 1.0; + constexpr double ecut_ry = 1.0; + const ModuleBase::Vector3 gamma(0.0, 0.0, 0.0); + const auto gamma_points = Direct2dCoulomb::enumerate_reciprocal_points( + reciprocal, gamma, tpiba, ecut_ry); + require(gamma_points.size() == 7, + "cubic 3D reciprocal enumeration must contain G=0 and six unit vectors"); + require(gamma_points.front().g_direct == std::array{0, 0, 0}, + "3D reciprocal enumeration does not place G=0 first"); + + constexpr std::size_t radial_size = 301; + constexpr double dk = 0.01; + std::vector radial_s(radial_size, 1.0); + constexpr double omega = 64.0; + const std::vector one_s_basis = { + {{0, dk, radial_s.data(), radial_s.size()}, 0, 0, {0.0, 0.0, 0.0}}, + }; + constexpr double chi = 2.5; + const auto one_s = Direct2dCoulomb::build_coulomb_matrix( + one_s_basis, gamma_points, omega, 1.0, chi, true); + require(one_s.regular_g_count == 6, "3D Gamma matrix did not remove only G=0"); + require(one_s.hermitian_residual < 1.0e-13, "3D one-function matrix is not Hermitian"); + const double expected_regular = 6.0 * 8.0 * std::pow(ModuleBase::PI, 3) / omega; + const double expected_gamma = 8.0 * std::pow(ModuleBase::PI, 3) * chi / omega; + require_close(one_s.matrix(0, 0).real(), + expected_regular + expected_gamma, + 2.0e-12, + "3D direct Coulomb normalization does not match the analytic s result"); + require_close(one_s.matrix(0, 0).imag(), + 0.0, + 1.0e-14, + "3D direct Coulomb diagonal is complex"); + + std::vector radial_p(radial_size); + for (std::size_t i = 0; i < radial_size; ++i) + { + const double k = dk * static_cast(i); + radial_p[i] = k * std::exp(-0.4 * k * k); + } + const std::vector two_function_basis = { + {{0, dk, radial_s.data(), radial_s.size()}, 0, 0, {0.0, 0.0, 0.0}}, + {{1, dk, radial_p.data(), radial_p.size()}, 0, 1, {0.17, 0.11, 0.23}}, + }; + const auto two_function = Direct2dCoulomb::build_coulomb_matrix( + two_function_basis, gamma_points, omega, 1.0, chi, true); + require(two_function.hermitian_residual < 1.0e-12, + "3D two-function matrix is not Hermitian"); + require_positive_quadratic_forms(two_function.matrix); + + const ModuleBase::Vector3 q_direct(0.25, 0.0, 0.0); + const auto non_gamma_points = Direct2dCoulomb::enumerate_reciprocal_points( + reciprocal, q_direct, tpiba, ecut_ry); + for (const auto& point: non_gamma_points) + { + require(point.k_bohr > 1.0e-14, + "non-Gamma 3D reciprocal enumeration contains zero momentum"); + } + const auto non_gamma = Direct2dCoulomb::build_coulomb_matrix( + two_function_basis, non_gamma_points, omega, 1.0, 0.0, false); + require(non_gamma.hermitian_residual < 1.0e-12, + "non-Gamma 3D matrix is not Hermitian"); + require_positive_quadratic_forms(non_gamma.matrix); + + Direct2dCoulomb::MethodMetadata3D metadata; + metadata.method = "direct_reciprocal"; + metadata.ecut_ry = 110.0; + metadata.nq = 65; + metadata.naux = 1108; + metadata.source_revision = "test-revision"; + const std::string formatted = Direct2dCoulomb::format_3d_method_metadata(metadata); + const std::string expected_metadata = + "# ABACUS reader-v1 3D Coulomb method\n" + "version = 1\n" + "method = direct_reciprocal\n" + "ecut_ry = 110\n" + "nq = 65\n" + "naux = 1108\n" + "source_revision = test-revision\n"; + require(formatted == expected_metadata, "3D method metadata is not deterministic"); + + std::cout << "direct 3D reciprocal Coulomb tests passed" << std::endl; + } + catch (const std::exception& error) + { + std::cerr << error.what() << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/source/source_lcao/module_ri/unittests/test_ewald_gaussian_cutoff.cpp b/source/source_lcao/module_ri/unittests/test_ewald_gaussian_cutoff.cpp new file mode 100644 index 00000000000..7b9d458ce72 --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_ewald_gaussian_cutoff.cpp @@ -0,0 +1,61 @@ +#include "../ewald_gaussian_cutoff.h" + +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} +} // namespace + +int main() +{ + const double historical_exponent = 17.5; + const double l8_tail = EwaldGaussianCutoff::real_space_multipole_tail(historical_exponent, 8); + require(std::abs(l8_tail - 1.396683e-2) < 1.0e-8, + "the historical real-space cutoff must expose the l=8 multipole loss"); + + const double exponent = EwaldGaussianCutoff::real_space_decay_exponent(8); + require(exponent > historical_exponent, "high angular momentum must enlarge the real-space cutoff"); + require(EwaldGaussianCutoff::real_space_multipole_tail(exponent, 8) + <= EwaldGaussianCutoff::real_space_tail_tolerance, + "the l=8 real-space multipole tail must satisfy the configured tolerance"); + + const double lambda = 1.7; + const double rcut = EwaldGaussianCutoff::real_space_radius(lambda, 8); + require(std::abs(0.5 * lambda * rcut * rcut - exponent) < 1.0e-12, + "real-space radius must use exp(-lambda*r^2/2)"); + + bool rejected = false; + try + { + (void)EwaldGaussianCutoff::real_space_radius(0.0, 8); + } + catch (const std::invalid_argument&) + { + rejected = true; + } + require(rejected, "non-positive lambda must be rejected"); + + const int lmax = 16; + const double reciprocal_exponent = EwaldGaussianCutoff::reciprocal_decay_exponent(lmax); + require(reciprocal_exponent > 35.0, "L=16 must enlarge the historical reciprocal cutoff"); + require(EwaldGaussianCutoff::reciprocal_tail_scale(reciprocal_exponent, lmax) + <= EwaldGaussianCutoff::reciprocal_tail_tolerance, + "the L=16 reciprocal Gaussian tail must satisfy the configured tolerance"); + + const double gmax = EwaldGaussianCutoff::reciprocal_radius(lambda, lmax); + require(std::abs(gmax * gmax / lambda - reciprocal_exponent) < 1.0e-12, + "reciprocal radius must use exp(-G^2/lambda)"); + + return 0; +} diff --git a/source/source_lcao/module_ri/unittests/test_ewald_mpi_distribution.cpp b/source/source_lcao/module_ri/unittests/test_ewald_mpi_distribution.cpp new file mode 100644 index 00000000000..6aef2020641 --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_ewald_mpi_distribution.cpp @@ -0,0 +1,176 @@ +#include "../ewald_mpi_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using Cell = std::array; +using Key = std::tuple; + +template +std::set make_local_keys(const Distribution& distribution) +{ + std::set keys; + for (const int atom0: distribution.first) + { + for (const auto& atom1_cell: distribution.second.at(0)) + { + const Cell& cell = atom1_cell.second; + keys.emplace(atom0, atom1_cell.first, cell[0], cell[1], cell[2]); + } + } + return keys; +} + +std::set make_full_keys(const std::vector& atoms, const Cell& period) +{ + std::set keys; + const auto atom_cells = RI::Divide_Atoms::traversal_atom_period(atoms, period); + for (const int atom0: atoms) + { + for (const auto& atom1_cell: atom_cells) + { + const Cell& cell = atom1_cell.second; + keys.emplace(atom0, atom1_cell.first, cell[0], cell[1], cell[2]); + } + } + return keys; +} + +std::size_t count_intersection(const std::set& lhs, const std::set& rhs) +{ + std::size_t count = 0; + for (const Key& key: lhs) + { + count += rhs.count(key); + } + return count; +} +} // namespace + +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + + int mpi_rank = 0; + int mpi_size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + if (mpi_size != 4) + { + if (mpi_rank == 0) + { + std::cout << "SKIP: this regression requires four MPI ranks\n"; + } + MPI_Finalize(); + return 0; + } + + const std::vector atoms = {0, 1, 2}; + const Cell bare_period = {13, 13, 3}; + const Cell gaussian_period = {7, 7, 3}; + const Cell probe_period = {15, 15, 3}; + const std::set gaussian_support = make_full_keys(atoms, gaussian_period); + + const auto common_distribution + = EwaldVqDetail::distribute_common_realspace_tasks(MPI_COMM_WORLD, atoms, bare_period); + const std::set local_bare = make_local_keys(common_distribution); + + std::set local_gaussian_on_bare_owner; + for (const Key& key: local_bare) + { + if (gaussian_support.count(key) != 0) + { + local_gaussian_on_bare_owner.insert(key); + } + } + + const auto independently_distributed_gaussian + = RI::Distribute_Equally::distribute_atoms_periods(MPI_COMM_WORLD, atoms, gaussian_period, 2, false); + const std::set local_gaussian_old = make_local_keys(independently_distributed_gaussian); + + unsigned long long local_common_overlap + = static_cast(count_intersection(local_bare, local_gaussian_on_bare_owner)); + unsigned long long local_old_overlap + = static_cast(count_intersection(local_bare, local_gaussian_old)); + unsigned long long local_bare_count = static_cast(local_bare.size()); + unsigned long long local_old_count = static_cast(local_gaussian_old.size()); + unsigned long long common_overlap = 0; + unsigned long long old_overlap = 0; + unsigned long long bare_count = 0; + unsigned long long old_count = 0; + MPI_Allreduce(&local_common_overlap, &common_overlap, 1, MPI_UNSIGNED_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); + MPI_Allreduce(&local_old_overlap, &old_overlap, 1, MPI_UNSIGNED_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); + MPI_Allreduce(&local_bare_count, &bare_count, 1, MPI_UNSIGNED_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); + MPI_Allreduce(&local_old_count, &old_count, 1, MPI_UNSIGNED_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); + + const auto expected_bare = static_cast(make_full_keys(atoms, bare_period).size()); + const auto expected_gaussian = static_cast(gaussian_support.size()); + const auto local_shell + = EwaldVqDetail::distribute_tail_shell_keys(atoms, bare_period, probe_period, mpi_rank, mpi_size); + const std::set local_shell_set(local_shell.begin(), local_shell.end()); + bool local_shell_ok = local_shell_set.size() == local_shell.size(); + for (const auto& key: local_shell) + { + local_shell_ok = local_shell_ok && EwaldVqDetail::shell_owner(key, mpi_size) == mpi_rank; + local_shell_ok = local_shell_ok + && local_shell_set.count(EwaldVqDetail::hermitian_partner(key)) == 1; + } + unsigned long long local_shell_count = static_cast(local_shell.size()); + unsigned long long shell_count = 0; + int local_shell_ok_int = local_shell_ok ? 1 : 0; + int shell_ok_int = 0; + MPI_Allreduce(&local_shell_count, &shell_count, 1, MPI_UNSIGNED_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); + MPI_Allreduce(&local_shell_ok_int, &shell_ok_int, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD); + const auto expected_shell + = static_cast(EwaldVqDetail::make_shell_keys(atoms, bare_period, probe_period).size()); + EwaldVqDetail::TailStats local_tail_stats; + local_tail_stats.production_blocks = static_cast(mpi_rank + 2); + local_tail_stats.shell_blocks = static_cast(mpi_rank + 1); + local_tail_stats.reference_norm = 10.0 + mpi_rank; + local_tail_stats.max_norm = 1.0 + mpi_rank; + local_tail_stats.sum_norm = 0.5 * (mpi_rank + 1); + local_tail_stats.hermitian_residual = 0.1 * mpi_rank; + local_tail_stats.worst_key = EwaldVqDetail::TailKey{mpi_rank, 0, {mpi_rank, 0, 0}}; + local_tail_stats.worst_bare_norm = 20.0 + mpi_rank; + local_tail_stats.worst_gaussian_norm = 15.0 + mpi_rank; + local_tail_stats.worst_distance = 8.0 + mpi_rank; + const auto global_tail_stats = EwaldVqDetail::reduce_tail_stats(MPI_COMM_WORLD, local_tail_stats); + const bool tail_reduce_ok = global_tail_stats.production_blocks == 14 + && global_tail_stats.shell_blocks == 10 + && std::abs(global_tail_stats.reference_norm - 13.0) < 1e-14 + && std::abs(global_tail_stats.max_norm - 4.0) < 1e-14 + && std::abs(global_tail_stats.sum_norm - 5.0) < 1e-14 + && std::abs(global_tail_stats.hermitian_residual - 0.3) < 1e-14 + && global_tail_stats.worst_key == EwaldVqDetail::TailKey{3, 0, {3, 0, 0}} + && std::abs(global_tail_stats.worst_bare_norm - 23.0) < 1e-14 + && std::abs(global_tail_stats.worst_gaussian_norm - 18.0) < 1e-14 + && std::abs(global_tail_stats.worst_distance - 11.0) < 1e-14; + const bool passed = bare_count == expected_bare && old_count == expected_gaussian + && common_overlap == expected_gaussian && old_overlap < expected_gaussian + && shell_count == expected_shell && shell_ok_int == 1 && tail_reduce_ok; + + if (mpi_rank == 0) + { + std::cout << "bare tasks: " << bare_count << "/" << expected_bare << '\n' + << "Gaussian tasks: " << old_count << "/" << expected_gaussian << '\n' + << "common-owner overlap: " << common_overlap << "/" << expected_gaussian << '\n' + << "old independent-owner overlap: " << old_overlap << "/" << expected_gaussian << '\n' + << "tail shell tasks: " << shell_count << "/" << expected_shell << '\n'; + if (!passed) + { + std::cerr << "Ewald MPI real-space owner regression failed\n"; + } + } + + MPI_Finalize(); + return passed ? 0 : 1; +} diff --git a/source/source_lcao/module_ri/unittests/test_ewald_tail_utils.cpp b/source/source_lcao/module_ri/unittests/test_ewald_tail_utils.cpp new file mode 100644 index 00000000000..1f0dbc0338a --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_ewald_tail_utils.cpp @@ -0,0 +1,242 @@ +#include "../ewald_tail_utils.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +using Cell = std::array; +using EwaldVqDetail::TailDecision; +using EwaldVqDetail::TailKey; +using EwaldVqDetail::TailMode; +using EwaldVqDetail::TailStats; +using EwaldVqDetail::TailTolerances; + +void require(const bool condition, const std::string& message) +{ + if (!condition) + { + throw std::runtime_error(message); + } +} + +void test_expand_odd_period() +{ + require(EwaldVqDetail::expand_odd_period(Cell{13, 13, 3}, {true, true, false}, 1) + == Cell{15, 15, 3}, + "period expansion must preserve a nonperiodic direction"); + require(EwaldVqDetail::expand_odd_period(Cell{1, 3, 5}, {true, true, true}, 2) + == Cell{5, 7, 9}, + "period expansion must add two cells per guard layer"); +} + +void test_shell_cells() +{ + const auto shell = EwaldVqDetail::make_shell_cells(Cell{3, 3, 1}, Cell{5, 5, 1}); + require(shell.size() == 16, "5x5 shell around 3x3 must contain sixteen cells"); + require(std::set(shell.begin(), shell.end()).size() == shell.size(), + "shell cells must not contain duplicates"); + for (const Cell& cell: shell) + { + require(cell[2] == 0, "nonperiodic shell coordinate must remain zero"); + require(!EwaldVqDetail::cell_in_period(cell, Cell{3, 3, 1}), + "shell must exclude production cells"); + require(EwaldVqDetail::cell_in_period(cell, Cell{5, 5, 1}), + "shell must stay inside the probe period"); + } +} + +void test_hermitian_partner_and_owner() +{ + const TailKey key{0, 1, {2, -1, 0}}; + const TailKey partner = EwaldVqDetail::hermitian_partner(key); + require(partner == TailKey{1, 0, {-2, 1, 0}}, + "Hermitian partner must swap atoms and negate the cell"); + require(EwaldVqDetail::shell_owner(key, 7) == EwaldVqDetail::shell_owner(partner, 7), + "Hermitian partners must use the same deterministic owner"); +} + +void test_tail_decisions() +{ + const TailTolerances tolerances{1e-12, 1e-10, 1e-8}; + TailStats stats; + stats.reference_norm = 10.0; + stats.max_norm = 5e-10; + stats.sum_norm = 2e-7; + stats.coverage_complete = true; + + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Warn, 0, 3) + == TailDecision::Warn, + "warn mode must not enlarge a failed shell"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Enlarge, 0, 3) + == TailDecision::Expand, + "enlarge mode must expand a failed shell"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Strict, 3, 3) + == TailDecision::Fail, + "strict mode must fail at the expansion limit"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Enlarge, 3, 3) + == TailDecision::Fail, + "enlarge mode must fail at the expansion limit"); + + stats.max_norm = 5e-11; + stats.sum_norm = 5e-8; + stats.hermitian_residual = 2e-9; + require(!EwaldVqDetail::tail_passes(stats, tolerances), + "a failed Hermitian invariant must reject an otherwise converged shell"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Warn, 0, 3) + == TailDecision::Fail, + "warn mode must still fail a Hermitian invariant violation"); + stats.hermitian_residual = 0.0; + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Enlarge, 0, 3) + == TailDecision::Accept, + "maximum, summed, and Hermitian tail criteria must accept"); + + stats.coverage_complete = false; + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Warn, 0, 3) + == TailDecision::Fail, + "warn mode must still fail incomplete physical coverage"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Enlarge, 0, 3) + == TailDecision::Expand, + "enlarge mode must expand incomplete physical coverage"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Strict, 0, 3) + == TailDecision::Expand, + "strict mode must expand incomplete physical coverage before the limit"); + require(EwaldVqDetail::decide_tail(stats, tolerances, TailMode::Strict, 3, 3) + == TailDecision::Fail, + "strict mode must reject incomplete physical coverage at the limit"); +} + +void test_adaptive_exact_rmesh_times() +{ + require(EwaldVqDetail::uses_adaptive_realspace_range(TailMode::Enlarge), + "enlarge mode must use the converged real-space range"); + require(EwaldVqDetail::uses_adaptive_realspace_range(TailMode::Strict), + "strict mode must use the converged real-space range"); + require(!EwaldVqDetail::uses_adaptive_realspace_range(TailMode::Warn), + "warn mode must preserve the requested production range"); + + require(std::abs(EwaldVqDetail::adaptive_exact_rmesh_times(0.01, {6.0}, {6.0}) - 1.0) + < 1e-14, + "adaptive evaluation must keep exact pair tables through the nonoverlap boundary"); + require(std::abs(EwaldVqDetail::adaptive_exact_rmesh_times(0.5, {4.0, 7.0}, {8.0, 6.0}) + - 5.0 / 3.0) + < 1e-14, + "heterogeneous cutoffs must cover every ordered ABFS pair"); + require(std::abs(EwaldVqDetail::adaptive_exact_rmesh_times(2.0, {6.0}, {6.0}) - 2.0) + < 1e-14, + "an already larger requested radial mesh must be preserved"); +} + +void test_missing_block_classification() +{ + using EwaldVqDetail::TailBlockCoverage; + require(EwaldVqDetail::classify_tail_block(true, true, false) == TailBlockCoverage::Common, + "two evaluated blocks must be subtracted directly"); + require(EwaldVqDetail::classify_tail_block(true, false, true) + == TailBlockCoverage::AnalyticCancellation, + "a nonoverlapping far-field pair may use analytic multipole cancellation"); + require(EwaldVqDetail::classify_tail_block(false, true, true) + == TailBlockCoverage::AnalyticCancellation, + "far-field analytic cancellation is symmetric between bare and Gaussian maps"); + require(EwaldVqDetail::classify_tail_block(true, false, false) == TailBlockCoverage::Incomplete, + "a near-field missing Gaussian block must not be treated as zero"); + require(EwaldVqDetail::classify_tail_block(false, true, false) == TailBlockCoverage::Incomplete, + "a near-field missing bare block must not be treated as zero"); + + const auto missing_gaussian = EwaldVqDetail::plan_tail_block(true, false, 8.0, 10.0, 7.0); + require(missing_gaussian.coverage == TailBlockCoverage::Common + && !missing_gaussian.use_bare_multipole + && missing_gaussian.use_gaussian_multipole, + "the mixed-support interval must combine exact bare and analytic Gaussian blocks"); + + const auto missing_bare = EwaldVqDetail::plan_tail_block(false, true, 8.0, 7.0, 10.0); + require(missing_bare.coverage == TailBlockCoverage::Common + && missing_bare.use_bare_multipole + && !missing_bare.use_gaussian_multipole, + "the mixed-support interval must combine analytic bare and exact Gaussian blocks"); + + const auto both_far = EwaldVqDetail::plan_tail_block(false, false, 12.0, 10.0, 11.0); + require(both_far.coverage == TailBlockCoverage::AnalyticCancellation + && !both_far.use_bare_multipole + && !both_far.use_gaussian_multipole, + "two absent far-field blocks must cancel without constructing a shared multipole tensor"); + + const auto evaluated_but_far = EwaldVqDetail::plan_tail_block(true, true, 34.1, 24.0, 20.9); + require(evaluated_but_far.coverage == TailBlockCoverage::AnalyticCancellation, + "available numerical blocks beyond both source supports must use exact multipole cancellation"); + + const auto gaussian_far_only = EwaldVqDetail::plan_tail_block(true, true, 22.0, 24.0, 20.9); + require(gaussian_far_only.coverage == TailBlockCoverage::Common + && !gaussian_far_only.use_bare_multipole + && gaussian_far_only.use_gaussian_multipole, + "a Gaussian block beyond its source support must use its exact multipole even when evaluated"); + + require(EwaldVqDetail::plan_tail_block(true, false, 6.0, 10.0, 7.0).coverage + == TailBlockCoverage::Incomplete, + "a missing Gaussian block inside its compact support must remain an error"); +} + +void test_tail_sample_aggregation() +{ + TailStats stats; + EwaldVqDetail::accumulate_tail_sample( + stats, + EwaldVqDetail::TailSample{TailKey{0, 1, {1, 0, 0}}, 5.0, 4.9, 0.1, 8.0}); + EwaldVqDetail::accumulate_tail_sample( + stats, + EwaldVqDetail::TailSample{TailKey{1, 1, {2, 0, 0}}, 3.0, 2.7, 0.3, 12.0}); + require(stats.shell_blocks == 2, "tail aggregation must count every sampled block"); + require(std::abs(stats.sum_norm - 0.4) < 1e-14, "tail aggregation must sum block norms"); + require(std::abs(stats.max_norm - 0.3) < 1e-14, "tail aggregation must retain the maximum norm"); + require(stats.worst_key == TailKey{1, 1, {2, 0, 0}}, "tail aggregation must retain the worst key"); + require(std::abs(stats.worst_bare_norm - 3.0) < 1e-14, + "tail aggregation must retain the worst bare norm"); + require(std::abs(stats.worst_gaussian_norm - 2.7) < 1e-14, + "tail aggregation must retain the worst Gaussian norm"); + require(std::abs(stats.worst_distance - 12.0) < 1e-14, + "tail aggregation must retain the worst-key distance"); +} + +void test_explicit_key_grouping_and_far_field_boundary() +{ + const std::vector keys{ + TailKey{1, 0, {2, 0, 0}}, + TailKey{0, 1, {-2, 0, 0}}, + TailKey{1, 1, {3, 0, 0}}, + }; + const auto grouped = EwaldVqDetail::group_tail_keys_by_atom(keys); + require(grouped.size() == 2, "explicit tail keys must be grouped by center atom"); + require(grouped.at(0).size() == 1 && grouped.at(1).size() == 2, + "grouping must preserve every explicit J,R key exactly once"); + require(EwaldVqDetail::far_multipoles_equivalent(12.0, 10.0, 11.0), + "nonoverlap of both compact supports permits far-field cancellation"); + require(!EwaldVqDetail::far_multipoles_equivalent(10.5, 10.0, 11.0), + "overlap of either compact support requires explicit evaluation"); +} +} // namespace + +int main() +{ + try + { + test_expand_odd_period(); + test_shell_cells(); + test_hermitian_partner_and_owner(); + test_tail_decisions(); + test_adaptive_exact_rmesh_times(); + test_missing_block_classification(); + test_tail_sample_aggregation(); + test_explicit_key_grouping_and_far_field_boundary(); + } + catch (const std::exception& error) + { + std::cerr << "Ewald tail utility test failed: " << error.what() << '\n'; + return 1; + } + std::cout << "Ewald tail utility tests passed\n"; + return 0; +} diff --git a/source/source_lcao/module_ri/unittests/test_gaussian_abfs_2d_integrals.cpp b/source/source_lcao/module_ri/unittests/test_gaussian_abfs_2d_integrals.cpp new file mode 100644 index 00000000000..77bff183ee3 --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_gaussian_abfs_2d_integrals.cpp @@ -0,0 +1,175 @@ +#include "../gaussian_abfs_2d_integrals.h" + +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << message << '\n'; + std::exit(EXIT_FAILURE); + } +} +} // namespace + +int main() +{ + const double coefficient = std::sqrt(3.0 / (4.0 * GaussianAbfs2D::pi)); + + const double q_in_plane = 0.06029258; + const double expected_in_plane + = -coefficient * GaussianAbfs2D::pi * std::exp(q_in_plane * q_in_plane) + * std::erfc(q_in_plane); + require(std::abs(GaussianAbfs2D::coulomb_l1(1, q_in_plane, 0.0, 0.0, 1.0).real() + - expected_in_plane) + < 1.0e-13, + "in-plane x L=1 analytic Coulomb integral mismatch"); + require(std::abs(GaussianAbfs2D::coulomb_l1(2, 0.0, q_in_plane, 0.0, 1.0).real() + - expected_in_plane) + < 1.0e-13, + "in-plane y L=1 analytic Coulomb integral mismatch"); + + const double q_out_of_plane = 0.10048763; + const double tau = 0.7; + const double step = 1.0e-5; + const double finite_difference + = (GaussianAbfs2D::i2(q_out_of_plane, tau + step, 1.0) + - GaussianAbfs2D::i2(q_out_of_plane, tau - step, 1.0)) + / (2.0 * step); + require(std::abs(GaussianAbfs2D::di2_dtau(q_out_of_plane, tau, 1.0) + - finite_difference) + < 2.0e-9, + "out-of-plane I2 derivative mismatch"); + require(std::abs(GaussianAbfs2D::coulomb_l1(0, q_out_of_plane, 0.0, tau, 1.0).imag() + + coefficient * finite_difference) + < 2.0e-9, + "out-of-plane L=1 analytic Coulomb integral mismatch"); + + const double small_q_value + = GaussianAbfs2D::coulomb_l1(1, 1.0e-8, 0.0, 0.0, 1.0).real(); + require(std::abs(small_q_value + coefficient * GaussianAbfs2D::pi) < 2.0e-8, + "in-plane L=1 small-q limit is not finite"); + + const double qx = 0.08; + const double qy = 0.06; + const double q = std::hypot(qx, qy); + const double tau_high_l = 3.7; + const double i2_value = GaussianAbfs2D::i2(q, tau_high_l, 1.0); + const double j0 = std::sqrt(GaussianAbfs2D::pi) + * std::exp(-tau_high_l * tau_high_l / 4.0); + const double y20_prefactor = std::sqrt(5.0 / (16.0 * GaussianAbfs2D::pi)); + const std::complex expected_y20 + = y20_prefactor * (2.0 * j0 - 3.0 * q * q * i2_value); + require(std::abs(GaussianAbfs2D::integral_lm(2, 0, -2.0, qx, qy, tau_high_l, 1.0) + - expected_y20) + < 2.0e-13, + "L=2 M=0 closed-form Coulomb integral mismatch"); + + const double y21_prefactor = std::sqrt(15.0 / (4.0 * GaussianAbfs2D::pi)); + const std::complex expected_y21( + 0.0, + y21_prefactor * qx * GaussianAbfs2D::di2_dtau(q, tau_high_l, 1.0)); + require(std::abs(GaussianAbfs2D::integral_lm(2, 1, -2.0, qx, qy, tau_high_l, 1.0) + - expected_y21) + < 2.0e-13, + "L=2 M=1 closed-form Coulomb integral mismatch"); + + const double y22_prefactor = std::sqrt(15.0 / (16.0 * GaussianAbfs2D::pi)); + const std::complex expected_y22 = y22_prefactor * (qx * qx - qy * qy) * i2_value; + require(std::abs(GaussianAbfs2D::integral_lm(2, 3, -2.0, qx, qy, tau_high_l, 1.0) + - expected_y22) + < 2.0e-13, + "L=2 M=3 closed-form Coulomb integral mismatch"); + + const std::complex j2 + = std::sqrt(GaussianAbfs2D::pi) * std::exp(-tau_high_l * tau_high_l / 4.0) + * (0.5 - tau_high_l * tau_high_l / 4.0); + const std::complex expected_y20_power0 + = y20_prefactor * (2.0 * j2 - q * q * j0); + require(std::abs(GaussianAbfs2D::integral_lm(2, 0, 0.0, qx, qy, tau_high_l, 1.0) + - expected_y20_power0) + < 2.0e-13, + "L=2 M=0 polynomial Gaussian integral mismatch"); + + for (const double beta : {0.5, 1.0, 2.0}) + { + for (int l = 2; l <= 16; ++l) + { + const int degree = l - 2; + const double scaled_tau = tau_high_l / std::sqrt(beta); + double hm2 = 1.0; + double hm1 = scaled_tau; + double hermite = degree == 0 ? hm2 : hm1; + for (int n = 2; n <= degree; ++n) + { + hermite = scaled_tau * hm1 - 2.0 * (n - 1) * hm2; + hm2 = hm1; + hm1 = hermite; + } + const std::complex i_power + = std::polar(1.0, 0.5 * GaussianAbfs2D::pi * static_cast(degree)); + const std::complex exact_gamma + = std::sqrt(static_cast(2 * l + 1) / (4.0 * GaussianAbfs2D::pi)) + * std::pow(beta, -0.5 * static_cast(degree + 1)) + * std::sqrt(GaussianAbfs2D::pi) / std::pow(2.0, degree) * hermite + * std::exp(-scaled_tau * scaled_tau / 4.0) * i_power; + // The degree-14 Hermite recursion that dominates this closed form + // cancels down to ~1e-11 relative agreement between the kernel and + // the reference on a given libm; 2e-11 was knife-edge across + // gcc/glibc and the Intel runtime, so keep 11-digit verification + // with explicit margin. + require(std::abs(GaussianAbfs2D::integral_lm(l, 0, -2.0, 0.0, 0.0, tau_high_l, beta) + - exact_gamma) + < 1.0e-9 * std::max(1.0, std::abs(exact_gamma)), + "high-L Gamma closed-form integral mismatch"); + } + } + + // At q_parallel=0 every M=0 polynomial channel reduces to one exact + // Fourier-Gaussian moment. This exercises every production power through + // degree 30 at a nonzero layer separation, where direct quadrature suffers + // severe oscillatory cancellation. + for (const double beta : {0.5, 1.0, 2.0}) + { + for (int power = 0; power <= 14; power += 2) + { + for (int l = 0; l <= 16; ++l) + { + const int degree = l + power; + const double scaled_tau = tau_high_l / std::sqrt(beta); + double hm2 = 1.0; + double hm1 = scaled_tau; + double hermite = degree == 0 ? hm2 : hm1; + for (int n = 2; n <= degree; ++n) + { + hermite = scaled_tau * hm1 - 2.0 * (n - 1) * hm2; + hm2 = hm1; + hm1 = hermite; + } + const std::complex i_power + = std::polar(1.0, 0.5 * GaussianAbfs2D::pi * static_cast(degree)); + const std::complex exact_gamma + = std::sqrt(static_cast(2 * l + 1) / (4.0 * GaussianAbfs2D::pi)) + * std::pow(beta, -0.5 * static_cast(degree + 1)) + * std::sqrt(GaussianAbfs2D::pi) / std::pow(2.0, degree) * hermite + * std::exp(-scaled_tau * scaled_tau / 4.0) * i_power; + require(std::abs(GaussianAbfs2D::integral_lm(l, + 0, + static_cast(power), + 0.0, + 0.0, + tau_high_l, + beta) + - exact_gamma) + < 5.0e-10 * std::max(1.0, std::abs(exact_gamma)), + "all-power Gamma Fourier-Gaussian moment mismatch"); + } + } + } + + return EXIT_SUCCESS; +} diff --git a/source/source_lcao/module_ri/unittests/test_gaussian_abfs_cutoff.cpp b/source/source_lcao/module_ri/unittests/test_gaussian_abfs_cutoff.cpp new file mode 100644 index 00000000000..98867499cda --- /dev/null +++ b/source/source_lcao/module_ri/unittests/test_gaussian_abfs_cutoff.cpp @@ -0,0 +1,42 @@ +#include "../gaussian_abfs.h" + +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} +} + +int main() +{ + for (const double lambda: {0.5, 1.0, 2.0}) + { + const double l0_cutoff = Gaussian_Abfs::radial_cutoff(lambda, 0); + const double l8_cutoff = Gaussian_Abfs::radial_cutoff(lambda, 8); + require(l0_cutoff > std::sqrt(35.0 / lambda), + "Gaussian radial cutoff must control the complete l=0 multipole tail"); + require(l8_cutoff > l0_cutoff, "high angular momentum must enlarge the Gaussian radial cutoff"); + } + + bool rejected = false; + try + { + (void)Gaussian_Abfs::radial_cutoff(0.0, 8); + } + catch (const std::invalid_argument&) + { + rejected = true; + } + require(rejected, "non-positive lambda must be rejected"); + return 0; +} diff --git a/source/source_lcao/module_ri/unittests/test_write_ri_cv.cpp b/source/source_lcao/module_ri/unittests/test_write_ri_cv.cpp index a7eee2a74a2..67cb990ef0f 100644 --- a/source/source_lcao/module_ri/unittests/test_write_ri_cv.cpp +++ b/source/source_lcao/module_ri/unittests/test_write_ri_cv.cpp @@ -2,9 +2,10 @@ #include #include -#include #include -#include "source_lcao/module_ri/write_ri_cv.hpp" +#include +#include +#include "../lri_cv_tools.h" using TC = std::array; using TAC = std::pair; @@ -31,4 +32,26 @@ TEST(LRI_CV_Tools, ReadCs) EXPECT_EQ(Vs.at(0).size(), 6); EXPECT_EQ(Vs.at(1).at({ 1, {1, 0, 0} }).shape.size(), 2); -} \ No newline at end of file +} + +TEST(LRI_CV_Tools, MinusCommonKeysKeepsLeftOnlyBlocks) +{ + std::map> lhs; + std::map> rhs; + + lhs[0][{0, {0, 0, 0}}] = 10; + lhs[0][{1, {0, 0, 0}}] = 20; + lhs[1][{0, {0, 0, 0}}] = 30; + + rhs[0][{0, {0, 0, 0}}] = 3; + rhs[2][{0, {0, 0, 0}}] = 40; + + const auto diff = LRI_CV_Tools::minus_common_keys(lhs, rhs); + + ASSERT_EQ(diff.size(), 2); + ASSERT_EQ(diff.at(0).size(), 2); + EXPECT_EQ(diff.at(0).at({0, {0, 0, 0}}), 7); + EXPECT_EQ(diff.at(0).at({1, {0, 0, 0}}), 20); + EXPECT_EQ(diff.at(1).at({0, {0, 0, 0}}), 30); + EXPECT_EQ(diff.count(2), 0); +} diff --git a/source/source_lcao/module_ri/write_ri_cv.hpp b/source/source_lcao/module_ri/write_ri_cv.hpp index c701aa1a0bd..382aff808fb 100644 --- a/source/source_lcao/module_ri/write_ri_cv.hpp +++ b/source/source_lcao/module_ri/write_ri_cv.hpp @@ -92,7 +92,7 @@ namespace LRI_CV_Tools } template - TLRI read_Cs_ao_all(const std::string& path, const double& threshold) + TLRI read_Cs_ao_all(const std::string& path, const double& threshold = 0.0) { IZ(natom) IZ(ncell) IZ(ia1) IZ(ia2) IZ(ic_1) IZ(ic_2) IZ(ic_3) SZ(nw1) SZ(nw2) SZ(nabf) diff --git a/source/source_lcao/module_rpa/CMakeLists.txt b/source/source_lcao/module_rpa/CMakeLists.txt index 9969d4431dd..b10c082309d 100644 --- a/source/source_lcao/module_rpa/CMakeLists.txt +++ b/source/source_lcao/module_rpa/CMakeLists.txt @@ -5,6 +5,7 @@ endif() add_subdirectory(module_librpa_io) add_library(rpa OBJECT + rpa_abfs_preorthogonalization.cpp rpa_lri.cpp rpa_lri_detail.cpp rpa_lri_band_io.cpp diff --git a/source/source_lcao/module_rpa/module_librpa_io/librpa_2d_coulomb_head.h b/source/source_lcao/module_rpa/module_librpa_io/librpa_2d_coulomb_head.h new file mode 100644 index 00000000000..8e887667dd6 --- /dev/null +++ b/source/source_lcao/module_rpa/module_librpa_io/librpa_2d_coulomb_head.h @@ -0,0 +1,89 @@ +#ifndef LIBRPA_2D_COULOMB_HEAD_H +#define LIBRPA_2D_COULOMB_HEAD_H + +#include +#include +#include +#include +#include +#include + +// Strict-2D Coulomb head normalization for the reader-v1 output. +// +// In a strict 2D (slab) cell the q_parallel -> 0 limit of the full Coulomb +// does not vanish: the auxiliary monopoles form a charged sheet whose +// interaction is the "head" of the separable Coulomb representation. With +// s_multipoles the per-type s-channel (l=0) moments, the head coefficient is +// 8 pi^2 * ||s||^2 / A_parallel, ||s||^2 = sum_I nat_I * sum_n s_{I,n}^2, +// and the sheet-to-raw scale sqrt(coefficient / 2 pi) converts between the +// sheet representation LibRPA consumes and the raw coefficient. Both are +// written to the librpa_2d_coulomb_head.txt sidecar next to the V files. +namespace RpaLriDetail +{ +struct Strict2dCoulombHeadNormalization +{ + double area_parallel_bohr2 = 0.0; + double multipole_norm_squared = 0.0; + double raw_head_coefficient = 0.0; + double sheet_to_raw_scale = 0.0; +}; + +inline Strict2dCoulombHeadNormalization strict_2d_coulomb_head_normalization( + const double area_parallel_bohr2, + const std::vector>& s_multipoles_by_type, + const std::vector& atoms_per_type) +{ + if (!(area_parallel_bohr2 > 0.0) || !std::isfinite(area_parallel_bohr2) + || s_multipoles_by_type.size() != atoms_per_type.size() + || s_multipoles_by_type.empty()) + { + throw std::invalid_argument("invalid strict 2D Coulomb head normalization inputs"); + } + + double multipole_norm_squared = 0.0; + for (std::size_t it = 0; it != s_multipoles_by_type.size(); ++it) + { + if (atoms_per_type[it] < 0) + { + throw std::invalid_argument("negative atom count in strict 2D Coulomb head normalization"); + } + double type_norm_squared = 0.0; + for (const double moment : s_multipoles_by_type[it]) + { + if (!std::isfinite(moment)) + { + throw std::invalid_argument("non-finite strict 2D auxiliary multipole"); + } + type_norm_squared += moment * moment; + } + multipole_norm_squared += atoms_per_type[it] * type_norm_squared; + } + if (!(multipole_norm_squared > 0.0) || !std::isfinite(multipole_norm_squared)) + { + throw std::invalid_argument("strict 2D auxiliary basis has no finite monopole channel"); + } + + constexpr double pi = 3.141592653589793238462643383279502884; + const double raw_head_coefficient = + 8.0 * pi * pi * multipole_norm_squared / area_parallel_bohr2; + return {area_parallel_bohr2, + multipole_norm_squared, + raw_head_coefficient, + std::sqrt(raw_head_coefficient / (2.0 * pi))}; +} + +inline std::string format_strict_2d_coulomb_head_sidecar( + const Strict2dCoulombHeadNormalization& normalization) +{ + std::ostringstream output; + output << "# ABACUS reader-v1 strict 2D Coulomb head normalization\n" + << "version = 1\n" << std::setprecision(17) + << "area_parallel_bohr2 = " << normalization.area_parallel_bohr2 << '\n' + << "multipole_norm_squared = " << normalization.multipole_norm_squared << '\n' + << "strict_2d_coulomb_head_coefficient = " + << normalization.raw_head_coefficient << '\n' + << "strict_2d_sheet_to_raw_scale = " << normalization.sheet_to_raw_scale << '\n'; + return output.str(); +} +} // namespace RpaLriDetail +#endif diff --git a/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.cpp b/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.cpp index 9e8eea6da14..21a09669f4d 100644 --- a/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.cpp +++ b/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.cpp @@ -15,4 +15,23 @@ ModuleBase::Vector3 librpa_cartesian_kvec(const ModuleBase::Vector3= stored_kpoint_count) + { + throw std::invalid_argument("LibRPA BZ output found an invalid stored k-point index."); + } + const int one_based_index = stored_kpoint_index + 1; + return {one_based_index, one_based_index}; +} } // namespace RpaLriDetail diff --git a/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.h b/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.h index 6e8f4d6f4c9..5e37691a679 100644 --- a/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.h +++ b/source/source_lcao/module_rpa/module_librpa_io/librpa_bz_sampling.h @@ -5,8 +5,18 @@ namespace RpaLriDetail { +struct LibRpaStoredQIndex +{ + int coulomb_irreducible_index; + int representative_scf_index; +}; + ModuleBase::Vector3 librpa_cartesian_kvec(const ModuleBase::Vector3& kvec_c, double lat0_bohr); + +int librpa_stored_coulomb_q_count(int stored_kpoint_count); + +LibRpaStoredQIndex librpa_stored_q_index(int stored_kpoint_index, int stored_kpoint_count); } // namespace RpaLriDetail #endif diff --git a/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.cpp b/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.cpp index db4d29a73e2..b124e919de1 100644 --- a/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.cpp +++ b/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.cpp @@ -3,6 +3,7 @@ #include "source_base/matrix3.h" #include "source_base/vector3.h" #include "source_cell/module_symmetry/symm_rot_spin.h" +#include "source_cell/module_symmetry/symmetry.h" #include #include @@ -118,4 +119,41 @@ void write_librpa_spin_symmetry(std::ostream& output, output << '\n'; } } +void write_librpa_symmetry_block(std::ostream& output, + const ModuleBase::Matrix3& latvec, + const ModuleSymmetry::Symmetry& symmetry, + const int nspin) +{ + if (symmetry.nrotk <= 0) + { + return; + } + + const int n_anti = symmetry.magnetic_nspin4 ? symmetry.nrotk_anti : 0; + std::vector unitary(static_cast(symmetry.nrotk)); + for (int isym = 0; isym < symmetry.nrotk; ++isym) + { + unitary[static_cast(isym)] + = make_librpa_symmetry_operation(symmetry.gmatrix[isym], symmetry.gtrans[isym]); + } + std::vector antiunitary(static_cast(n_anti)); + for (int isym = 0; isym < n_anti; ++isym) + { + antiunitary[static_cast(isym)] + = make_librpa_symmetry_operation(symmetry.gmatrix_anti[isym], symmetry.gtrans_anti[isym]); + } + write_librpa_symmetry_rows(output, unitary, antiunitary); + + if (nspin == 4) + { + const std::vector unitary_rotations(symmetry.gmatrix, + symmetry.gmatrix + symmetry.nrotk); + const std::vector antiunitary_rotations(symmetry.gmatrix_anti, + symmetry.gmatrix_anti + n_anti); + const auto spin_operations + = make_librpa_spin_symmetry_operations(latvec, unitary_rotations, antiunitary_rotations); + const int grey_group = symmetry.magnetic_nspin4 ? 0 : 1; + write_librpa_spin_symmetry(output, grey_group, 1, spin_operations); + } +} } // namespace RpaLriDetail diff --git a/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.h b/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.h index f48f355ef48..e5da7b161a0 100644 --- a/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.h +++ b/source/source_lcao/module_rpa/module_librpa_io/librpa_stru_symmetry.h @@ -13,6 +13,11 @@ template class Vector3; } +namespace ModuleSymmetry +{ +class Symmetry; +} + namespace RpaLriDetail { struct LibRpaSymmetryOperation @@ -45,6 +50,14 @@ void write_librpa_spin_symmetry(std::ostream& output, int grey_group, int spin_source, const std::vector& operations); + +// Serialize the complete symmetry block used by RPA_LRI::out_struc. Keeping the +// production assembly here makes the SOC/magnetic ordering testable without +// constructing the full RPA driver or running an electronic-structure job. +void write_librpa_symmetry_block(std::ostream& output, + const ModuleBase::Matrix3& latvec, + const ModuleSymmetry::Symmetry& symmetry, + int nspin); } // namespace RpaLriDetail #endif diff --git a/source/source_lcao/module_rpa/module_librpa_io/unittests/CMakeLists.txt b/source/source_lcao/module_rpa/module_librpa_io/unittests/CMakeLists.txt index bb351373876..26d23d0a657 100644 --- a/source/source_lcao/module_rpa/module_librpa_io/unittests/CMakeLists.txt +++ b/source/source_lcao/module_rpa/module_librpa_io/unittests/CMakeLists.txt @@ -1,3 +1,7 @@ +abacus_disable_feature_definitions(__MLALGO) +abacus_disable_feature_definitions(__CUDA) +abacus_disable_feature_definitions(__ROCM) + AddTest( TARGET MODULE_RPA_librpa_stru_symmetry_test LIBS base device symmetry @@ -6,3 +10,45 @@ AddTest( ../librpa_stru_symmetry.cpp ../librpa_stru_units.cpp ) + +add_executable(MODULE_RPA_librpa_stru_units_test test_librpa_stru_units.cpp + ../librpa_stru_units.cpp) +target_include_directories(MODULE_RPA_librpa_stru_units_test PRIVATE ${CMAKE_SOURCE_DIR}/source) +install(TARGETS MODULE_RPA_librpa_stru_units_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RPA_librpa_stru_units_test + COMMAND ./MODULE_RPA_librpa_stru_units_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + +add_executable(MODULE_RPA_librpa_bz_sampling_test test_librpa_bz_sampling.cpp + ../librpa_bz_sampling.cpp) +target_include_directories(MODULE_RPA_librpa_bz_sampling_test PRIVATE ${CMAKE_SOURCE_DIR}/source) +install(TARGETS MODULE_RPA_librpa_bz_sampling_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RPA_librpa_bz_sampling_test + COMMAND ./MODULE_RPA_librpa_bz_sampling_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + +add_executable(MODULE_RPA_librpa_2d_coulomb_head_test test_librpa_2d_coulomb_head.cpp) +target_include_directories(MODULE_RPA_librpa_2d_coulomb_head_test PRIVATE ${CMAKE_SOURCE_DIR}/source) +install(TARGETS MODULE_RPA_librpa_2d_coulomb_head_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RPA_librpa_2d_coulomb_head_test + COMMAND ./MODULE_RPA_librpa_2d_coulomb_head_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + +add_executable(MODULE_RPA_librpa_stru_symmetry_writers_test + test_librpa_stru_symmetry_writers.cpp + ../librpa_stru_symmetry.cpp) +target_include_directories(MODULE_RPA_librpa_stru_symmetry_writers_test PRIVATE ${CMAKE_SOURCE_DIR}/source) +target_link_libraries(MODULE_RPA_librpa_stru_symmetry_writers_test + PRIVATE base device symmetry abacus::linalg_libs) +install(TARGETS MODULE_RPA_librpa_stru_symmetry_writers_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RPA_librpa_stru_symmetry_writers_test + COMMAND ./MODULE_RPA_librpa_stru_symmetry_writers_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_2d_coulomb_head.cpp b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_2d_coulomb_head.cpp new file mode 100644 index 00000000000..6c16e9fe4e4 --- /dev/null +++ b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_2d_coulomb_head.cpp @@ -0,0 +1,84 @@ +#include "../librpa_2d_coulomb_head.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} + +void require_near(const double actual, const double expected, const double tolerance, + const char* message) +{ + require(std::abs(actual - expected) <= tolerance, message); +} + +void test_mos2_rotated_shrink_basis_coefficient() +{ + constexpr double area_parallel_bohr2 = 31.347506240476704; + const std::vector> moments{{11.09877004933396}, {6.073507331002011}}; + const std::vector atoms_per_type{1, 2}; + const auto normalization = RpaLriDetail::strict_2d_coulomb_head_normalization( + area_parallel_bohr2, moments, atoms_per_type); + + require_near(normalization.multipole_norm_squared, 196.9576792074629, 2.0e-13, + "MoS2 shrink-basis monopole norm is wrong"); + require_near(normalization.raw_head_coefficient, 496.0890637033998, 5.0e-13, + "MoS2 raw 2D Coulomb head coefficient is wrong"); + require_near(normalization.sheet_to_raw_scale, 8.885664111490273, 2.0e-14, + "MoS2 sheet-to-raw scale is wrong"); +} + +void test_invalid_inputs_are_rejected() +{ + for (const double area : {0.0, -1.0, std::numeric_limits::infinity()}) + { + bool rejected = false; + try + { + (void)RpaLriDetail::strict_2d_coulomb_head_normalization( + area, {{1.0}}, {1}); + } + catch (const std::invalid_argument&) + { + rejected = true; + } + require(rejected, "invalid in-plane area was accepted"); + } +} + +void test_sidecar_uses_librpa_key_value_syntax() +{ + const auto normalization = RpaLriDetail::strict_2d_coulomb_head_normalization( + 31.347506240476704, {{11.09877004933396}, {6.073507331002011}}, {1, 2}); + const std::string sidecar = + RpaLriDetail::format_strict_2d_coulomb_head_sidecar(normalization); + + require(sidecar.find("version = 1\n") != std::string::npos, + "sidecar version is not in LibRPA key-value syntax"); + require(sidecar.find("strict_2d_coulomb_head_coefficient = ") != std::string::npos, + "sidecar coefficient is not in LibRPA key-value syntax"); + require(sidecar.find("strict_2d_sheet_to_raw_scale = ") != std::string::npos, + "sidecar scale is not in LibRPA key-value syntax"); +} +} // namespace + +int main() +{ + test_mos2_rotated_shrink_basis_coefficient(); + test_invalid_inputs_are_rejected(); + test_sidecar_uses_librpa_key_value_syntax(); + std::cout << "LibRPA strict 2D Coulomb head normalization tests passed\n"; + return 0; +} diff --git a/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_bz_sampling.cpp b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_bz_sampling.cpp new file mode 100644 index 00000000000..3e9d32d5065 --- /dev/null +++ b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_bz_sampling.cpp @@ -0,0 +1,82 @@ +#include "../librpa_bz_sampling.h" + +#include "source_base/constants.h" + +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} + +void test_stored_irreducible_points_keep_identity_labels() +{ + constexpr int stored_points = 8; + require(RpaLriDetail::librpa_stored_coulomb_q_count(stored_points) == stored_points, + "every stored Coulomb q point must be declared"); + for (int ik = 0; ik != stored_points; ++ik) + { + const auto index = RpaLriDetail::librpa_stored_q_index(ik, stored_points); + require(index.coulomb_irreducible_index == ik + 1, + "stored Coulomb q labels must be one-based and unique"); + require(index.representative_scf_index == ik + 1, + "stored SCF representative labels must refer to their own rows"); + } +} + +void test_cartesian_units_are_bohr_inverse() +{ + const auto kvec = RpaLriDetail::librpa_cartesian_kvec(ModuleBase::Vector3(0.25, -0.5, 0.0), 2.0); + const double scale = ModuleBase::TWO_PI / 2.0; + require(std::abs(kvec.x - 0.25 * scale) < 1e-12, + "BZ output must convert ABACUS 2*pi/lat0 coordinates to Bohr^-1"); + require(std::abs(kvec.y + 0.5 * scale) < 1e-12, + "BZ output must preserve the Cartesian vector components"); +} + +void test_invalid_counts_and_indices_are_rejected() +{ + bool rejected = false; + try + { + (void)RpaLriDetail::librpa_stored_coulomb_q_count(0); + } + catch (const std::invalid_argument&) + { + rejected = true; + } + require(rejected, "an empty stored q mesh must be rejected"); + + for (const int ik: {-1, 8}) + { + rejected = false; + try + { + (void)RpaLriDetail::librpa_stored_q_index(ik, 8); + } + catch (const std::invalid_argument&) + { + rejected = true; + } + require(rejected, "out-of-range stored q indices must be rejected"); + } +} +} // namespace + +int main() +{ + test_stored_irreducible_points_keep_identity_labels(); + test_invalid_counts_and_indices_are_rejected(); + test_cartesian_units_are_bohr_inverse(); + std::cout << "LibRPA BZ sampling tests passed\n"; + return 0; +} diff --git a/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_stru_symmetry_writers.cpp b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_stru_symmetry_writers.cpp new file mode 100644 index 00000000000..4442a6a3316 --- /dev/null +++ b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_stru_symmetry_writers.cpp @@ -0,0 +1,154 @@ +#include "../librpa_stru_symmetry.h" + +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_cell/module_symmetry/symmetry.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} + +std::vector split_lines(const std::string& text) +{ + std::vector lines; + std::istringstream input(text); + std::string line; + while (std::getline(input, line)) + { + lines.push_back(line); + } + return lines; +} + +void require_operation_row(const std::string& line, const char* message) +{ + std::istringstream input(line); + std::vector fields; + std::string field; + while (input >> field) + { + fields.push_back(field); + } + require(fields.size() == 12, message); + for (int i = 0; i < 9; ++i) + { + std::size_t consumed = 0; + (void)std::stoi(fields[i], &consumed); + require(consumed == fields[i].size(), "rotation fields must be integers"); + } + for (int i = 9; i < 12; ++i) + { + std::size_t consumed = 0; + const double value = std::stod(fields[i], &consumed); + require(consumed == fields[i].size() && std::isfinite(value), + "translation fields must be finite floating-point values"); + } +} + +void test_common_spatial_and_magnetic_rows() +{ + const RpaLriDetail::LibRpaSymmetryOperation unitary[] = { + {{1, 0, 0, 0, 1, 0, 0, 0, 1}, {0.0, 0.0, 0.0}}, + {{-1, 0, 0, 0, -1, 0, 0, 0, -1}, {0.5, 0.0, 0.0}}}; + const RpaLriDetail::LibRpaSymmetryOperation antiunitary[] = { + {{1, 0, 0, 0, -1, 0, 0, 0, -1}, {0.125, 0.25, 0.375}}}; + const std::vector unitary_operations(unitary, unitary + 2); + const std::vector antiunitary_operations(antiunitary, antiunitary + 1); + + std::ostringstream output; + RpaLriDetail::write_librpa_symmetry_rows(output, unitary_operations, antiunitary_operations); + const std::string serialized = output.str(); + const auto lines = split_lines(serialized); + + require(lines.size() == 4, "symmetry output must contain one count line and three rows"); + require(lines[0] == "3 row", "ordinary and antiunitary operations share one row block"); + require_operation_row(lines[1], "identity operation row must contain 12 fields"); + require_operation_row(lines[2], "ordinary inversion row must contain 12 fields"); + require_operation_row(lines[3], "antiunitary spatial row must contain 12 fields"); + require(lines[1].find("1.250000000000000e-01") == std::string::npos, + "ordinary rows must precede the antiunitary row"); + require(lines[3].find("1.250000000000000e-01") != std::string::npos, + "antiunitary translation must be preserved"); + require(serialized.find("spin_symmetry") == std::string::npos, + "legacy spin-specific trailer must not be emitted"); +} + +void test_explicit_spin_rows() +{ + const std::vector unitary_rotations = { + ModuleBase::Matrix3(), + ModuleBase::Matrix3(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)}; + const std::vector antiunitary_rotations = { + ModuleBase::Matrix3(0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0)}; + const auto operations = RpaLriDetail::make_librpa_spin_symmetry_operations( + ModuleBase::Matrix3(), unitary_rotations, antiunitary_rotations); + require(operations.size() == 3, "spin operations must preserve unitary and antiunitary order"); + require(operations[0].antiunitary == 0 && operations[1].antiunitary == 0, + "unitary spin operations must precede antiunitary operations"); + require(operations[2].antiunitary == 1, "antiunitary spin operation must be marked"); + require(std::abs(operations[0].spin_u[0] - 1.0) < 1e-12 + && std::abs(operations[0].spin_u[7]) < 1e-12, + "identity operation must export the identity SU(2) matrix"); + + std::ostringstream output; + RpaLriDetail::write_librpa_spin_symmetry(output, 0, 1, {operations[0], operations[2]}); + const auto lines = split_lines(output.str()); + require(lines.size() == 3, "spin symmetry output must contain one header and one row per operation"); + require(lines[0] == "spin_symmetry 0 1", "spin symmetry header must identify an explicit magnetic table"); + + std::istringstream unitary_row(lines[1]); + std::vector unitary_fields; + std::string field; + while (unitary_row >> field) + { + unitary_fields.push_back(field); + } + require(unitary_fields.size() == 9, "explicit spin rows must contain an anti flag and eight doubles"); + require(unitary_fields[0] == "0", "the first spin operation must be unitary"); + require(lines[2].rfind("1 ", 0) == 0, "the second spin operation must be antiunitary"); +} + +void test_production_symmetry_block() +{ + ModuleSymmetry::Symmetry symmetry; + symmetry.nrotk = 2; + symmetry.nrotk_anti = 1; + symmetry.magnetic_nspin4 = true; + symmetry.gmatrix[0] = ModuleBase::Matrix3(); + symmetry.gmatrix[1] = ModuleBase::Matrix3(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0); + symmetry.gtrans[1] = ModuleBase::Vector3(0.5, 0.0, 0.0); + symmetry.gmatrix_anti[0] = ModuleBase::Matrix3(0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0); + symmetry.gtrans_anti[0] = ModuleBase::Vector3(0.125, 0.25, 0.375); + + std::ostringstream output; + RpaLriDetail::write_librpa_symmetry_block(output, ModuleBase::Matrix3(), symmetry, 4); + const auto lines = split_lines(output.str()); + require(lines.size() == 8, "production symmetry block must contain spatial and spin rows"); + require(lines[0] == "3 row", "production block must export all spatial operations first"); + require(lines[4] == "spin_symmetry 0 1", "production block must export magnetic metadata for nspin=4"); + require(lines[7].rfind("1 ", 0) == 0, "production block must preserve the antiunitary flag"); +} +} // namespace + +int main() +{ + test_common_spatial_and_magnetic_rows(); + test_explicit_spin_rows(); + test_production_symmetry_block(); + std::cout << "LibRPA stru_out symmetry tests passed\n"; + return 0; +} diff --git a/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_stru_units.cpp b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_stru_units.cpp new file mode 100644 index 00000000000..a1a3318cd85 --- /dev/null +++ b/source/source_lcao/module_rpa/module_librpa_io/unittests/test_librpa_stru_units.cpp @@ -0,0 +1,120 @@ +#include "../librpa_stru_units.h" + +#include "source_base/constants.h" + +#include +#include +#include +#include +#include + +namespace +{ +void require(const bool condition, const char* message) +{ + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} + +void require_near(const double actual, const double expected, const double tolerance, const char* message) +{ + require(std::abs(actual - expected) <= tolerance, message); +} + +void test_general_lat0() +{ + const double lat0_bohr = 5.3; + const auto scales = RpaLriDetail::librpa_stru_unit_scales(lat0_bohr); + + require(scales.real_space_bohr == lat0_bohr, + "real-space stru_out scale must use the actual lattice constant"); + require_near(scales.reciprocal_space_bohr_inv, + ModuleBase::TWO_PI / lat0_bohr, + 0.0, + "reciprocal-space stru_out scale must use 2*pi/lat0"); + require_near(scales.real_space_bohr * scales.reciprocal_space_bohr_inv, + ModuleBase::TWO_PI, + 4.0 * std::numeric_limits::epsilon(), + "real and reciprocal stru_out scales must be mutually consistent"); +} + +void test_exact_one_angstrom_compatibility() +{ + const double lat0_bohr = 1.0 / ModuleBase::BOHR_TO_A; + const auto scales = RpaLriDetail::librpa_stru_unit_scales(lat0_bohr); + + require(scales.real_space_bohr == 1.0 / ModuleBase::BOHR_TO_A, + "exact one-Angstrom real-space scale must match the legacy conversion"); + require(scales.reciprocal_space_bohr_inv == ModuleBase::TWO_PI * ModuleBase::BOHR_TO_A, + "exact one-Angstrom reciprocal scale must match the legacy conversion"); +} + +void test_common_truncated_one_angstrom_constant() +{ + const double lat0_bohr = 1.8897162; + const auto scales = RpaLriDetail::librpa_stru_unit_scales(lat0_bohr); + const double legacy_real_scale = 1.0 / ModuleBase::BOHR_TO_A; + const double legacy_reciprocal_scale = ModuleBase::TWO_PI * ModuleBase::BOHR_TO_A; + const double real_relative_change = std::abs(scales.real_space_bohr / legacy_real_scale - 1.0); + const double reciprocal_relative_change + = std::abs(scales.reciprocal_space_bohr_inv / legacy_reciprocal_scale - 1.0); + const double expected_real_relative_change + = std::abs(lat0_bohr * ModuleBase::BOHR_TO_A - 1.0); + const double expected_reciprocal_relative_change + = std::abs(1.0 / (lat0_bohr * ModuleBase::BOHR_TO_A) - 1.0); + + require_near(real_relative_change, + expected_real_relative_change, + std::numeric_limits::epsilon(), + "the common-case real-space change must equal the input conversion error"); + require_near(reciprocal_relative_change, + expected_reciprocal_relative_change, + std::numeric_limits::epsilon(), + "the common-case reciprocal change must equal the input conversion error"); + require(real_relative_change < 5.7e-6, + "the common truncated one-Angstrom constant must stay within its conversion error"); + require(reciprocal_relative_change < 5.7e-6, + "the reciprocal common-case change must stay within its conversion error"); + require_near(scales.real_space_bohr * scales.reciprocal_space_bohr_inv, + ModuleBase::TWO_PI, + 4.0 * std::numeric_limits::epsilon(), + "the common truncated constant must still produce reciprocal geometry"); + + const double tau_component = 0.573; + const double legacy_direct_position = tau_component * lat0_bohr; + const double new_direct_position = tau_component * scales.real_space_bohr; + require(new_direct_position == legacy_direct_position, + "Direct-coordinate atom output must remain exactly unchanged"); +} + +void test_invalid_lat0() +{ + for (const double value : {0.0, -1.0, std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) + { + bool rejected = false; + try + { + (void)RpaLriDetail::librpa_stru_unit_scales(value); + } + catch (const std::invalid_argument&) + { + rejected = true; + } + require(rejected, "invalid lattice constants must be rejected"); + } +} +} // namespace + +int main() +{ + test_general_lat0(); + test_exact_one_angstrom_compatibility(); + test_common_truncated_one_angstrom_constant(); + test_invalid_lat0(); + std::cout << "LibRPA stru_out unit tests passed\n"; + return 0; +} diff --git a/source/source_lcao/module_rpa/rpa_abfs_preorthogonalization.cpp b/source/source_lcao/module_rpa/rpa_abfs_preorthogonalization.cpp new file mode 100644 index 00000000000..a93a6a76901 --- /dev/null +++ b/source/source_lcao/module_rpa/rpa_abfs_preorthogonalization.cpp @@ -0,0 +1,400 @@ +#include "rpa_abfs_preorthogonalization.h" + +#include "source_base/constants.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +constexpr double kGridTolerance = 1.0e-12; +constexpr double kOrthogonalityTolerance = 1.0e-20; +constexpr int kMaximumProjectionPasses = 4; +constexpr double kIdentityTolerance = 1.0e-10; + +int expanded_size(const RpaAbfsOrbitalSet& abfs) +{ + int result = 0; + for (const auto& type: abfs) + { + for (std::size_t l = 0; l != type.size(); ++l) + { + result += static_cast(type[l].size() * (2 * l + 1)); + } + } + return result; +} + +std::vector simpson_weights(const Numerical_Orbital_Lm& reference) +{ + const int nr = reference.getNr(); + if (nr < 3 || nr % 2 == 0) + { + throw std::invalid_argument( + "RPA ABFS Coulomb preorthogonalization requires an odd radial mesh with at least three points"); + } + + std::vector weights(nr, 0.0); + for (int ir = 0; ir != nr; ++ir) + { + const double coefficient = (ir == 0 || ir == nr - 1) ? 1.0 : (ir % 2 == 0 ? 2.0 : 4.0); + weights[ir] = coefficient * reference.getRab(ir) / 3.0; + } + return weights; +} + +void validate_channel(const std::vector& channel) +{ + if (channel.empty()) + { + return; + } + const Numerical_Orbital_Lm& reference = channel.front(); + for (std::size_t i = 0; i != channel.size(); ++i) + { + const Numerical_Orbital_Lm& orbital = channel[i]; + if (orbital.getType() != reference.getType() || orbital.getL() != reference.getL() + || orbital.getNr() != reference.getNr()) + { + throw std::invalid_argument( + "RPA ABFS Coulomb preorthogonalization requires one consistent (type,l) radial channel"); + } + for (int ir = 0; ir != reference.getNr(); ++ir) + { + if (std::abs(orbital.getRadial(ir) - reference.getRadial(ir)) + > kGridTolerance * std::max(1.0, std::abs(reference.getRadial(ir))) + || std::abs(orbital.getRab(ir) - reference.getRab(ir)) + > kGridTolerance * std::max(1.0, std::abs(reference.getRab(ir)))) + { + throw std::invalid_argument( + "RPA ABFS Coulomb preorthogonalization requires identical radial grids within a channel"); + } + } + } +} + +double metric_product(const std::vector& lhs, + const std::vector>& metric, + const std::vector& rhs) +{ + double result = 0.0; + for (std::size_t i = 0; i != lhs.size(); ++i) + { + for (std::size_t j = 0; j != rhs.size(); ++j) + { + result += lhs[i] * metric[i][j] * rhs[j]; + } + } + return result; +} + +std::vector metric_overlaps(const std::vector>& accepted, + const std::vector>& metric, + const std::vector& candidate) +{ + std::vector overlaps; + overlaps.reserve(accepted.size()); + for (const auto& vector: accepted) + { + overlaps.push_back(metric_product(vector, metric, candidate)); + } + return overlaps; +} + +double squared_norm(const std::vector& values) +{ + return std::inner_product(values.begin(), values.end(), values.begin(), 0.0); +} + +Numerical_Orbital_Lm rebuild_orbital(const std::vector& original, + const std::vector& coefficients, + const int output_index, + const bool force_flag) +{ + const Numerical_Orbital_Lm& reference = original.front(); + std::vector psi(reference.getNr(), 0.0); + for (std::size_t input_index = 0; input_index != original.size(); ++input_index) + { + for (int ir = 0; ir != reference.getNr(); ++ir) + { + psi[ir] += coefficients[input_index] * original[input_index].getPsi(ir); + } + } + + Numerical_Orbital_Lm output; + output.set_orbital_info(reference.getLabel(), + reference.getType(), + reference.getL(), + output_index, + reference.getNr(), + reference.getRab(), + reference.getRadial(), + Numerical_Orbital_Lm::Psi_Type::Psi, + psi.data(), + reference.getNk(), + reference.getDk(), + reference.getDruniform(), + false, + true, + force_flag); + return output; +} + +RpaAbfsPreorthChannelReport preorthogonalize_channel(std::vector& channel, + const double threshold, + const bool force_flag) +{ + validate_channel(channel); + const int atom_type = channel.front().getType(); + const int angular_momentum = channel.front().getL(); + const auto metric = compute_onsite_coulomb_metric(channel); + std::vector> accepted; + std::vector rejected; + double minimum_residual = std::numeric_limits::infinity(); + + for (std::size_t input_index = 0; input_index != channel.size(); ++input_index) + { + std::vector candidate(channel.size(), 0.0); + candidate[input_index] = 1.0; + const double input_norm = metric_product(candidate, metric, candidate); + if (!std::isfinite(input_norm) || input_norm <= 0.0) + { + throw std::invalid_argument( + "RPA ABFS Coulomb preorthogonalization found a non-positive input norm for type " + + std::to_string(atom_type) + ", l " + std::to_string(angular_momentum) + ", radial " + + std::to_string(input_index)); + } + for (double& value: candidate) + { + value /= std::sqrt(input_norm); + } + + std::vector overlaps = metric_overlaps(accepted, metric, candidate); + double residual_norm = 1.0 - squared_norm(overlaps); + if (!std::isfinite(residual_norm) || residual_norm < -threshold) + { + throw std::runtime_error("RPA ABFS Coulomb preorthogonalization produced an invalid residual norm"); + } + residual_norm = std::max(0.0, residual_norm); + minimum_residual = std::min(minimum_residual, residual_norm); + if (residual_norm <= threshold) + { + rejected.push_back(static_cast(input_index)); + continue; + } + + bool orthogonal = accepted.empty(); + for (int pass = 0; pass != kMaximumProjectionPasses && !orthogonal; ++pass) + { + for (std::size_t i = 0; i != accepted.size(); ++i) + { + for (std::size_t j = 0; j != candidate.size(); ++j) + { + candidate[j] -= overlaps[i] * accepted[i][j]; + } + } + const double norm = metric_product(candidate, metric, candidate); + if (!std::isfinite(norm) || norm <= 0.0) + { + throw std::invalid_argument("RPA ABFS Coulomb preorthogonalization lost a positive residual norm"); + } + for (double& value: candidate) + { + value /= std::sqrt(norm); + } + overlaps = metric_overlaps(accepted, metric, candidate); + orthogonal = squared_norm(overlaps) <= kOrthogonalityTolerance; + } + if (!orthogonal) + { + throw std::runtime_error("RPA ABFS Coulomb preorthogonalization did not reach the orthogonality tolerance"); + } + accepted.push_back(std::move(candidate)); + } + + if (accepted.empty()) + { + throw std::invalid_argument("RPA ABFS Coulomb preorthogonalization rejected an entire radial channel"); + } + + const std::vector original = channel; + channel.clear(); + channel.reserve(accepted.size()); + for (std::size_t output_index = 0; output_index != accepted.size(); ++output_index) + { + channel.push_back( + rebuild_orbital(original, accepted[output_index], static_cast(output_index), force_flag)); + } + + const auto transformed_metric = compute_onsite_coulomb_metric(channel); + double maximum_identity_error = 0.0; + for (std::size_t i = 0; i != transformed_metric.size(); ++i) + { + for (std::size_t j = 0; j != transformed_metric.size(); ++j) + { + const double target = i == j ? 1.0 : 0.0; + maximum_identity_error = std::max(maximum_identity_error, std::abs(transformed_metric[i][j] - target)); + } + } + if (!std::isfinite(maximum_identity_error) || maximum_identity_error > kIdentityTolerance) + { + throw std::runtime_error("RPA ABFS Coulomb preorthogonalization failed its on-site identity check"); + } + + RpaAbfsPreorthChannelReport report; + report.atom_type = atom_type; + report.angular_momentum = angular_momentum; + report.input_count = static_cast(original.size()); + report.output_count = static_cast(channel.size()); + report.rejected_indices = std::move(rejected); + report.minimum_residual_norm = minimum_residual; + report.maximum_identity_error = maximum_identity_error; + return report; +} + +} // namespace + +std::vector> compute_onsite_coulomb_metric(const std::vector& channel) +{ + validate_channel(channel); + if (channel.empty()) + { + return {}; + } + + const Numerical_Orbital_Lm& reference = channel.front(); + const int nr = reference.getNr(); + const int l = reference.getL(); + const std::vector weights = simpson_weights(reference); + std::vector> metric(channel.size(), std::vector(channel.size(), 0.0)); + + for (std::size_t b = 0; b != channel.size(); ++b) + { + std::vector prefix(nr, 0.0); + std::vector suffix(nr, 0.0); + for (int ir = 1; ir != nr; ++ir) + { + const double r = reference.getRadial(ir); + if (!(r > 0.0) || !std::isfinite(r)) + { + throw std::invalid_argument( + "RPA ABFS Coulomb preorthogonalization requires positive finite radii after the origin"); + } + prefix[ir] = prefix[ir - 1] + weights[ir] * std::pow(r, l + 2) * channel[b].getPsi(ir); + } + for (int ir = nr - 2; ir >= 1; --ir) + { + const double r_next = reference.getRadial(ir + 1); + suffix[ir] = suffix[ir + 1] + weights[ir + 1] * std::pow(r_next, 1 - l) * channel[b].getPsi(ir + 1); + } + + for (std::size_t a = 0; a != channel.size(); ++a) + { + double value = 0.0; + for (int ir = 1; ir != nr; ++ir) + { + const double r = reference.getRadial(ir); + const double potential = std::pow(r, -l - 1) * prefix[ir] + std::pow(r, l) * suffix[ir]; + value += weights[ir] * r * r * channel[a].getPsi(ir) * potential; + } + metric[a][b] = ModuleBase::FOUR_PI * value / (2 * l + 1); + } + } + + for (std::size_t i = 0; i != metric.size(); ++i) + { + for (std::size_t j = i + 1; j != metric.size(); ++j) + { + const double symmetric = 0.5 * (metric[i][j] + metric[j][i]); + metric[i][j] = symmetric; + metric[j][i] = symmetric; + } + } + return metric; +} + +RpaAbfsPreorthReport apply_rpa_abfs_preorthogonalization(RpaAbfsOrbitalSet& abfs, + const std::string& mode, + const double threshold, + const bool force_flag) +{ + if (mode != "none" && mode != "onsite_coulomb") + { + throw std::invalid_argument("rpa_abfs_preorth must be none or onsite_coulomb"); + } + if (!std::isfinite(threshold) || threshold <= 0.0 || threshold >= 1.0) + { + throw std::invalid_argument("rpa_abfs_preorth_threshold must be finite and strictly inside (0,1)"); + } + + RpaAbfsPreorthReport report; + report.mode = mode; + report.threshold = threshold; + report.input_expanded_size = expanded_size(abfs); + report.output_expanded_size = report.input_expanded_size; + if (mode == "none") + { + return report; + } + + for (auto& type: abfs) + { + for (auto& channel: type) + { + if (!channel.empty()) + { + report.channels.push_back(preorthogonalize_channel(channel, threshold, force_flag)); + } + } + } + report.output_expanded_size = expanded_size(abfs); + return report; +} + +RpaAbfsPreorthReport finalize_rpa_abfs_from_input(RpaAbfsOrbitalSet& abfs, + const Input_para& input, + const bool force_flag) +{ + return apply_rpa_abfs_preorthogonalization(abfs, + input.rpa_abfs_preorth, + input.rpa_abfs_preorth_threshold, + force_flag); +} + +std::string format_rpa_abfs_preorth_report(const RpaAbfsPreorthReport& report) +{ + std::ostringstream output; + output << std::setprecision(16) << "RPA_ABFS_PREORTH mode=" << report.mode << " threshold=" << report.threshold + << " input_expanded_size=" << report.input_expanded_size + << " output_expanded_size=" << report.output_expanded_size << '\n'; + for (const auto& channel: report.channels) + { + output << "RPA_ABFS_PREORTH_CHANNEL type=" << channel.atom_type << " l=" << channel.angular_momentum + << " input_count=" << channel.input_count << " output_count=" << channel.output_count + << " rejected_indices="; + if (channel.rejected_indices.empty()) + { + output << "none"; + } + else + { + for (std::size_t i = 0; i != channel.rejected_indices.size(); ++i) + { + output << (i == 0 ? "" : ",") << channel.rejected_indices[i]; + } + } + output << " minimum_residual_norm=" << channel.minimum_residual_norm + << " maximum_identity_error=" << channel.maximum_identity_error << '\n'; + } + return output.str(); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/rpa_abfs_preorthogonalization.h b/source/source_lcao/module_rpa/rpa_abfs_preorthogonalization.h new file mode 100644 index 00000000000..786b2a43b56 --- /dev/null +++ b/source/source_lcao/module_rpa/rpa_abfs_preorthogonalization.h @@ -0,0 +1,48 @@ +#ifndef MODULE_RI_RPA_ABFS_PREORTHOGONALIZATION_H +#define MODULE_RI_RPA_ABFS_PREORTHOGONALIZATION_H + +#include "source_basis/module_ao/orb_atomic_lm.h" +#include "source_io/module_parameter/input_parameter.h" + +#include +#include + +namespace ModuleRI +{ + +using RpaAbfsOrbitalSet = std::vector>>; + +struct RpaAbfsPreorthChannelReport +{ + int atom_type = -1; + int angular_momentum = -1; + int input_count = 0; + int output_count = 0; + std::vector rejected_indices; + double minimum_residual_norm = 1.0; + double maximum_identity_error = 0.0; +}; + +struct RpaAbfsPreorthReport +{ + std::string mode = "none"; + double threshold = 1.0e-2; + int input_expanded_size = 0; + int output_expanded_size = 0; + std::vector channels; +}; + +std::vector> compute_onsite_coulomb_metric(const std::vector& channel); + +RpaAbfsPreorthReport apply_rpa_abfs_preorthogonalization(RpaAbfsOrbitalSet& abfs, + const std::string& mode, + double threshold, + bool force_flag); + +RpaAbfsPreorthReport finalize_rpa_abfs_from_input(RpaAbfsOrbitalSet& abfs, const Input_para& input, bool force_flag); + +std::string format_rpa_abfs_preorth_report(const RpaAbfsPreorthReport& report); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/rpa_lri.cpp b/source/source_lcao/module_rpa/rpa_lri.cpp index 7c9cde7ba73..84d1fe91cba 100644 --- a/source/source_lcao/module_rpa/rpa_lri.cpp +++ b/source/source_lcao/module_rpa/rpa_lri.cpp @@ -45,7 +45,14 @@ try ModuleBase::timer::start("RPA_LRI", "postSCF"); ModuleBase::GlobalFunc::MAKE_DIR(this->runtime.input.rpa_outdir); this->ccp_rmesh_times_cut = this->runtime.input.rpa_ccp_rmesh_times; - this->ccp_rmesh_times_ewald = this->info.ccp_rmesh_times; // should be `exx_ccp_rmesh_times` + this->ccp_rmesh_times_ewald = this->info.ccp_rmesh_times; + if (this->runtime.input.out_librpa_abf_overlap + && (!this->runtime.input.rpa || this->runtime.input.out_librpa_ver != 1 + || this->info.shrink_abfs_pca_thr < 0.0)) + { + throw std::runtime_error("out_librpa_abf_overlap requires rpa=true, " + "out_librpa_ver=1, and shrink ABFs."); + } // should be `exx_ccp_rmesh_times` this->cal_postSCF_exx(dm, mpi_comm_in, ucell, kv, orb, parav); this->init(mpi_comm_in, kv, orb.cutoffs()); diff --git a/source/source_lcao/module_rpa/rpa_lri.h b/source/source_lcao/module_rpa/rpa_lri.h index e9a98c0bf53..5928b8de226 100644 --- a/source/source_lcao/module_rpa/rpa_lri.h +++ b/source/source_lcao/module_rpa/rpa_lri.h @@ -9,6 +9,7 @@ #include "source_lcao/module_ri/lri_cv.h" #include "source_hamilt/module_xc/exx_info_ri.h" #include "source_psi/psi.h" +#include "source_lcao/module_ri/module_exx_symmetry/symm_rotation.h" // #include "module_xc/exx_info.h" // #include "source_basis/module_ao/orb_atomic_lm.h" #include "source_base/matrix.h" @@ -110,6 +111,9 @@ template class RPA_LRI std::string filename, const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs_s, const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs); + void out_abfs_overlap_raw_v1(const UnitCell& ucell, + const std::map>>& overlap_abfs_abfs, + const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs_s); void out_eigen_vector(const Parallel_Orbitals& parav, const psi::Psi& psi); void out_struc(const UnitCell& ucell); void out_bz_sampling(const UnitCell& ucell); @@ -117,9 +121,10 @@ template class RPA_LRI void out_velocity(const UnitCell& ucell, const Grid_Driver& gd, const TwoCenterBundle& two_center_bundle, - const Parallel_Orbitals& parav,/*nbasis×nbasis*/ + const Parallel_Orbitals& parav, const psi::Psi& psi, const elecstate::ElecState* pelec); + void output_cut_coulomb_cs(const UnitCell& ucell, Exx_LRI* exx_lri_rpa); void out_Cs(const UnitCell& ucell, std::map>>& Cs_in, std::string filename); void out_Cs_v1(const UnitCell& ucell, std::map>>& Cs_in, std::string filename); @@ -133,9 +138,9 @@ template class RPA_LRI Exx_LRI* exx_lri); void out_librpa_basis_v1(const UnitCell& ucell, Exx_LRI* exx_lri, - const std::string& aux_filename, - const std::string& legacy_filename, - bool write_wfc_basis); + const std::string& aux_filename = "basis_aux_out", + const std::string& legacy_filename = "basis_out", + const bool write_wfc_basis = true); // void print_matrix(char *desc, const ModuleBase::matrix &mat); // void print_complex_matrix(char *desc, const ModuleBase::ComplexMatrix &mat); // void init(const MPI_Comm &mpi_comm_in); @@ -150,7 +155,7 @@ template class RPA_LRI std::vector collect_atom_naux_(const UnitCell& ucell, Exx_LRI* exx_lri) const; const ModuleRI::RpaLriRuntime runtime; - Exx_Info_RI info; + const Exx_Info_RI &info; const K_Vectors *p_kv=nullptr; MPI_Comm mpi_comm; std::vector orb_cutoff_; @@ -172,6 +177,8 @@ template class RPA_LRI // Tdata post_process_Erpa( const Tdata &Erpa_in ) const; + ModuleSymmetry::Symmetry_rotation symmetry_rotation_; + bool use_spacegroup_symmetry_ = false; std::unique_ptr> exx_cut_coulomb; std::unique_ptr> exx_full_coulomb; }; diff --git a/source/source_lcao/module_rpa/rpa_lri_coulomb.cpp b/source/source_lcao/module_rpa/rpa_lri_coulomb.cpp index 00246bbdc13..6251f7dce3e 100644 --- a/source/source_lcao/module_rpa/rpa_lri_coulomb.cpp +++ b/source/source_lcao/module_rpa/rpa_lri_coulomb.cpp @@ -5,6 +5,7 @@ #include "source_basis/module_ao/elem_basis_idx_orb.h" #include "source_estate/elecstate_lcao.h" #include "source_io/module_parameter/input_parameter.h" +#include "source_lcao/module_rpa/module_librpa_io/librpa_2d_coulomb_head.h" #include "source_lcao/module_ri/module_exx_symmetry/symm_rotation.h" #include @@ -145,6 +146,98 @@ void RPA_LRI::output_ewald_coulomb(const UnitCell& ucell, const K_Vect exx_full_coulomb->init(mpi_comm, ucell, kv, orb, this->abfs_shrink); else exx_full_coulomb->init(mpi_comm, ucell, kv, orb, this->abfs); + + const auto write_strict_2d_coulomb_head_sidecar = [&]() { + if (this->info.ewald_dimension != 2 || this->runtime.rank != 0) + { + return; + } + const auto multipoles = Exx_Abfs::Construct_Orbs::get_multipole(exx_full_coulomb->abfs); + std::vector> s_multipoles_by_type( + static_cast(ucell.ntype)); + std::vector atoms_per_type(static_cast(ucell.ntype), 0); + for (int it = 0; it != ucell.ntype; ++it) + { + if (static_cast(it) < multipoles.size() + && !multipoles[static_cast(it)].empty()) + { + s_multipoles_by_type[static_cast(it)] + = multipoles[static_cast(it)][0]; + } + atoms_per_type[static_cast(it)] = ucell.atoms[it].na; + } + const ModuleBase::Vector3 a1_bohr = ucell.a1 * ucell.lat0; + const ModuleBase::Vector3 a2_bohr = ucell.a2 * ucell.lat0; + const auto normalization = RpaLriDetail::strict_2d_coulomb_head_normalization( + (a1_bohr ^ a2_bohr).norm(), s_multipoles_by_type, atoms_per_type); + + const std::string filename = this->runtime.input.rpa_outdir + "librpa_2d_coulomb_head.txt"; + std::ofstream ofs(filename, std::ios::out | std::ios::trunc); + if (!ofs.good()) + { + throw std::runtime_error("Failed to open " + filename); + } + ofs << RpaLriDetail::format_strict_2d_coulomb_head_sidecar(normalization); + if (!ofs.good()) + { + throw std::runtime_error("Failed to write " + filename); + } + std::cout << "Wrote strict 2D Coulomb head normalization to " << filename + << ": A_lambda=" << std::setprecision(17) + << normalization.raw_head_coefficient + << ", sheet_to_raw_scale=" << normalization.sheet_to_raw_scale + << std::endl; + }; + + const bool use_direct_2d_coulomb + = this->runtime.input.out_librpa_2d_coulomb_method == "direct_mixed_fourier"; + const bool use_direct_3d_coulomb + = this->runtime.input.out_librpa_3d_coulomb_method == "direct_reciprocal"; + if (use_direct_2d_coulomb || use_direct_3d_coulomb) + { + if (this->runtime.input.out_librpa_ver != 1) + { + throw std::invalid_argument("Direct Coulomb output requires out_librpa_ver=1."); + } + const int required_dimension = use_direct_2d_coulomb ? 2 : 3; + if (this->info.ewald_dimension != required_dimension) + { + throw std::invalid_argument( + "Direct Coulomb output dimension does not match exx_ewald_dimension."); + } + + const bool use_shrink = this->info.shrink_abfs_pca_thr >= 0.0; + const std::string aux_filename = use_shrink ? "aux_basis_s.txt" : "aux_basis.txt"; + const std::string legacy_filename = use_shrink ? "" : "basis_map.txt"; + this->out_librpa_basis_v1(ucell, exx_full_coulomb.get(), aux_filename, legacy_filename, !use_shrink); + const auto ewald_object + = exx_full_coulomb->exx_objs.find(Conv_Coulomb_Pot_K::Coulomb_Method::Ewald); + if (ewald_object == exx_full_coulomb->exx_objs.end()) + { + throw std::runtime_error( + "Direct Coulomb output could not find the Ewald auxiliary object."); + } + if (use_direct_2d_coulomb) + { + ewald_object->second.evq.output_direct_2d_coulomb( + ucell, + this->runtime.input.out_librpa_2d_direct_ecut, + this->runtime.input.out_librpa_2d_direct_kz_order, + this->runtime.input.out_librpa_2d_direct_gamma_order); + write_strict_2d_coulomb_head_sidecar(); + } + else + { + ewald_object->second.evq.output_direct_3d_coulomb( + ucell, this->runtime.input.out_librpa_3d_direct_ecut); + } + + exx_full_coulomb.reset(); + RpaLriDetail::trim_malloc_cache(); + ModuleBase::timer::end("RPA_LRI", "output_ewald_coulomb"); + return; + } + std::map>> Vs_full_IJR; std::map>> Cs; std::map>> tmp; @@ -184,6 +277,7 @@ void RPA_LRI::output_ewald_coulomb(const UnitCell& ucell, const K_Vect // call emits only the reduced auxiliary basis. this->out_librpa_basis_v1(ucell, exx_full_coulomb.get(), aux_filename, legacy_filename, !use_shrink); this->out_coulomb_k_v1(ucell, this->Vs_period, "V_full_", exx_full_coulomb.get()); + write_strict_2d_coulomb_head_sidecar(); } else { diff --git a/source/source_lcao/module_rpa/rpa_lri_detail.h b/source/source_lcao/module_rpa/rpa_lri_detail.h index 6d40779ab44..f27c2a61e30 100644 --- a/source/source_lcao/module_rpa/rpa_lri_detail.h +++ b/source/source_lcao/module_rpa/rpa_lri_detail.h @@ -29,6 +29,9 @@ constexpr int LIBRPA_SHRINK_SINVS_V1_MARKER = -30241621; constexpr int LIBRPA_KS_EIGENVECTOR_V1_MARKER = -12345679; constexpr int LIBRPA_KS_EIGENVECTOR_V1_KIND_COMPLEX_DOUBLE = 28; constexpr int LIBRPA_COULOMB_V1_COMPLEX_FLAG = 1; +constexpr int LIBRPA_ABF_OVERLAP_V1_MARKER = -40817329; +constexpr int LIBRPA_ABF_OVERLAP_V1_VERSION = 1; +constexpr int LIBRPA_ABF_OVERLAP_V1_KIND_ACTIVE = 1; static_assert(sizeof(std::complex) == 2 * sizeof(double), "LibRPA v1 binary output expects complex as two doubles."); diff --git a/source/source_lcao/module_rpa/rpa_lri_overlap_io.cpp b/source/source_lcao/module_rpa/rpa_lri_overlap_io.cpp index 91e8ac6b70f..89576b4315d 100644 --- a/source/source_lcao/module_rpa/rpa_lri_overlap_io.cpp +++ b/source/source_lcao/module_rpa/rpa_lri_overlap_io.cpp @@ -291,6 +291,11 @@ try } } + if (this->runtime.input.out_librpa_abf_overlap) + { + out_abfs_overlap_raw_v1(ucell, overlap_abfs_abfs, index_abfs_s); + } + inverse_olp(ucell, olp_q_ss, index_abfs_s); std::vector records; @@ -409,6 +414,320 @@ catch (...) RpaLriDetail::abort_output(this->mpi_comm, "Unknown exception in out_abfs_overlap_v1"); } + +template +void RPA_LRI::out_abfs_overlap_raw_v1( + const UnitCell& ucell, + const std::map>>& overlap_abfs_abfs, + const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs_s) +{ + if (!this->runtime.input.rpa || this->runtime.input.out_librpa_ver != 1 + || this->info.shrink_abfs_pca_thr < 0.0) + { + throw std::runtime_error("raw active-ABF overlap writer requires rpa=true, " + "out_librpa_ver=1, and shrink ABFs."); + } + + const int natom = ucell.nat; + if (natom <= 0) + { + throw std::runtime_error("raw active-ABF overlap writer found no atoms."); + } + std::vector atom_naux(static_cast(natom), 0); + std::vector atom_shift(static_cast(natom), 0); + int naux = 0; + for (int I = 0; I < natom; ++I) + { + const int count = index_abfs_s[ucell.iat2it[I]].count_size; + if (count <= 0 || naux > std::numeric_limits::max() - count) + { + throw std::runtime_error("raw active-ABF overlap writer found an invalid active basis layout."); + } + atom_shift[static_cast(I)] = naux; + atom_naux[static_cast(I)] = count; + naux += count; + } + + const int nks_tot = this->runtime.input.nspin == 2 ? static_cast(p_kv->get_nks()) / 2 : p_kv->get_nks(); + if (nks_tot <= 0) + { + throw std::runtime_error("raw active-ABF overlap writer found no q points."); + } + const double hermitian_tol = 1e-10; + const double rank_tol = 1e-12; + const int natom_metadata = natom; + int natom_min = 0; + int natom_max = 0; + MPI_Allreduce(&natom_metadata, &natom_min, 1, MPI_INT, MPI_MIN, mpi_comm); + MPI_Allreduce(&natom_metadata, &natom_max, 1, MPI_INT, MPI_MAX, mpi_comm); + if (natom_min != natom_metadata || natom_max != natom_metadata) + { + throw std::runtime_error("raw active-ABF overlap writer found rank-inconsistent atom metadata."); + } + for (const int count: atom_naux) + { + int min_count = 0; + int max_count = 0; + MPI_Allreduce(&count, &min_count, 1, MPI_INT, MPI_MIN, mpi_comm); + MPI_Allreduce(&count, &max_count, 1, MPI_INT, MPI_MAX, mpi_comm); + if (min_count != count || max_count != count) + { + throw std::runtime_error("raw active-ABF overlap writer found rank-inconsistent active basis metadata."); + } + } + + // comm_map2_first may concentrate all R blocks for one (I,J) on one rank. + // Check only actual post-communication keys; no complete R coverage is assumed. + int local_invalid_metadata = 0; + std::string local_metadata_error; + std::vector> local_keys; + std::vector local_pair_seen(static_cast(natom) * static_cast(natom), 0); + for (const auto& Ip: overlap_abfs_abfs) + { + const int I = Ip.first; + if (I < 0 || I >= natom) + { + local_invalid_metadata = 1; + if (local_metadata_error.empty()) + { + local_metadata_error = "raw active-ABF overlap writer found an invalid row atom."; + } + continue; + } + for (const auto& JPp: Ip.second) + { + const int J = JPp.first.first; + const auto R = JPp.first.second; + if (J < 0 || J >= natom) + { + local_invalid_metadata = 1; + if (local_metadata_error.empty()) + { + local_metadata_error = "raw active-ABF overlap writer found invalid block (I,J,R)=(" + + std::to_string(I) + "," + std::to_string(J) + "," + + std::to_string(R[0]) + "," + std::to_string(R[1]) + "," + + std::to_string(R[2]) + ")."; + } + continue; + } + const auto& tensor = JPp.second; + if (tensor.shape.size() != 2 + || tensor.shape[0] != atom_naux[static_cast(I)] + || tensor.shape[1] != atom_naux[static_cast(J)]) + { + local_invalid_metadata = 1; + if (local_metadata_error.empty()) + { + local_metadata_error = "raw active-ABF overlap writer found inconsistent metadata for block (I,J,R)=(" + + std::to_string(I) + "," + std::to_string(J) + "," + + std::to_string(R[0]) + "," + std::to_string(R[1]) + "," + + std::to_string(R[2]) + ")."; + } + continue; + } + local_keys.push_back({I, J, R[0], R[1], R[2]}); + local_pair_seen[static_cast(I) * static_cast(natom) + + static_cast(J)] = 1; + } + } + int global_invalid_metadata = 0; + MPI_Allreduce(&local_invalid_metadata, &global_invalid_metadata, 1, MPI_INT, MPI_MAX, mpi_comm); + if (global_invalid_metadata != 0) + { + throw std::runtime_error(local_metadata_error.empty() + ? "raw active-ABF overlap writer found invalid block metadata on another rank." + : local_metadata_error); + } + for (int& seen: local_pair_seen) + { + int global_seen = 0; + MPI_Allreduce(&seen, &global_seen, 1, MPI_INT, MPI_MAX, mpi_comm); + seen = global_seen; + } + for (std::size_t pair_index = 0; pair_index < local_pair_seen.size(); ++pair_index) + { + if (local_pair_seen[pair_index] == 0) + { + throw std::runtime_error("raw active-ABF overlap writer found a missing atom pair."); + } + } + + // Sorting is also a defensive local-map duplicate check; std::map normally + // makes such a duplicate impossible. The gathered check below rejects both + // local and cross-rank repeats and reports the same concrete key on all ranks. + std::sort(local_keys.begin(), local_keys.end()); + const bool local_duplicate = std::adjacent_find(local_keys.begin(), local_keys.end()) != local_keys.end(); + int global_duplicate_flag = 0; + int mpi_size = 1; + MPI_Comm_size(mpi_comm, &mpi_size); + const std::size_t key_width = 5; + const bool local_count_overflow = local_keys.size() > static_cast(std::numeric_limits::max()) / key_width; + int any_count_overflow = local_count_overflow ? 1 : 0; + MPI_Allreduce(&any_count_overflow, &global_duplicate_flag, 1, MPI_INT, MPI_MAX, mpi_comm); + if (global_duplicate_flag != 0) + { + throw std::runtime_error("raw active-ABF overlap writer cannot represent MPI key count."); + } + const int local_int_count = static_cast(local_keys.size() * key_width); + std::vector recv_counts(static_cast(mpi_size), 0); + MPI_Allgather(&local_int_count, 1, MPI_INT, recv_counts.data(), 1, MPI_INT, mpi_comm); + std::vector displacements(static_cast(mpi_size), 0); + int total_int_count = 0; + for (int rank = 0; rank < mpi_size; ++rank) + { + if (recv_counts[static_cast(rank)] < 0 + || recv_counts[static_cast(rank)] > std::numeric_limits::max() - total_int_count) + { + throw std::runtime_error("raw active-ABF overlap writer cannot represent gathered MPI key counts."); + } + displacements[static_cast(rank)] = total_int_count; + total_int_count += recv_counts[static_cast(rank)]; + } + std::vector local_packed; + local_packed.reserve(static_cast(local_int_count)); + for (const auto& key: local_keys) + { + local_packed.insert(local_packed.end(), key.begin(), key.end()); + } + std::vector gathered(static_cast(total_int_count)); + MPI_Allgatherv(local_packed.data(), local_int_count, MPI_INT, gathered.data(), recv_counts.data(), + displacements.data(), MPI_INT, mpi_comm); + std::vector> gathered_keys(static_cast(total_int_count) / key_width); + for (std::size_t index = 0; index < gathered_keys.size(); ++index) + { + std::copy_n(gathered.begin() + index * key_width, key_width, gathered_keys[index].begin()); + } + std::sort(gathered_keys.begin(), gathered_keys.end()); + const auto duplicate = std::adjacent_find(gathered_keys.begin(), gathered_keys.end()); + if (local_duplicate || duplicate != gathered_keys.end()) + { + throw std::runtime_error("raw active-ABF overlap writer found post-communication duplicate contributor for block (I,J,R)=(" + + std::to_string((*duplicate)[0]) + "," + std::to_string((*duplicate)[1]) + "," + + std::to_string((*duplicate)[2]) + "," + std::to_string((*duplicate)[3]) + "," + + std::to_string((*duplicate)[4]) + ")."); + } + + for (int ik = 0; ik < nks_tot; ++ik) + { + const auto q = RI_Util::Vector3_to_array3(p_kv->kvec_c[ik]); + const double q_weight = p_kv->wk[ik] / 2.0 * this->runtime.input.nspin; + if (!std::isfinite(q_weight) + || !std::isfinite(q[0]) || !std::isfinite(q[1]) || !std::isfinite(q[2])) + { + throw std::runtime_error("raw active-ABF overlap writer found non-finite q metadata."); + } + for (const double coordinate: q) + { + double min_coordinate = 0.0; + double max_coordinate = 0.0; + MPI_Allreduce(&coordinate, &min_coordinate, 1, MPI_DOUBLE, MPI_MIN, mpi_comm); + MPI_Allreduce(&coordinate, &max_coordinate, 1, MPI_DOUBLE, MPI_MAX, mpi_comm); + if (max_coordinate - min_coordinate > rank_tol * std::max(1.0, std::abs(coordinate))) + { + throw std::runtime_error("raw active-ABF overlap writer found rank-inconsistent q coordinates."); + } + } + std::vector> overlap( + static_cast(naux) * static_cast(naux), std::complex(0.0, 0.0)); + for (const auto& Ip: overlap_abfs_abfs) + { + const int I = Ip.first; + for (const auto& JPp: Ip.second) + { + const int J = JPp.first.first; + const auto& tensor = JPp.second; + const auto R = JPp.first.second; + const double arg = (p_kv->kvec_c[ik] * (RI_Util::array3_to_Vector3(R) * ucell.latvec)) + * ModuleBase::TWO_PI; + const std::complex phase(std::cos(arg), std::sin(arg)); + for (int ir = 0; ir < atom_naux[static_cast(I)]; ++ir) + { + for (int ic = 0; ic < atom_naux[static_cast(J)]; ++ic) + { + const std::size_t row = static_cast(atom_shift[static_cast(I)] + ir); + const std::size_t col = static_cast(atom_shift[static_cast(J)] + ic); + overlap[row * static_cast(naux) + col] + += static_cast>(tensor(ir, ic)) * phase; + } + } + } + } + for (std::complex& value: overlap) + { + Parallel_Reduce::reduce_all>(value); + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + { + throw std::runtime_error("raw active-ABF overlap writer found a non-finite overlap value."); + } + } + for (int i = 0; i < naux; ++i) + { + for (int j = 0; j < naux; ++j) + { + const std::complex difference + = overlap[static_cast(i) * naux + j] + - std::conj(overlap[static_cast(j) * naux + i]); + const double scale = std::max( + 1.0, + std::max(std::abs(overlap[static_cast(i) * naux + j]), + std::abs(overlap[static_cast(j) * naux + i]))); + if (std::abs(difference) > hermitian_tol * scale) + { + throw std::runtime_error("raw active-ABF overlap writer found a non-Hermitian S(q)."); + } + } + } + + double min_weight = 0.0, max_weight = 0.0; + MPI_Allreduce(&q_weight, &min_weight, 1, MPI_DOUBLE, MPI_MIN, mpi_comm); + MPI_Allreduce(&q_weight, &max_weight, 1, MPI_DOUBLE, MPI_MAX, mpi_comm); + if (max_weight - min_weight > rank_tol * std::max(1.0, std::abs(q_weight))) + { + throw std::runtime_error("raw active-ABF overlap writer found rank-inconsistent q weight."); + } + + if (this->runtime.rank != 0) + { + continue; + } + const std::string out_name = this->runtime.input.rpa_outdir + "v1_abf_overlap_active_iq_" + std::to_string(ik + 1) + ".dat"; + const std::string tmp_name = out_name + ".tmp"; + std::ofstream ofs(tmp_name.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if (!ofs.good()) + { + throw std::runtime_error("Failed to open " + tmp_name); + } + const std::int32_t marker = RpaLriDetail::LIBRPA_ABF_OVERLAP_V1_MARKER; + const std::int32_t version = RpaLriDetail::LIBRPA_ABF_OVERLAP_V1_VERSION; + const std::int32_t iq = ik + 1; + const std::int32_t kind = RpaLriDetail::LIBRPA_ABF_OVERLAP_V1_KIND_ACTIVE; + const std::int32_t naux_i32 = naux; + const std::int32_t natom_i32 = natom; + RpaLriDetail::write_scalar(ofs, marker, tmp_name); + RpaLriDetail::write_scalar(ofs, version, tmp_name); + RpaLriDetail::write_scalar(ofs, iq, tmp_name); + RpaLriDetail::write_scalar(ofs, kind, tmp_name); + RpaLriDetail::write_scalar(ofs, naux_i32, tmp_name); + RpaLriDetail::write_scalar(ofs, natom_i32, tmp_name); + RpaLriDetail::write_scalar(ofs, q_weight, tmp_name); + for (const double coordinate: q) + { + RpaLriDetail::write_scalar(ofs, coordinate, tmp_name); + } + for (const int count: atom_naux) + { + const std::int32_t count_i32 = count; + RpaLriDetail::write_scalar(ofs, count_i32, tmp_name); + } + RpaLriDetail::checked_write(ofs, overlap.data(), overlap.size() * sizeof(std::complex), tmp_name); + ofs.close(); + if (!ofs.good() || std::rename(tmp_name.c_str(), out_name.c_str()) != 0) + { + throw std::runtime_error("Failed to finalize " + out_name); + } + } +} + template void RPA_LRI::out_abfs_overlap( const UnitCell& ucell, std::map>, RI::Tensor>>& overlap_abfs_abfs, @@ -437,3 +756,11 @@ template void RPA_LRI, double>::out_abfs_overlap_v1( std::string filename, const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs_s, const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs); +template void RPA_LRI::out_abfs_overlap_raw_v1( + const UnitCell& ucell, + const std::map>, RI::Tensor>>& overlap_abfs_abfs, + const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs_s); +template void RPA_LRI, double>::out_abfs_overlap_raw_v1( + const UnitCell& ucell, + const std::map>, RI::Tensor>>& overlap_abfs_abfs, + const ModuleBase::Element_Basis_Index::IndexLNM& index_abfs_s); diff --git a/source/source_lcao/module_rpa/unittests/CMakeLists.txt b/source/source_lcao/module_rpa/unittests/CMakeLists.txt index cc94ec19e45..cab84fb27c0 100644 --- a/source/source_lcao/module_rpa/unittests/CMakeLists.txt +++ b/source/source_lcao/module_rpa/unittests/CMakeLists.txt @@ -12,3 +12,11 @@ AddTest( LIBS parameter base device SOURCES test_rpa_lri_band_io.cpp ../rpa_lri_band_io.cpp ) + +AddTest( + TARGET MODULE_RPA_rpa_abfs_preorthogonalization_test + LIBS parameter base ${math_libs} device + SOURCES test_rpa_abfs_preorthogonalization.cpp + ../rpa_abfs_preorthogonalization.cpp + ../../../source_basis/module_ao/orb_atomic_lm.cpp +) diff --git a/source/source_lcao/module_rpa/unittests/test_rpa_abfs_preorthogonalization.cpp b/source/source_lcao/module_rpa/unittests/test_rpa_abfs_preorthogonalization.cpp new file mode 100644 index 00000000000..20edae32d65 --- /dev/null +++ b/source/source_lcao/module_rpa/unittests/test_rpa_abfs_preorthogonalization.cpp @@ -0,0 +1,199 @@ +#include "source_lcao/module_rpa/rpa_abfs_preorthogonalization.h" + +#include "source_io/module_parameter/input_parameter.h" + +#include +#include +#include +#include +#include + +namespace +{ + +constexpr int kNr = 101; +constexpr double kDr = 0.05; + +Numerical_Orbital_Lm make_orbital(const int l, + const int chi, + const std::vector& radial, + const int atom_type = 0) +{ + std::vector r(kNr, 0.0); + std::vector rab(kNr, kDr); + for (int ir = 0; ir != kNr; ++ir) + { + r[ir] = ir * kDr; + } + + Numerical_Orbital_Lm orbital; + orbital.set_orbital_info("N", + atom_type, + l, + chi, + kNr, + rab.data(), + r.data(), + Numerical_Orbital_Lm::Psi_Type::Psi, + radial.data(), + 101, + 0.05, + kDr, + false, + true, + false); + return orbital; +} + +std::vector radial_exp(const double power) +{ + std::vector values(kNr, 0.0); + for (int ir = 0; ir != kNr; ++ir) + { + const double r = ir * kDr; + values[ir] = std::pow(r, power) * std::exp(-r); + } + return values; +} + +ModuleRI::RpaAbfsOrbitalSet make_set(const int l, const std::vector>& radials) +{ + ModuleRI::RpaAbfsOrbitalSet abfs(1); + abfs[0].resize(l + 1); + for (std::size_t i = 0; i != radials.size(); ++i) + { + abfs[0][l].push_back(make_orbital(l, static_cast(i), radials[i])); + } + return abfs; +} + +void expect_same_orbital(const Numerical_Orbital_Lm& lhs, const Numerical_Orbital_Lm& rhs) +{ + EXPECT_EQ(lhs.getLabel(), rhs.getLabel()); + EXPECT_EQ(lhs.getType(), rhs.getType()); + EXPECT_EQ(lhs.getL(), rhs.getL()); + EXPECT_EQ(lhs.getChi(), rhs.getChi()); + EXPECT_EQ(lhs.getNr(), rhs.getNr()); + EXPECT_EQ(lhs.getNk(), rhs.getNk()); + for (int ir = 0; ir != lhs.getNr(); ++ir) + { + EXPECT_DOUBLE_EQ(lhs.getRadial(ir), rhs.getRadial(ir)); + EXPECT_DOUBLE_EQ(lhs.getRab(ir), rhs.getRab(ir)); + EXPECT_DOUBLE_EQ(lhs.getPsi(ir), rhs.getPsi(ir)); + } +} + +} // namespace + +TEST(RpaAbfsPreorthogonalization, NoneLeavesOrbitalsExactlyUnchanged) +{ + auto abfs = make_set(0, {radial_exp(0.0), radial_exp(1.0)}); + const auto original = abfs; + + const auto report = ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "none", 1.0e-2, false); + + ASSERT_EQ(abfs.size(), original.size()); + ASSERT_EQ(abfs[0][0].size(), original[0][0].size()); + for (std::size_t i = 0; i != abfs[0][0].size(); ++i) + { + expect_same_orbital(abfs[0][0][i], original[0][0][i]); + } + EXPECT_EQ(report.mode, "none"); + EXPECT_EQ(report.input_expanded_size, 2); + EXPECT_EQ(report.output_expanded_size, 2); +} + +TEST(RpaAbfsPreorthogonalization, RejectsDuplicateRadial) +{ + const auto radial = radial_exp(0.0); + auto abfs = make_set(0, {radial, radial}); + + const auto report = ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "onsite_coulomb", 1.0e-2, false); + + ASSERT_EQ(abfs[0][0].size(), 1U); + ASSERT_EQ(report.channels.size(), 1U); + EXPECT_EQ(report.channels[0].input_count, 2); + EXPECT_EQ(report.channels[0].output_count, 1); + EXPECT_EQ(report.channels[0].rejected_indices, std::vector({1})); +} + +TEST(RpaAbfsPreorthogonalization, RetainedRadialsAreCoulombOrthonormal) +{ + auto abfs = make_set(0, {radial_exp(0.0), radial_exp(2.0)}); + + const auto report = ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "onsite_coulomb", 1.0e-2, false); + const auto metric = ModuleRI::compute_onsite_coulomb_metric(abfs[0][0]); + + ASSERT_EQ(metric.size(), 2U); + for (std::size_t i = 0; i != metric.size(); ++i) + { + for (std::size_t j = 0; j != metric.size(); ++j) + { + EXPECT_NEAR(metric[i][j], i == j ? 1.0 : 0.0, 1.0e-10); + } + } + ASSERT_EQ(report.channels.size(), 1U); + EXPECT_LE(report.channels[0].maximum_identity_error, 1.0e-10); +} + +TEST(RpaAbfsPreorthogonalization, RejectsWholeAngularMultiplet) +{ + const auto radial = radial_exp(2.0); + auto abfs = make_set(2, {radial, radial}); + + const auto report = ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "onsite_coulomb", 1.0e-2, false); + + EXPECT_EQ(report.input_expanded_size, 10); + EXPECT_EQ(report.output_expanded_size, 5); + ASSERT_EQ(abfs[0][2].size(), 1U); +} + +TEST(RpaAbfsPreorthogonalization, RejectsInvalidModeThresholdAndNorm) +{ + auto abfs = make_set(0, {radial_exp(0.0)}); + EXPECT_THROW(ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "unknown", 1.0e-2, false), std::invalid_argument); + EXPECT_THROW(ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "onsite_coulomb", 0.0, false), + std::invalid_argument); + EXPECT_THROW(ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "onsite_coulomb", -1.0, false), + std::invalid_argument); + EXPECT_THROW(ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, + "onsite_coulomb", + std::numeric_limits::infinity(), + false), + std::invalid_argument); + + auto zero = make_set(0, {std::vector(kNr, 0.0)}); + EXPECT_THROW(ModuleRI::apply_rpa_abfs_preorthogonalization(zero, "onsite_coulomb", 1.0e-2, false), + std::invalid_argument); +} + +TEST(RpaAbfsPreorthogonalization, FormatsCompleteProvenance) +{ + const auto radial = radial_exp(0.0); + auto abfs = make_set(0, {radial, radial}); + const auto report = ModuleRI::apply_rpa_abfs_preorthogonalization(abfs, "onsite_coulomb", 1.0e-2, false); + + const std::string text = ModuleRI::format_rpa_abfs_preorth_report(report); + EXPECT_NE(text.find("mode=onsite_coulomb"), std::string::npos); + EXPECT_NE(text.find("input_expanded_size=2"), std::string::npos); + EXPECT_NE(text.find("output_expanded_size=1"), std::string::npos); + EXPECT_NE(text.find("rejected_indices=1"), std::string::npos); + EXPECT_NE(text.find("minimum_residual_norm="), std::string::npos); + EXPECT_NE(text.find("maximum_identity_error="), std::string::npos); +} + +TEST(RpaAbfsPreorthogonalization, InputAdapterUsesOnlyPublicInputs) +{ + const auto radial = radial_exp(0.0); + Input_para input; + input.rpa_abfs_preorth = "none"; + input.rpa_abfs_preorth_threshold = 1.0e-2; + auto legacy = make_set(0, {radial, radial}); + ModuleRI::finalize_rpa_abfs_from_input(legacy, input, false); + EXPECT_EQ(legacy[0][0].size(), 2U); + + input.rpa_abfs_preorth = "onsite_coulomb"; + auto stable = make_set(0, {radial, radial}); + ModuleRI::finalize_rpa_abfs_from_input(stable, input, false); + EXPECT_EQ(stable[0][0].size(), 1U); +} diff --git a/tests/08_RI/61_librpa_2d_reader_v1/INPUT b/tests/08_RI/61_librpa_2d_reader_v1/INPUT new file mode 100644 index 00000000000..4b2fff47aa7 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/INPUT @@ -0,0 +1,28 @@ +INPUT_PARAMETERS +suffix autotest +pseudo_dir ../../PP_ORB +orbital_dir ../../PP_ORB +calculation scf +ntype 1 +nbands 2 +ecutwfc 20 +smearing_method gaussian +smearing_sigma 1e-4 +basis_type lcao +ks_solver genelpa +dft_functional pbe +rpa 1 +rpa_out_vel 1 +out_mat_xc 1 +out_wfc_lcao 1 +out_librpa_ver 1 +exx_ewald_dimension 2 +symmetry -1 +exx_separate_loop 1 +exx_pca_threshold 1e-3 +shrink_abfs_pca_thr 1e-1 +shrink_lu_inv_thr 1e-3 +exx_singularity_correction massidda +exx_ccp_rmesh_times 1 +rpa_ccp_rmesh_times 1 +exx_real_number 1 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/KPT b/tests/08_RI/61_librpa_2d_reader_v1/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/README b/tests/08_RI/61_librpa_2d_reader_v1/README new file mode 100644 index 00000000000..63fa0ff67c1 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/README @@ -0,0 +1,2 @@ +Check reader-v1 RPA producer output and the 2D GW-preprocessor inputs with the +two-dimensional Ewald Coulomb setting. diff --git a/tests/08_RI/61_librpa_2d_reader_v1/STRU b/tests/08_RI/61_librpa_2d_reader_v1/STRU new file mode 100644 index 00000000000..6c22fe45bf1 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/STRU @@ -0,0 +1,22 @@ +ATOMIC_SPECIES +H 1.008 H_ONCV_PBE-1.0.upf + +NUMERICAL_ORBITAL +H_gga_6au_60Ry_1s.orb + +LATTICE_CONSTANT +1.8897261254578281 + +LATTICE_VECTORS +2.000000 0.000000 0.000000 +0.000000 2.000000 0.000000 +0.000000 0.000000 8.000000 + +ATOMIC_POSITIONS +Direct + +H +0.0 +2 +0.000000 0.000000 0.000000 0 0 0 +0.500000 0.500000 0.500000 0 0 0 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/librpa_producer_manifest.json b/tests/08_RI/61_librpa_2d_reader_v1/librpa_producer_manifest.json new file mode 100644 index 00000000000..eba6a61b7d9 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/librpa_producer_manifest.json @@ -0,0 +1,20 @@ +{ + "output_dir": "OUT.librpa", + "reference_dir": "producer_reference", + "required": [ + {"pattern": "stru_out.txt", "kind": "stru", "reference": true, "abs_tol": 1e-7}, + {"pattern": "band_out.txt", "kind": "band", "reference": true, "abs_tol": 1e-7}, + {"pattern": "librpa_2d_coulomb_head.txt", "kind": "strict_2d_head", "reference": true, "abs_tol": 1e-7}, + {"pattern": "*basis*.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"pattern": "bz_sample.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"pattern": "Cs*_*.dat", "kind": "lri_coeff_v1", "reference": true, "abs_tol": 1e-7}, + {"pattern": "sinvS_*.dat", "kind": "shrink_sinvs_v1", "reference": true, "abs_tol": 1e-7}, + {"pattern": "V_cut_*.dat", "kind": "coulomb_v1", "reference": true, "abs_tol": 1e-7}, + {"pattern": "V_full_*.dat", "kind": "coulomb_v1", "reference": true, "abs_tol": 1e-4}, + {"pattern": "KS_wfc_0.dat", "kind": "ks_wfc_v1", "nao_dir": "OUT.autotest", "abs_tol": 1e-7}, + {"pattern": "velocity_matrix.txt", "kind": "velocity"}, + {"base_dir": ".", "pattern": "OUT.autotest/vxc.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"base_dir": ".", "pattern": "OUT.autotest/KPT.info", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"base_dir": ".", "pattern": "OUT.autotest/wfk*_nao.txt", "kind": "wfc_nao", "expected_count": 1} + ] +} diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/Cs_0.dat b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/Cs_0.dat new file mode 100644 index 00000000000..527a44af0fd Binary files /dev/null and b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/Cs_0.dat differ diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/Cs_shrink_0.dat b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/Cs_shrink_0.dat new file mode 100644 index 00000000000..44214bb34ab Binary files /dev/null and b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/Cs_shrink_0.dat differ diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/OUT.autotest/KPT.info b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/OUT.autotest/KPT.info new file mode 100644 index 00000000000..be7ed93d917 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/OUT.autotest/KPT.info @@ -0,0 +1,5 @@ + nkstot now = 1 + K-POINTS DIRECT COORDINATES + KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT + 1 0.00000000 0.00000000 0.00000000 1.0000 + diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/OUT.autotest/vxc.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/OUT.autotest/vxc.txt new file mode 100644 index 00000000000..2921efdb9f3 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/OUT.autotest/vxc.txt @@ -0,0 +1,5 @@ +1 +1 +2 +-3.3205709310745635e-01 -9.0357370551558649e+00 +-3.3920471090019816e-01 -9.2302337133708079e+00 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/V_cut_1_r0.dat b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/V_cut_1_r0.dat new file mode 100644 index 00000000000..97140c7930d Binary files /dev/null and b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/V_cut_1_r0.dat differ diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/V_full_1_r0.dat b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/V_full_1_r0.dat new file mode 100644 index 00000000000..70900619123 Binary files /dev/null and b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/V_full_1_r0.dat differ diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/aux_basis.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/aux_basis.txt new file mode 100644 index 00000000000..cbfe2adfdcc --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/aux_basis.txt @@ -0,0 +1,4 @@ + 1 2 abacus + 1 1 + 1 1 + 0 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/aux_basis_s.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/aux_basis_s.txt new file mode 100644 index 00000000000..cbfe2adfdcc --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/aux_basis_s.txt @@ -0,0 +1,4 @@ + 1 2 abacus + 1 1 + 1 1 + 0 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/band_out.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/band_out.txt new file mode 100644 index 00000000000..48ec0b3a5a2 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/band_out.txt @@ -0,0 +1,8 @@ +1 +1 +2 +2 +-0.340538689488387 + 1 1 + 1 2.000000000000000 -0.345751556925252 -9.408382533109579 + 2 0.000000000000000 -0.335325822051521 -9.124683732869466 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/basis_map.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/basis_map.txt new file mode 100644 index 00000000000..d144ade7bb6 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/basis_map.txt @@ -0,0 +1,6 @@ + 1 2 2 fallback + 1 1 1 + 1 1 + 0 + 1 1 + 0 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/bz_sample.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/bz_sample.txt new file mode 100644 index 00000000000..94992ee6721 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/bz_sample.txt @@ -0,0 +1,3 @@ +1 1 1 +1 1 + 1 1.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 1 1 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/librpa_2d_coulomb_head.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/librpa_2d_coulomb_head.txt new file mode 100644 index 00000000000..3f81f567680 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/librpa_2d_coulomb_head.txt @@ -0,0 +1,6 @@ +# ABACUS reader-v1 strict 2D Coulomb head normalization +version = 1 +area_parallel_bohr2 = 14.28425931695142 +multipole_norm_squared = 2.9332092125273888 +strict_2d_coulomb_head_coefficient = 16.21343545278323 +strict_2d_sheet_to_raw_scale = 1.6063774141875355 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/sinvS_0.dat b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/sinvS_0.dat new file mode 100644 index 00000000000..d2925df3446 Binary files /dev/null and b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/sinvS_0.dat differ diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/stru_out.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/stru_out.txt new file mode 100644 index 00000000000..7a739ccb7d2 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/stru_out.txt @@ -0,0 +1,9 @@ +3.779452251 0.000000000 0.000000000 +0.000000000 3.779452251 0.000000000 +0.000000000 0.000000000 15.117809004 +1.662459237 -0.000000000 0.000000000 +-0.000000000 1.662459237 -0.000000000 +0.000000000 -0.000000000 0.415614809 +2 + 0.000000000 0.000000000 0.000000000 1 + 1.889726125 1.889726125 7.558904502 1 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/wfc_basis.txt b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/wfc_basis.txt new file mode 100644 index 00000000000..cbfe2adfdcc --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/producer_reference/wfc_basis.txt @@ -0,0 +1,4 @@ + 1 2 abacus + 1 1 + 1 1 + 0 diff --git a/tests/08_RI/61_librpa_2d_reader_v1/result.ref b/tests/08_RI/61_librpa_2d_reader_v1/result.ref new file mode 100644 index 00000000000..4d474fe7055 --- /dev/null +++ b/tests/08_RI/61_librpa_2d_reader_v1/result.ref @@ -0,0 +1,4 @@ +etotref -33.19076629302716 +etotperatomref -16.59538314651358 +Etot_without_rpa -1.241363118026088 +totaltimeref 0.42 diff --git a/tests/08_RI/CASES_LIBRPA_PRODUCER.txt b/tests/08_RI/CASES_LIBRPA_PRODUCER.txt index 62735a2d0de..4faa9a0d650 100644 --- a/tests/08_RI/CASES_LIBRPA_PRODUCER.txt +++ b/tests/08_RI/CASES_LIBRPA_PRODUCER.txt @@ -1,2 +1,3 @@ 60_LIBRPA_RPA_READER_V1 +61_librpa_2d_reader_v1 64_LIBRPA_NSPIN2 diff --git a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_1.txt b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_1.txt index b2e57b8df95..a5c9efad95b 100644 --- a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_1.txt +++ b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_1.txt @@ -1,10 +1,10 @@ 1 11 1 1 1 1 -1 1 - 12.804597578519097 0.000000000000000 +1 1.000000000000000 + 12.804583204574673 0.000000000000000 11 1 1 2 2 1 1.000000000000000 - 9.013593005979113 0.000000000000000 + 9.013576873445704 0.000000000000000 11 2 2 2 2 1 1.000000000000000 - 12.804597578519097 0.000000000000000 + 12.804583204574673 0.000000000000000 diff --git a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_2.txt b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_2.txt index f7f93957870..d00491fd7e5 100644 --- a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_2.txt +++ b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_2.txt @@ -1,23 +1 @@ 1 -11 1 1 3 11 -1 1 - -10.938562529210184 0.000000000000000 - 1.985189593322613 0.000000000000000 - -0.060920254766777 0.000000000000000 - -0.081225860637984 0.000000000000000 - 0.635713414457716 0.000000000000000 - -0.030028606717280 0.000000000000000 - -0.040037987442468 0.000000000000000 - -0.000384724861473 0.000000000000000 - 0.001209798262051 0.000000000000000 -11 2 2 3 11 -1 1.000000000000000 - -11.017679370352148 0.000000000000000 - -1.974811355325449 0.000000000000000 - -0.041573703097393 0.000000000000000 - -0.041573703097393 0.000000000000000 - 0.618655881552475 0.000000000000000 - 0.020135312548884 0.000000000000000 - 0.020135312548884 0.000000000000000 - 0.000000000000000 0.000000000000000 - 0.000417869626873 0.000000000000000 diff --git a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_3.txt b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_3.txt index d00491fd7e5..02000eccb45 100644 --- a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_3.txt +++ b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_3.txt @@ -1 +1,84 @@ 1 +11 3 11 3 11 +1 1 + 12.241290333503347 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 2.678419405227881 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 2.678419405227881 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 2.678419405227881 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 1.163785153425166 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 1.114874984917156 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 1.114874984917156 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 1.163785153425166 0.000000000000000 + 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 1.114874984917156 0.000000000000000 diff --git a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_4.txt b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_4.txt index 02000eccb45..744c4edec0d 100644 --- a/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_4.txt +++ b/tests/08_RI/rpa_scf_gamma/refcoulomb_mat_4.txt @@ -1,84 +1,84 @@ 1 11 3 11 3 11 -1 1 - 12.241290333503347 0.000000000000000 - -0.000000000000000 0.000000000000000 +1 1.000000000000000 + 12.241277777536894 0.000000000000000 + 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 + 2.678419027583669 0.000000000000000 -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 2.678419405227881 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 - 2.678419405227881 0.000000000000000 + 2.678419027583670 0.000000000000000 -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 + 2.678419027583669 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 2.678419405227881 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 - 0.000000000000000 0.000000000000000 - 1.163785153425166 0.000000000000000 + 1.163785314950500 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 - 1.114874984917156 0.000000000000000 + 0.000000000000000 0.000000000000000 + 1.114874953499876 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 1.114874984917156 0.000000000000000 + 1.114874953499876 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 - 0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - 1.163785153425166 0.000000000000000 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 + 1.163785314950500 0.000000000000000 + 0.000000000000000 0.000000000000000 -0.000000000000000 0.000000000000000 - -0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 + -0.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 0.000000000000000 - 1.114874984917156 0.000000000000000 + 1.114874953499876 0.000000000000000 diff --git a/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_1.txt b/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_1.txt index 8f456b59f20..a72c22da9d3 100644 --- a/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_1.txt +++ b/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_1.txt @@ -1,7 +1,7 @@ 2 2 1 1 1 1 -1 0.5 - 18.136220814911479 0.000000000000000 +1 0.500000000000000 + 18.136148320246726 0.000000000000000 2 1 1 1 1 2 0.500000000000000 - 7.134447849351986 -0.000000000000001 + 7.134445976275458 -0.000000000000001 diff --git a/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_2.txt b/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_2.txt index adb8bf8fb33..9d441d9068e 100644 --- a/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_2.txt +++ b/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_2.txt @@ -1,7 +1,7 @@ 2 2 1 1 2 2 -1 0.5 - 15.595012180772425 0.000000000000000 +1 0.500000000000000 + 15.594931291865311 0.000000000000000 2 1 1 2 2 2 0.500000000000000 0.000000000000000 -0.000000000000001 diff --git a/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_4.txt b/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_4.txt index 0524c8eac0e..1abed1c897c 100644 --- a/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_4.txt +++ b/tests/08_RI/rpa_scf_shrink_multik/refcoulomb_mat_4.txt @@ -1,7 +1,7 @@ 2 2 2 2 2 2 -1 0.5 - 18.136220814911479 0.000000000000000 +1 0.500000000000000 + 18.136148320246726 0.000000000000000 2 2 2 2 2 2 0.500000000000000 - 7.134447849351986 -0.000000000000001 + 7.134445976275458 -0.000000000000001 diff --git a/tests/integrate/validation_tools/check_librpa_producer.py b/tests/integrate/validation_tools/check_librpa_producer.py index ee8a47e6e98..31a5a7b1d81 100644 --- a/tests/integrate/validation_tools/check_librpa_producer.py +++ b/tests/integrate/validation_tools/check_librpa_producer.py @@ -219,6 +219,38 @@ def _check_wfc_nao(path): ) +def _check_strict_2d_head(path): + """Validate the reader-v1 strict-2D Coulomb-head metadata sidecar.""" + values = {} + for line_number, line in enumerate(path.read_text().splitlines(), 1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if "=" not in stripped: + raise ProducerContractError("{} line {} is not a key/value record".format(path, line_number)) + key, value = (part.strip() for part in stripped.split("=", 1)) + if not key or key in values: + raise ProducerContractError("{} line {} has an invalid or duplicate key".format(path, line_number)) + values[key] = value + required = { + "version", + "area_parallel_bohr2", + "multipole_norm_squared", + "strict_2d_coulomb_head_coefficient", + "strict_2d_sheet_to_raw_scale", + } + if set(values) != required: + raise ProducerContractError( + "{} has metadata keys {}; expected {}".format(path, sorted(values), sorted(required)) + ) + if values["version"] != "1": + raise ProducerContractError("{} has unsupported metadata version {}".format(path, values["version"])) + for key in sorted(required - {"version"}): + value = _required_number(path, values[key], key) + if value <= 0.0: + raise ProducerContractError("{} has non-positive {}".format(path, key)) + + def _upper_pair(pair_index, natom): current = 0 for iatom in range(natom): @@ -397,6 +429,8 @@ def _check_file(path, entry): return if kind == "stru": _check_stru(path, entry.get("symmetry_rows"), entry.get("spin_symmetry_rows")) + elif kind == "strict_2d_head": + _check_strict_2d_head(path) elif kind == "wfc_nao": _check_wfc_nao(path) elif kind == "ks_wfc_v1": diff --git a/tests/integrate/validation_tools/run_librpa_sym.py b/tests/integrate/validation_tools/run_librpa_sym.py index 61c4606075c..566e2ec1808 100644 --- a/tests/integrate/validation_tools/run_librpa_sym.py +++ b/tests/integrate/validation_tools/run_librpa_sym.py @@ -105,6 +105,18 @@ def check_output(case, mesh, symmetry, nspin, wfc_nspin=None, "nspin": nspin, "nspin_wfc": wfc_nspin, "nk_full": full, "nk_scf": nk, "nq": nq} +def mesh_variants(input_text): + """Mesh variants for a producer case, honoring its dimensionality. + + The strict-2D Coulomb path (exx_ewald_dimension 2) requires a single kz + layer, so its symmetry variants must stay in-plane; the generic 3D + variants would trip the nkz = 1 guard in Ewald_Vq::init. + """ + if re.search(r"(?m)^exx_ewald_dimension\s+2\b", input_text): + return ((1, 3, 1), (2, 2, 1)) + return ((1, 1, 3), (2, 2, 2)) + + def run(args, root): source = args.cases.resolve() work = root / "08_RI" @@ -119,8 +131,12 @@ def run(args, root): ABACUS_MPIEXEC_PREFLAGS=" ".join(args.mpi_preflag), ABACUS_MPIEXEC_POSTFLAGS=" ".join(args.mpi_postflag)) cases = (source / "CASES_LIBRPA_PRODUCER.txt").read_text().split() - for name, nspin in zip(cases[:2], (1, 2)): - for mesh in ((1, 1, 3), (2, 2, 2)): + for name in cases: + input_text = (source / name / "INPUT").read_text() + meshes = mesh_variants(input_text) + nspin_match = re.search(r"(?m)^nspin\s+(\d+)", input_text) + nspin = int(nspin_match.group(1)) if nspin_match else 1 + for mesh in meshes: for symmetry in (-1, 0, 1): case = work / "{}_{}_s{}".format(name, "".join(map(str, mesh)), symmetry) shutil.copytree(source / name, case, ignore=ignore_outputs) @@ -197,7 +213,7 @@ def run(args, root): results.append(magnetic_result) print(json.dumps(magnetic_result), flush=True) (root / "summary.json").write_text(json.dumps(results, indent=2) + "\n") - print("Symmetry producer checks: PASS (12 baseline runs and two SOC runs)") + print("Symmetry producer checks: PASS ({} baseline runs and two SOC runs)".format(len(results) - 2)) def main(): diff --git a/tests/integrate/validation_tools/test_check_librpa_producer.py b/tests/integrate/validation_tools/test_check_librpa_producer.py new file mode 100644 index 00000000000..7346d1387dd --- /dev/null +++ b/tests/integrate/validation_tools/test_check_librpa_producer.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +"""Unit tests for the ABACUS -> LibRPA producer contract checker.""" + +import json +import math +import struct +import tempfile +import unittest +from pathlib import Path + +from check_librpa_producer import ProducerContractError, check_manifest + + +def _write_stru(path: Path, symmetry_rows: int = 0, spin_symmetry_rows: int = 0) -> None: + lines = [ + "1 0 0\n", + "0 1 0\n", + "0 0 1\n", + "6.283185307179586 0 0\n", + "0 6.283185307179586 0\n", + "0 0 6.283185307179586\n", + "1\n", + "0 0 0 1\n", + ] + if symmetry_rows: + lines.append(f"{symmetry_rows} row\n") + for _ in range(symmetry_rows): + lines.append("1 0 0 0 1 0 0 0 1 0 0 0\n") + if spin_symmetry_rows: + lines.append("spin_symmetry 1 1\n") + for _ in range(spin_symmetry_rows): + lines.append("0 1 0 0 0 1 0 0 0\n") + path.write_text("".join(lines)) + + +def _write_coulomb(path: Path) -> None: + # marker, iq, naux, complex flag, natom, nblocks, atom_naux, pair/offset, + # followed by one complex value. + payload = struct.pack( + "<6i i i q 2d", + -20129433, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 40, + 1.25, + -0.5, + ) + path.write_bytes(payload) + + +def _write_chi0(path: Path) -> None: + payload = struct.pack( + "<6i 2d i i i q 2d", + -41073291, + 1, + 1, + 1, + 1, + 1, + 0.25, + 0.5, + 1, + 1, + 0, + 60, + 0.75, + -0.25, + ) + path.write_bytes(payload) + + +def _write_lri_coeff(path: Path, value: float = 0.5) -> None: + # marker, atom/cell dimensions, record table, then one double payload. + path.write_bytes( + struct.pack( + "<3i 2q 5i d q d", + -10267453, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1.25, + 64, + value, + ) + ) + + +def _write_shrink_sinvs(path: Path, value: float = 0.75) -> None: + # marker, one record, then one double payload. + path.write_bytes( + struct.pack( + "<2i 7i d q d", + -30241621, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0.125, + 52, + value, + ) + ) + + +def _write_complex_wfc(path: Path, coefficient_count: int = 4) -> None: + coefficients = " ".join("{:.1f}".format(float(index)) for index in range(coefficient_count)) + path.write_text( + "1 (index of k points)\n" + "0.0 0.0 0.0\n" + "1 (number of bands)\n" + "2 (number of orbitals)\n" + "1 (band)\n" + "-0.5 (Ry)\n" + "1.0 (Occupations)\n" + "{}\n".format(coefficients) + ) + + +def _write_strict_2d_head(path: Path) -> None: + path.write_text( + "# ABACUS reader-v1 strict 2D Coulomb head normalization\n" + "version = 1\n" + "area_parallel_bohr2 = 10.0\n" + "multipole_norm_squared = 2.0\n" + "strict_2d_coulomb_head_coefficient = 3.0\n" + "strict_2d_sheet_to_raw_scale = 4.0\n" + ) + + +class ProducerContractTests(unittest.TestCase): + def test_checks_required_files_and_symmetry_rows(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + out.mkdir() + _write_stru(out / "stru_out.txt", symmetry_rows=2) + (out / "band_out.txt").write_text("1\n1\n2\n2\n0.0\n1 1\n1 1.0 0.0 0.0\n") + manifest = { + "output_dir": "OUT.librpa", + "required": [ + {"pattern": "stru_out.txt", "kind": "stru", "symmetry_rows": 2}, + {"pattern": "band_out.txt", "kind": "band"}, + ], + } + self.assertEqual(check_manifest(root, manifest), []) + + def test_requires_complete_spin_symmetry_block(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + out.mkdir() + _write_stru(out / "stru_out.txt", symmetry_rows=2) + manifest = { + "output_dir": "OUT.librpa", + "required": [ + { + "pattern": "stru_out.txt", + "kind": "stru", + "symmetry_rows": 2, + "spin_symmetry_rows": 2, + } + ], + } + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + _write_stru(out / "stru_out.txt", symmetry_rows=2, spin_symmetry_rows=2) + self.assertEqual(check_manifest(root, manifest), []) + + def test_missing_required_file_is_an_error(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "OUT.librpa").mkdir() + manifest = { + "output_dir": "OUT.librpa", + "required": [{"pattern": "stru_out.txt", "kind": "stru"}], + } + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + def test_checks_reader_v1_binary_headers(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + out.mkdir() + _write_coulomb(out / "v1_coulomb_full_iq_1_rank0.dat") + _write_chi0(out / "v1_sternheimer_chi0_iq_1_ifreq_1_rank0.dat") + manifest = { + "output_dir": "OUT.librpa", + "required": [ + {"pattern": "v1_coulomb_full_iq_*.dat", "kind": "coulomb_v1"}, + {"pattern": "v1_sternheimer_chi0_*.dat", "kind": "chi0_v1"}, + ], + } + self.assertEqual(check_manifest(root, manifest), []) + + def test_checks_strict_2d_head_metadata(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + out.mkdir() + _write_strict_2d_head(out / "librpa_2d_coulomb_head.txt") + manifest = { + "output_dir": "OUT.librpa", + "required": [{"pattern": "librpa_2d_coulomb_head.txt", "kind": "strict_2d_head"}], + } + self.assertEqual(check_manifest(root, manifest), []) + (out / "librpa_2d_coulomb_head.txt").write_text("version = 1\n") + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + def test_compares_lri_and_shrink_v1_payloads(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + ref = root / "reference" + out.mkdir() + ref.mkdir() + _write_lri_coeff(out / "v1_Cs_data_1.txt") + _write_lri_coeff(ref / "v1_Cs_data_1.txt") + _write_shrink_sinvs(out / "v1_shrink_sinvS_1.txt") + _write_shrink_sinvs(ref / "v1_shrink_sinvS_1.txt") + manifest = { + "output_dir": "OUT.librpa", + "reference_dir": "reference", + "required": [ + { + "pattern": "v1_Cs_data_*.txt", + "kind": "lri_coeff_v1", + "reference": True, + }, + { + "pattern": "v1_shrink_sinvS_*.txt", + "kind": "shrink_sinvs_v1", + "reference": True, + }, + ], + } + self.assertEqual(check_manifest(root, manifest), []) + _write_lri_coeff(out / "v1_Cs_data_1.txt", value=0.6) + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + def test_gauge_dependent_files_are_presence_only(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + out.mkdir() + (out / "KS_eigenvector_0.dat").write_bytes(b"not compared\n") + (out / "velocity_matrix").write_bytes(b"not compared\n") + manifest = { + "output_dir": "OUT.librpa", + "required": [ + {"pattern": "KS_eigenvector_*.dat", "kind": "gauge"}, + {"pattern": "velocity_matrix", "kind": "gauge"}, + ], + } + self.assertEqual(check_manifest(root, manifest), []) + + def test_checks_complex_nao_wavefunction_layout(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "OUT.librpa").mkdir() + out = root / "OUT.autotest" + out.mkdir() + wavefunction = out / "wfk1_nao.txt" + _write_complex_wfc(wavefunction) + manifest = { + "output_dir": "OUT.librpa", + "required": [ + { + "base_dir": ".", + "pattern": "OUT.autotest/wfk*_nao.txt", + "kind": "wfc_nao", + } + ], + } + self.assertEqual(check_manifest(root, manifest), []) + _write_complex_wfc(wavefunction, coefficient_count=3) + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + def test_rejects_non_numeric_wavefunction_coefficients(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "OUT.librpa").mkdir() + out = root / "OUT.autotest" + out.mkdir() + wavefunction = out / "wfk1_nao.txt" + _write_complex_wfc(wavefunction) + wavefunction.write_text(wavefunction.read_text().replace("0.0 1.0 2.0 3.0", "0.0 bad 2.0 3.0")) + manifest = { + "output_dir": "OUT.librpa", + "required": [{"base_dir": ".", "pattern": "OUT.autotest/wfk*_nao.txt", "kind": "wfc_nao"}], + } + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + def test_requires_all_declared_wavefunction_files(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "OUT.librpa").mkdir() + out = root / "OUT.autotest" + out.mkdir() + _write_complex_wfc(out / "wfk1_nao.txt") + manifest = { + "output_dir": "OUT.librpa", + "required": [ + {"base_dir": ".", "pattern": "OUT.autotest/wfk*_nao.txt", "kind": "wfc_nao", "expected_count": 2} + ], + } + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + def test_checks_case_relative_vxc_reference(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "OUT.librpa").mkdir() + output = root / "OUT.autotest" + reference = root / "reference" / "OUT.autotest" + output.mkdir() + reference.mkdir(parents=True) + (output / "vxc_out.dat").write_text("1.0000000001 2.0\n") + (reference / "vxc_out.dat").write_text("1.0 2.0\n") + manifest = { + "output_dir": "OUT.librpa", + "reference_dir": "reference", + "required": [ + { + "base_dir": ".", + "pattern": "OUT.autotest/vxc_out.dat", + "kind": "text", + "reference": True, + "abs_tol": 1.0e-8, + } + ], + } + self.assertEqual(check_manifest(root, manifest), []) + + def test_numeric_reference_uses_tolerance(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + ref = root / "reference" + out.mkdir() + ref.mkdir() + (out / "Cs_data_0.txt").write_text("1.0000000001 2.0\n") + (ref / "Cs_data_0.txt").write_text("1.0 2.0\n") + manifest = { + "output_dir": "OUT.librpa", + "reference_dir": "reference", + "required": [ + { + "pattern": "Cs_data_*.txt", + "kind": "numeric", + "reference": True, + "abs_tol": 1.0e-8, + } + ], + } + self.assertEqual(check_manifest(root, manifest), []) + + def test_nonfinite_numeric_value_is_rejected(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + out = root / "OUT.librpa" + out.mkdir() + (out / "Cs_data_0.txt").write_text("nan 1.0\n") + manifest = { + "output_dir": "OUT.librpa", + "required": [{"pattern": "Cs_data_*.txt", "kind": "numeric"}], + } + with self.assertRaises(ProducerContractError): + check_manifest(root, manifest) + + +if __name__ == "__main__": + unittest.main()