diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 8a6922da9a6..c7124a1fa97 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -586,6 +586,14 @@ - [Reduced Density Matrix Functional Theory](#reduced-density-matrix-functional-theory) - [rdmft](#rdmft) - [rdmft\_power\_alpha](#rdmft_power_alpha) + - [Density functional perturbation theory](#density-functional-perturbation-theory) + - [dfpt\_qmesh](#dfpt_qmesh) + - [dfpt\_qfile](#dfpt_qfile) + - [dfpt\_compute\_q0](#dfpt_compute_q0) + - [dfpt\_loto](#dfpt_loto) + - [dfpt\_conv\_thr](#dfpt_conv_thr) + - [dfpt\_max\_iter](#dfpt_max_iter) + - [dfpt\_mix\_beta](#dfpt_mix_beta) ## System variables @@ -634,6 +642,7 @@ - nep: Neuroevolution Potential - ks-lr: Kohn-Sham density functional theory + LR-TDDFT (Under Development Feature) - lr: LR-TDDFT with given KS orbitals (Under Development Feature) + - dfpt: density functional perturbation theory (Under Development Feature) - **Default**: ksdft ### symmetry @@ -5170,3 +5179,49 @@ - **Default**: 0.656 [back to top](#full-list-of-input-keywords) + +## Density functional perturbation theory + +### dfpt_qmesh + +- **Type**: Vector of Int (1 or 3 values) +- **Description**: Set the Monkhorst-Pack q mesh (gamma-centered) for DFPT phonon calculations. The q mesh must be commensurate with the ground-state k mesh: k + q must be a point of the k list (modulo a reciprocal lattice vector). For example, a 4x4x4 KPT mesh is commensurate with dfpt_qmesh values of 1, 2, or 4 along each direction. This parameter is ignored when dfpt_qfile is set. +- **Default**: 1 1 1 + +### dfpt_qfile + +- **Type**: String +- **Description**: Set the file containing the q points for DFPT, in the same format as the KPT file (Q_POINTS card: Gamma/Monkhorst-Pack mesh, or an explicit Direct/Cartesian list; symmetry reduction is not applied to file q lists). When set, it overrides dfpt_qmesh. Each q point must still be commensurate with the ground-state k mesh. +- **Default**: "" + +### dfpt_compute_q0 + +- **Type**: Boolean +- **Description**: Whether to compute the macroscopic dielectric tensor (epsilon_inf) and the Born effective charges at q = 0 within the same DFPT run. Requires a q point at Gamma (the default dfpt_qmesh 1 1 1). +- **Default**: false + +### dfpt_loto + +- **Type**: Boolean +- **Description**: Whether to apply the Lyddane-Sachs-Teller non-analytic correction to the Gamma-point dynamical matrix, which splits the longitudinal and transverse optical modes. Requires dfpt_compute_q0 to be true, since the correction is built from epsilon_inf and the Born effective charges. +- **Default**: false + +### dfpt_conv_thr + +- **Type**: Real +- **Description**: Set the convergence threshold of the self-consistent DFPT cycle: the iteration stops when the relative residual of the first-order density ||drho_out - drho_in|| / ||drho_out|| drops below this value for every displacement. +- **Default**: 1.0e-8 + +### dfpt_max_iter + +- **Type**: Integer +- **Description**: Set the maximum number of self-consistent DFPT iterations for each atomic displacement. +- **Default**: 100 + +### dfpt_mix_beta + +- **Type**: Real +- **Description**: Set the plain-mixing coefficient of the first-order density in the self-consistent DFPT cycle. The response Jacobian has strongly negative eigenvalues on the smallest-G shells (Coulomb stiffness), so beta must stay below 2 / (1 + |lambda_min|); the default 0.4 keeps margin up to |lambda_min| ~ 3. A larger value accelerates convergence for weakly screened systems but may diverge. +- **Default**: 0.4 + +[back to top](#full-list-of-input-keywords) diff --git a/docs/parameters.yaml b/docs/parameters.yaml index f9fccde7883..47eea5b099d 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -53,6 +53,7 @@ parameters: * nep: Neuroevolution Potential * ks-lr: Kohn-Sham density functional theory + LR-TDDFT (Under Development Feature) * lr: LR-TDDFT with given KS orbitals (Under Development Feature) + * dfpt: density functional perturbation theory (Under Development Feature) default_value: ksdft unit: "" availability: "" @@ -3022,6 +3023,62 @@ parameters: default_value: "-1 2 -1 2" unit: primitive cells availability: "lr_solver==plot and exciton_plot_format in [slice, both]" + - name: dfpt_qmesh + category: Density functional perturbation theory + type: Vector of Int (1 or 3 values) + description: | + Set the Monkhorst-Pack q mesh (gamma-centered) for DFPT phonon calculations. The q mesh must be commensurate with the ground-state k mesh: k + q must be a point of the k list (modulo a reciprocal lattice vector). For example, a 4x4x4 KPT mesh is commensurate with dfpt_qmesh values of 1, 2, or 4 along each direction. This parameter is ignored when dfpt_qfile is set. + default_value: 1 1 1 + unit: "" + availability: "" + - name: dfpt_qfile + category: Density functional perturbation theory + type: String + description: | + Set the file containing the q points for DFPT, in the same format as the KPT file (Q_POINTS card: Gamma/Monkhorst-Pack mesh, or an explicit Direct/Cartesian list; symmetry reduction is not applied to file q lists). When set, it overrides dfpt_qmesh. Each q point must still be commensurate with the ground-state k mesh. + default_value: "\"\"" + unit: "" + availability: "" + - name: dfpt_compute_q0 + category: Density functional perturbation theory + type: Boolean + description: | + Whether to compute the macroscopic dielectric tensor (epsilon_inf) and the Born effective charges at q = 0 within the same DFPT run. Requires a q point at Gamma (the default dfpt_qmesh 1 1 1). + default_value: "false" + unit: "" + availability: "" + - name: dfpt_loto + category: Density functional perturbation theory + type: Boolean + description: | + Whether to apply the Lyddane-Sachs-Teller non-analytic correction to the Gamma-point dynamical matrix, which splits the longitudinal and transverse optical modes. Requires dfpt_compute_q0 to be true, since the correction is built from epsilon_inf and the Born effective charges. + default_value: "false" + unit: "" + availability: "" + - name: dfpt_conv_thr + category: Density functional perturbation theory + type: Real + description: | + Set the convergence threshold of the self-consistent DFPT cycle: the iteration stops when the relative residual of the first-order density ||drho_out - drho_in|| / ||drho_out|| drops below this value for every displacement. + default_value: "1.0e-8" + unit: "" + availability: "" + - name: dfpt_max_iter + category: Density functional perturbation theory + type: Integer + description: | + Set the maximum number of self-consistent DFPT iterations for each atomic displacement. + default_value: "100" + unit: "" + availability: "" + - name: dfpt_mix_beta + category: Density functional perturbation theory + type: Real + description: | + Set the plain-mixing coefficient of the first-order density in the self-consistent DFPT cycle. The response Jacobian has strongly negative eigenvalues on the smallest-G shells (Coulomb stiffness), so beta must stay below 2 / (1 + |lambda_min|); the default 0.4 keeps margin up to |lambda_min| ~ 3. A larger value accelerates convergence for weakly screened systems but may diverge. + default_value: "0.4" + unit: "" + availability: "" - name: out_freq_ion category: Output information type: Integer diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 0515b73494b..489d6db1312 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -212,6 +212,7 @@ OBJS_CELL=atom_pseudo.o\ setup_nonlocal.o\ klist.o\ k_vector_utils.o\ + reciprocal_grid.o\ cell_index.o\ cell_tools.o\ check_atomic_stru.o\ @@ -374,6 +375,8 @@ OBJS_HAMILT_OF=kedf_tf.o\ evolve_ofdft.o\ OBJS_DFPT=dfpt_metal.o\ + dfpt_hamilt_shift.o\ + dfpt_kq_basis.o\ dfpt_pert.o\ dfpt_phon.o\ dfpt_pw.o\ @@ -564,6 +567,7 @@ OBJS_SYMMETRY=symm_other.o\ symm_magnetic.o\ symm_pricell.o\ symm_rho.o\ + little_group.o\ symmetry.o\ OBJS_XC=xc_functional.o\ @@ -674,6 +678,7 @@ OBJS_IO=module_parameter/input_conv.o\ module_parameter/read_inp_model.o\ module_parameter/read_inp_postproc.o\ module_parameter/read_inp_exx_dftu.o\ + module_parameter/read_inp_dfpt.o\ module_parameter/read_inp_other.o\ module_parameter/read_inp_out.o\ module_parameter/read_set_globalv.o\ diff --git a/source/source_cell/CMakeLists.txt b/source/source_cell/CMakeLists.txt index 5c687e778f6..9792a2279ed 100644 --- a/source/source_cell/CMakeLists.txt +++ b/source/source_cell/CMakeLists.txt @@ -22,6 +22,7 @@ add_library( read_atoms_helper.cpp read_orb.cpp klist.cpp + reciprocal_grid.cpp parallel_kpoints.cpp cell_index.cpp cell_tools.cpp diff --git a/source/source_cell/k_vector_utils.cpp b/source/source_cell/k_vector_utils.cpp index 88d441a0903..bc1366209f8 100644 --- a/source/source_cell/k_vector_utils.cpp +++ b/source/source_cell/k_vector_utils.cpp @@ -2,6 +2,11 @@ * @file k_vector_utils.cpp * @brief Implementation of k-vector utility functions. * @author rhx (created on 25-6-3) + * + * @note Since 2026-08-14 these free functions are thin wrappers around the + * spin-free members of ModuleCell::ReciprocalGrid / the K_Vectors + * IBZ orchestration, so that existing call sites (esolver_fp.cpp, + * klist.cpp, tests) keep working unchanged. */ #include "k_vector_utils.h" @@ -17,144 +22,26 @@ namespace KVectorUtils { void kvec_d2c(K_Vectors& kv, const ModuleBase::Matrix3& reciprocal_vec) { - // throw std::runtime_error("k_vec_d2c: This function is not implemented in the new codebase. Please use the new - // implementation."); - if (kv.kvec_d.size() != kv.kvec_c.size()) - { - // ModuleBase::WARNING_QUIT("k_vec_d2c", "Size of Cartesian and Direct K vectors mismatch. "); - kv.kvec_c.resize(kv.kvec_d.size()); - } - int nks = kv.kvec_d.size(); // always convert all k vectors - - for (int i = 0; i < nks; i++) - { - // wrong!! kvec_c[i] = G * kvec_d[i]; - // mohan fixed bug 2010-1-10 - if (std::abs(kv.kvec_d[i].x) < 1.0e-10) - { - kv.kvec_d[i].x = 0.0; - } - if (std::abs(kv.kvec_d[i].y) < 1.0e-10) - { - kv.kvec_d[i].y = 0.0; - } - if (std::abs(kv.kvec_d[i].z) < 1.0e-10) - { - kv.kvec_d[i].z = 0.0; - } - - kv.kvec_c[i] = kv.kvec_d[i] * reciprocal_vec; - - // mohan add2012-06-10 - if (std::abs(kv.kvec_c[i].x) < 1.0e-10) - { - kv.kvec_c[i].x = 0.0; - } - if (std::abs(kv.kvec_c[i].y) < 1.0e-10) - { - kv.kvec_c[i].y = 0.0; - } - if (std::abs(kv.kvec_c[i].z) < 1.0e-10) - { - kv.kvec_c[i].z = 0.0; - } - } + kv.kvec_d2c(reciprocal_vec); } void kvec_c2d(K_Vectors& kv, const ModuleBase::Matrix3& latvec) { - if (kv.kvec_d.size() != kv.kvec_c.size()) - { - kv.kvec_d.resize(kv.kvec_c.size()); - } - int nks = kv.kvec_d.size(); // always convert all k vectors - - ModuleBase::Matrix3 RT = latvec.Transpose(); - for (int i = 0; i < nks; i++) - { - // std::cout << " ik=" << i - // << " kvec.x=" << kvec_c[i].x - // << " kvec.y=" << kvec_c[i].y - // << " kvec.z=" << kvec_c[i].z << std::endl; - // wrong! kvec_d[i] = RT * kvec_c[i]; - // mohan fixed bug 2011-03-07 - kv.kvec_d[i] = kv.kvec_c[i] * RT; - } + kv.kvec_c2d(latvec); } void set_both_kvec(K_Vectors& kv, const ModuleBase::Matrix3& G, const ModuleBase::Matrix3& R, std::string& skpt) { - if (true) // Originally GlobalV::FINAL_SCF, but we don't have this variable in the new code. - { - if (kv.get_k_nkstot() == 0) - { - kv.kd_done = true; - kv.kc_done = false; - } - else - { - if (kv.get_k_kword() == "Cartesian" || kv.get_k_kword() == "C") - { - kv.kc_done = true; - kv.kd_done = false; - } - else if (kv.get_k_kword() == "Direct" || kv.get_k_kword() == "D") - { - kv.kd_done = true; - kv.kc_done = false; - } - else - { - GlobalV::ofs_warning << " Error : neither Cartesian nor Direct kpoint." << std::endl; - } - } - } - - // set cartesian k vectors. - if (!kv.kc_done && kv.kd_done) - { - KVectorUtils::kvec_d2c(kv, G); - kv.kc_done = true; - } - - // set direct k vectors - else if (kv.kc_done && !kv.kd_done) - { - KVectorUtils::kvec_c2d(kv, R); - kv.kd_done = true; - } - std::string table; - table += " K-POINTS DIRECT COORDINATES\n"; - table += FmtCore::format("%8s%12s%12s%12s%8s\n", "KPOINTS", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT"); - for (int i = 0; i < kv.get_nkstot(); i++) - { - table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", - i + 1, - kv.kvec_d[i].x, - kv.kvec_d[i].y, - kv.kvec_d[i].z, - kv.wk[i]); - } - GlobalV::ofs_running << table << std::endl; - if (GlobalV::MY_RANK == 0) - { - std::stringstream ss; - ss << " " << std::setw(40) << "nkstot now" - << " = " << kv.get_nkstot() << std::endl; - ss << table << std::endl; - skpt = ss.str(); - } - return; + kv.set_both_kvec(G, R, skpt); } void set_after_vc(K_Vectors& kv, const int& nspin_in, const ModuleBase::Matrix3& reciprocal_vec) { GlobalV::ofs_running << "\n SETUP K-POINTS" << std::endl; - // kv.nspin = nspin_in; kv.set_nspin(nspin_in); ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "nspin", kv.get_nspin()); // set cartesian k vectors. - KVectorUtils::kvec_d2c(kv, reciprocal_vec); + kv.kvec_d2c(reciprocal_vec); std::string table; table += "K-POINTS DIRECT COORDINATES\n"; @@ -178,44 +65,7 @@ void set_after_vc(K_Vectors& kv, const int& nspin_in, const ModuleBase::Matrix3& void print_klists(const K_Vectors& kv, std::ofstream& ofs) { - ModuleBase::TITLE("KVectorUtils", "print_klists"); - int nks = kv.get_nks(); - int nkstot = kv.get_nkstot(); - - if (nkstot < nks) - { - std::cout << "\n nkstot=" << nkstot; - std::cout << "\n nks=" << nks; - ModuleBase::WARNING_QUIT("print_klists", "nkstot < nks"); - } - std::string table; - table += " K-POINTS CARTESIAN COORDINATES\n"; - table += FmtCore::format("%8s%12s%12s%12s%8s\n", "KPOINTS", "CARTESIAN_X", "CARTESIAN_Y", "CARTESIAN_Z", "WEIGHT"); - for (int i = 0; i < nks; i++) - { - table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", - i + 1, - kv.kvec_c[i].x, - kv.kvec_c[i].y, - kv.kvec_c[i].z, - kv.wk[i]); - } - GlobalV::ofs_running << "\n" << table << std::endl; - - table.clear(); - table += " K-POINTS DIRECT COORDINATES\n"; - table += FmtCore::format("%8s%12s%12s%12s%8s\n", "KPOINTS", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT"); - for (int i = 0; i < nks; i++) - { - table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", - i + 1, - kv.kvec_d[i].x, - kv.kvec_d[i].y, - kv.kvec_d[i].z, - kv.wk[i]); - } - GlobalV::ofs_running << "\n" << table << std::endl; - return; + kv.print_klists(ofs); } #ifdef __MPI @@ -321,7 +171,6 @@ void kvec_mpi_k(K_Vectors& kv) { int starsize = kv.kstars[ikibz].size(); Parallel_Common::bcast_int(starsize); - //GlobalV::ofs_running << "starsize: " << starsize << std::endl; auto ks = kv.kstars[ikibz].begin(); for (int ik = 0; ik < starsize; ++ik) { @@ -337,8 +186,6 @@ void kvec_mpi_k(K_Vectors& kv) Parallel_Common::bcast_double(ks_vec.x); Parallel_Common::bcast_double(ks_vec.y); Parallel_Common::bcast_double(ks_vec.z); - //GlobalV::ofs_running << "isym: " << isym << " ks_vec: " << ks_vec.x << " " << ks_vec.y << " " - // << ks_vec.z << std::endl; if (GlobalV::MY_RANK != 0) { kv.kstars[ikibz].insert(std::make_pair(isym, ks_vec)); @@ -350,7 +197,6 @@ void kvec_mpi_k(K_Vectors& kv) } // END SUBROUTINE #endif - void kvec_ibz_kpoint(K_Vectors& kv, const ModuleSymmetry::Symmetry& symm, bool use_symm, @@ -358,474 +204,6 @@ void kvec_ibz_kpoint(K_Vectors& kv, const UnitCell& ucell, bool& match) { - if (GlobalV::MY_RANK != 0) - { - return; - } - ModuleBase::TITLE("K_Vectors", "ibz_kpoint"); - - // k-lattice: "pricell" of reciprocal space - // CAUTION: should fit into all k-input method, not only MP !!! - // the basis vector of reciprocal lattice: recip_vec1, recip_vec2, recip_vec3 - ModuleBase::Vector3 recip_vec1(ucell.G.e11, ucell.G.e12, ucell.G.e13); - ModuleBase::Vector3 recip_vec2(ucell.G.e21, ucell.G.e22, ucell.G.e23); - ModuleBase::Vector3 recip_vec3(ucell.G.e31, ucell.G.e32, ucell.G.e33); - ModuleBase::Vector3 k_vec1, k_vec2, k_vec3; - ModuleBase::Matrix3 k_vec; - if (kv.get_is_mp()) - { - k_vec1 = ModuleBase::Vector3(recip_vec1.x / kv.nmp[0], recip_vec1.y / kv.nmp[0], recip_vec1.z / kv.nmp[0]); - k_vec2 = ModuleBase::Vector3(recip_vec2.x / kv.nmp[1], recip_vec2.y / kv.nmp[1], recip_vec2.z / kv.nmp[1]); - k_vec3 = ModuleBase::Vector3(recip_vec3.x / kv.nmp[2], recip_vec3.y / kv.nmp[2], recip_vec3.z / kv.nmp[2]); - k_vec = ModuleBase::Matrix3(k_vec1.x, - k_vec1.y, - k_vec1.z, - k_vec2.x, - k_vec2.y, - k_vec2.z, - k_vec3.x, - k_vec3.y, - k_vec3.z); - } - - //=============================================== - // search in all space group operations - // if the operations does not already included - // inverse operation, double it. - //=============================================== - bool include_inv = false; - std::vector kgmatrix(48 * 2); - ModuleBase::Matrix3 inv(-1, 0, 0, 0, -1, 0, 0, 0, -1); - ModuleBase::Matrix3 ind(1, 0, 0, 0, 1, 0, 0, 0, 1); - - int nrotkm = 0; - if (use_symm) - { - // bravais type of reciprocal lattice and k-lattice - - double recip_vec_const[6]; - double recip_vec0_const[6]; - double k_vec_const[6]; - double k_vec0_const[6]; - int recip_brav_type = 15; - int k_brav_type = 15; - std::string recip_brav_name; - std::string k_brav_name; - ModuleBase::Vector3 k_vec01 = k_vec1, k_vec02 = k_vec2, k_vec03 = k_vec3; - - // it's not necessary to calculate gb01, gb02, gb03, - // because they are only used as a vector, no need to be assigned values - - // determine the Bravais type and related parameters of the lattice - symm.lattice_type(recip_vec1, - recip_vec2, - recip_vec3, - recip_vec1, - recip_vec2, - recip_vec3, - recip_vec_const, - recip_vec0_const, - recip_brav_type, - recip_brav_name, - ucell.atoms, - false, - nullptr, - 1e-6); - GlobalV::ofs_running << "\n For reciprocal-space lattice" << std::endl; - ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice type", recip_brav_type); - ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice name", recip_brav_name); - - // the map of bravis lattice from real to reciprocal space - // for example, 3(fcc) in real space matches 2(bcc) in reciprocal space - std::vector ibrav_a2b{1, 3, 2, 4, 5, 6, 7, 8, 10, 9, 11, 12, 13, 14}; - // check if the reciprocal lattice is compatible with the real space lattice - auto ibrav_match = [&](int ibrav_b) -> bool { - const int& ibrav_a = symm.real_brav; - if (ibrav_a < 1 || ibrav_a > 14) - { - return false; - } - return (ibrav_b == ibrav_a2b[ibrav_a - 1]); - }; - if (!ibrav_match(recip_brav_type)) // if not match, exit and return - { - GlobalV::ofs_running << "Error: Bravais lattice type of reciprocal lattice is not compatible with that of " - "real space lattice:" - << std::endl; - GlobalV::ofs_running << "ibrav of real space lattice: " << symm.ilattname << std::endl; - GlobalV::ofs_running << "ibrav of reciprocal lattice: " << recip_brav_name << std::endl; - GlobalV::ofs_running << "(which should be " << ibrav_a2b[symm.real_brav - 1] << ")." << std::endl; - match = false; - return; - } - - // if match, continue - if (kv.get_is_mp()) - { - symm.lattice_type(k_vec1, - k_vec2, - k_vec3, - k_vec01, - k_vec02, - k_vec03, - k_vec_const, - k_vec0_const, - k_brav_type, - k_brav_name, - ucell.atoms, - false, - nullptr, - 1e-6); - GlobalV::ofs_running << "\n For k-vectors" << std::endl; - ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice type", k_brav_type); - ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice name", k_brav_name); - } - // point-group analysis of reciprocal lattice - ModuleBase::Matrix3 bsymop[48]; - int bnop = 0; - // search again - symm.lattice_type(recip_vec1, - recip_vec2, - recip_vec3, - recip_vec1, - recip_vec2, - recip_vec3, - recip_vec_const, - recip_vec0_const, - recip_brav_type, - recip_brav_name, - ucell.atoms, - false, - nullptr, - 1e-6); - ModuleBase::Matrix3 b_optlat_new(recip_vec1.x, recip_vec1.y, recip_vec1.z, - recip_vec2.x, recip_vec2.y, recip_vec2.z, - recip_vec3.x, recip_vec3.y, recip_vec3.z); - // set the crystal point-group symmetry operation - const int cal_symm_repr[2] = {0, 6}; - symm.setgroup(bsymop, bnop, recip_brav_type, cal_symm_repr); - // transform the above symmetric operation matrices between different coordinate - symm.gmatrix_convert(bsymop, bsymop, bnop, b_optlat_new, ucell.G); - - // check if all the kgmatrix are in bsymop - auto matequal = [&symm](ModuleBase::Matrix3 a, ModuleBase::Matrix3 b) { - return (symm.equal(a.e11, b.e11) && symm.equal(a.e12, b.e12) && symm.equal(a.e13, b.e13) - && symm.equal(a.e21, b.e21) && symm.equal(a.e22, b.e22) && symm.equal(a.e23, b.e23) - && symm.equal(a.e31, b.e31) && symm.equal(a.e32, b.e32) && symm.equal(a.e33, b.e33)); - }; - for (int i = 0; i < symm.nrotk; ++i) - { - match = false; - for (int j = 0; j < bnop; ++j) - { - if (matequal(symm.kgmatrix[i], bsymop[j])) - { - match = true; - break; - } - } - if (!match) - { - return; - } - } - nrotkm = symm.nrotk; // change if inv not included - for (int i = 0; i < nrotkm; ++i) - { - if (symm.kgmatrix[i] == inv) - { - include_inv = true; - } - kgmatrix[i] = symm.kgmatrix[i]; - } - - if (symm.magnetic_nspin4) - { - // (nspin=4, magnetic) Time reversal Theta reverses the magnetization, so Theta alone is - // NOT a symmetry and the blanket "-k is always equivalent" doubling below is invalid. - // Only the antiunitary elements Theta*g with g in the moment-reversing coset belong to - // the Shubnikov group; append exactly those, keeping the index convention - // j + nrotk <-> Theta * gmatrix_anti[j] (decoded the same way in restore_dm). - // (nspin=2 is unaffected: there the antiunitary operation is plain conjugation K, which - // does not touch the spin, so D_s(-k)=D_s^*(k) holds even for a ferromagnet and the - // generic branch below stays correct.) - for (int j = 0; j < symm.nrotk_anti; ++j) - { - kgmatrix[j + symm.nrotk] = inv * symm.kgmatrix_anti[j]; - } - nrotkm = symm.nrotk + symm.nrotk_anti; - } - else if (!include_inv) - { - for (int i = 0; i < symm.nrotk; ++i) - { - kgmatrix[i + symm.nrotk] = inv * symm.kgmatrix[i]; - } - nrotkm = 2 * symm.nrotk; - } - } - else if (kv.get_is_mp()) // only include for Monkhorst-Pack grid - { - nrotkm = 2; - kgmatrix[0] = ind; - kgmatrix[1] = inv; - } - else - { - return; - } - - // convert kgmatrix to k-lattice - ModuleBase::Matrix3* kkmatrix = new ModuleBase::Matrix3[nrotkm]; - if (kv.get_is_mp()) - { - symm.gmatrix_convert(kgmatrix.data(), kkmatrix, nrotkm, ucell.G, k_vec); - } - // direct coordinates of k-points in k-lattice - std::vector> kvec_d_k(kv.get_nkstot()); - if (kv.get_is_mp()) - { - for (int i = 0; i < kv.get_nkstot(); ++i) - { - kvec_d_k[i] = kv.kvec_d[i] * ucell.G * k_vec.Inverse(); - } - } - - // use operation : kgmatrix to find - // the new set kvec_d : ir_kpt - int nkstot_ibz = 0; - - assert(kv.get_nkstot() > 0); - std::vector> kvec_d_ibz(kv.get_nkstot()); - std::vector wk_ibz(kv.get_nkstot()); // ibz kpoint wk ,weight of k points - std::vector ibz2bz(kv.get_nkstot()); - - // nkstot is the total input k-points number. - double weight = 1.0 / static_cast(kv.get_nkstot()); - - ModuleBase::Vector3 kvec_rot; - ModuleBase::Vector3 kvec_rot_k; - - // for(int i=0; i& kvec) { - // in (-0.5, 0.5] - kvec.x = fmod(kvec.x + 100.5 - 0.5 * symm.epsilon, 1) - 0.5 + 0.5 * symm.epsilon; - kvec.y = fmod(kvec.y + 100.5 - 0.5 * symm.epsilon, 1) - 0.5 + 0.5 * symm.epsilon; - kvec.z = fmod(kvec.z + 100.5 - 0.5 * symm.epsilon, 1) - 0.5 + 0.5 * symm.epsilon; - // in [0, 1) - // kvec.x = fmod(kvec.x + 100 + symm.epsilon, 1) - symm.epsilon; - // kvec.y = fmod(kvec.y + 100 + symm.epsilon, 1) - symm.epsilon; - // kvec.z = fmod(kvec.z + 100 + symm.epsilon, 1) - symm.epsilon; - if (std::abs(kvec.x) < symm.epsilon) - { - kvec.x = 0.0; - } - if (std::abs(kvec.y) < symm.epsilon) - { - kvec.y = 0.0; - } - if (std::abs(kvec.z) < symm.epsilon) - { - kvec.z = 0.0; - } - return; - }; - // update map k -> irreducible k - kv.ibz_index.assign( kv.get_nkstot_full(), -1); // -1 means not in ibz_kpoint list - // search in all k-poins. - for (int i = 0; i < kv.get_nkstot(); ++i) - { - if (!kv.get_is_mp()) { weight = kv.wk[i]; } // use the input weight, instead of 1/nkstot - - // restrict to [0, 1) - restrict_kpt(kv.kvec_d[i]); - - // std::cout << "\n kpoint = " << i << std::endl; - // std::cout << "\n kvec_d = " << kvec_d[i].x << " " << kvec_d[i].y << " " << kvec_d[i].z; - bool already_exist = false; - int exist_number = -1; - // search over all symmetry operations - for (int j = 0; j < nrotkm; ++j) - { - if (!already_exist) - { - // rotate the kvec_d within all operations. - // here use direct coordinates. - // kvec_rot = kgmatrix[j] * kvec_d[i]; - // mohan modify 2010-01-30. - // mohan modify again 2010-01-31 - // fix the bug like kvec_d * G; is wrong - kvec_rot = kv.kvec_d[i] * kgmatrix[j]; // wrong for total energy, but correct for nonlocal force. - // kvec_rot = kgmatrix[j] * kvec_d[i]; //correct for total energy, but wrong for nonlocal force. - restrict_kpt(kvec_rot); - if (kv.get_is_mp()) - { - kvec_rot_k = kvec_d_k[i] * kkmatrix[j]; // k-lattice rotation - kvec_rot_k = kvec_rot_k * k_vec * ucell.G.Inverse(); // convert to recip lattice - restrict_kpt(kvec_rot_k); - - assert(symm.equal(kvec_rot.x, kvec_rot_k.x)); - assert(symm.equal(kvec_rot.y, kvec_rot_k.y)); - assert(symm.equal(kvec_rot.z, kvec_rot_k.z)); - // std::cout << "\n kvec_rot (in recip) = " << kvec_rot.x << " " << kvec_rot.y << " " << kvec_rot.z; - // std::cout << "\n kvec_rot(k to recip)= " << kvec_rot_k.x << " " << kvec_rot_k.y << " " << - // kvec_rot_k.z; - kvec_rot_k = kvec_rot_k * ucell.G * k_vec.Inverse(); // convert back to k-latice - } - for (int k = 0; k < nkstot_ibz; ++k) - { - if (symm.equal(kvec_rot.x, kvec_d_ibz[k].x) && symm.equal(kvec_rot.y, kvec_d_ibz[k].y) - && symm.equal(kvec_rot.z, kvec_d_ibz[k].z)) - { - already_exist = true; - // find another ibz k point, - // but is already in the ibz_kpoint list. - // so the weight need to +1; - wk_ibz[k] += weight; - exist_number = k; - break; - } - } - } // end !already_exist - } - // if really there is no equivalent k point in the list, then add it. - if (!already_exist) - { - // if it's a new ibz kpoint. - // nkstot_ibz indicate the index of ibz kpoint. - kvec_d_ibz[nkstot_ibz] = kv.kvec_d[i]; - // output in kpoints file - kv.ibz_index[i] = nkstot_ibz; - - // the weight should be averged k-point weight. - wk_ibz[nkstot_ibz] = weight; - - // ibz2bz records the index of origin k points. - ibz2bz[nkstot_ibz] = i; - ++nkstot_ibz; - } - else // mohan fix bug 2010-1-30 - { - // std::cout << "\n\n already exist ! "; - - // std::cout << "\n kvec_rot = " << kvec_rot.x << " " << kvec_rot.y << " " << kvec_rot.z; - // std::cout << "\n kvec_d_ibz = " << kvec_d_ibz[exist_number].x - // << " " << kvec_d_ibz[exist_number].y - // << " " << kvec_d_ibz[exist_number].z; - - double kmol_new = kv.kvec_d[i].norm2(); - double kmol_old = kvec_d_ibz[exist_number].norm2(); - - kv.ibz_index[i] = exist_number; - - // std::cout << "\n kmol_new = " << kmol_new; - // std::cout << "\n kmol_old = " << kmol_old; - - // why we need this step? - // because in pw_basis.cpp, while calculate ggwfc2, - // if we want to keep the result of symmetry operation is right. - // we need to fix the number of plane wave. - // and the number of plane wave is depending on the |K+G|, - // so we need to |K|max to be the same as 'no symmetry'. - // mohan 2010-01-30 - if (kmol_new > kmol_old) - { - kvec_d_ibz[exist_number] = kv.kvec_d[i]; - } - } - // BLOCK_HERE("check k point"); - } - - delete[] kkmatrix; - -#ifdef __EXX - // setup kstars according to the final (max-norm) kvec_d_ibz - kv.kstars.resize(nkstot_ibz); - if (ModuleSymmetry::Symmetry::symm_flag == 1) - { - for (int i = 0; i < kv.get_nkstot(); ++i) - { - int exist_number = -1; - int isym = 0; - for (int j = 0; j < nrotkm; ++j) - { - kvec_rot = kv.kvec_d[i] * kgmatrix[j]; - restrict_kpt(kvec_rot); - for (int k = 0; k < nkstot_ibz; ++k) - { - if (symm.equal(kvec_rot.x, kvec_d_ibz[k].x) && symm.equal(kvec_rot.y, kvec_d_ibz[k].y) - && symm.equal(kvec_rot.z, kvec_d_ibz[k].z)) - { - isym = j; - exist_number = k; - break; - } - } - if (exist_number != -1) - { - break; - } - } - kv.kstars[exist_number].insert(std::make_pair(isym, kv.kvec_d[i])); - } - } -#endif - - // output in kpoints file - std::stringstream ss; - ss << " " << std::setw(40) << "nkstot" - << " = " << kv.get_nkstot() << std::setw(66) << "ibzkpt" << std::endl; - std::string table; - table += "K-POINTS REDUCTION ACCORDING TO SYMMETRY\n"; - table += FmtCore::format("%8s%12s%12s%12s%8s%12s%12s%12s\n", - "KPT", - "DIRECT_X", - "DIRECT_Y", - "DIRECT_Z", - "IBZ", - "DIRECT_X", - "DIRECT_Y", - "DIRECT_Z"); - for (int i = 0; i < kv.get_nkstot(); ++i) - { - table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8d%12.8f%12.8f%12.8f\n", - i + 1, - kv.kvec_d[i].x, - kv.kvec_d[i].y, - kv.kvec_d[i].z, - kv.ibz_index[i] + 1, - kvec_d_ibz[kv.ibz_index[i]].x, - kvec_d_ibz[kv.ibz_index[i]].y, - kvec_d_ibz[kv.ibz_index[i]].z); - } - ss << table << std::endl; - skpt = ss.str(); - ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Number of irreducible k-points", nkstot_ibz); - - table.clear(); - table += "\n K-POINTS REDUCTION ACCORDING TO SYMMETRY\n"; - table += FmtCore::format("%8s%12s%12s%12s%8s%8s\n", "IBZ", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT", "ibz2bz"); - for (int ik = 0; ik < nkstot_ibz; ik++) - { - table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f%8d\n", - ik + 1, - kvec_d_ibz[ik].x, - kvec_d_ibz[ik].y, - kvec_d_ibz[ik].z, - wk_ibz[ik], - ibz2bz[ik]); - } - GlobalV::ofs_running << table << std::endl; - - // resize the kpoint container according to nkstot_ibz - if (use_symm || kv.get_is_mp()) - { - kv.update_use_ibz(nkstot_ibz, kvec_d_ibz, wk_ibz); - } - - return; + kv.reduce_by_symmetry(ucell, symm, use_symm, skpt, match); } } // namespace KVectorUtils diff --git a/source/source_cell/klist.cpp b/source/source_cell/klist.cpp index 3db31b68acf..8d8b1447ce6 100644 --- a/source/source_cell/klist.cpp +++ b/source/source_cell/klist.cpp @@ -201,12 +201,8 @@ void K_Vectors::set(const UnitCell& ucell, // 2.reserve space for nspin>2 (symmetry) void K_Vectors::renew(const int& kpoint_number) { - kvec_c.resize(kpoint_number); - kvec_d.resize(kpoint_number); - kvec_c_full.resize(kpoint_number); - wk.resize(kpoint_number); + ReciprocalGrid::renew(kpoint_number); isk.resize(kpoint_number); - ngk.resize(kpoint_number); return; } @@ -503,66 +499,6 @@ void K_Vectors::interpolate_k_between(std::ifstream& ifk, std::vectornkstot = mpnx * mpny * mpnz; - // only can renew after nkstot is estimated. - this->renew(nkstot * nspin); // mohan fix bug 2009-09-01 - - for (int x = 1; x <= mpnx; x++) - { - double v1 = Monkhorst_Pack_formula(k_type, koffset_in[0], x, mpnx); - if (std::abs(v1) < 1.0e-10) { - v1 = 0.0; // mohan update 2012-06-10 - } - for (int y = 1; y <= mpny; y++) - { - double v2 = Monkhorst_Pack_formula(k_type, koffset_in[1], y, mpny); - if (std::abs(v2) < 1.0e-10) { - v2 = 0.0; - } - for (int z = 1; z <= mpnz; z++) - { - double v3 = Monkhorst_Pack_formula(k_type, koffset_in[2], z, mpnz); - if (std::abs(v3) < 1.0e-10) { - v3 = 0.0; - } - // index of nks kpoint - const int i = mpnx * mpny * (z - 1) + mpnx * (y - 1) + (x - 1); - kvec_d[i].set(v1, v2, v3); - } - } - } - - const double weight = 1.0 / static_cast(nkstot); - for (int ik = 0; ik < nkstot; ik++) - { - wk[ik] = weight; - } - this->kd_done = true; - - return; -} - void K_Vectors::update_use_ibz(const int& nkstot_ibz, const std::vector>& kvec_d_ibz, const std::vector& wk_ibz) @@ -593,44 +529,6 @@ void K_Vectors::update_use_ibz(const int& nkstot_ibz, return; } -void K_Vectors::normalize_wk(const int& degspin) -{ - if (GlobalV::MY_RANK != 0) { - return; - } - double sum = 0.0; - - for (int ik = 0; ik < nkstot; ik++) - { - sum += this->wk[ik]; - } - - // If sum of weights is zero or very small, set equal weights - if (sum < 1e-10) - { - ModuleBase::WARNING("K_Vectors::normalize_wk", - "Sum of k-point weights is zero or very small. " - "Setting equal weights for all k-points."); - for (int ik = 0; ik < nkstot; ik++) - { - this->wk[ik] = 1.0 / double(nkstot); - } - sum = 1.0; - } - - for (int ik = 0; ik < nkstot; ik++) - { - this->wk[ik] /= sum; - } - - for (int ik = 0; ik < nkstot; ik++) - { - this->wk[ik] *= degspin; - } - - return; -} - //---------------------------------------------------------- // This routine sets the k vectors for the up and down spin //---------------------------------------------------------- @@ -684,3 +582,196 @@ void K_Vectors::set_kup_and_kdw() return; } // end subroutine set_kup_and_kdw + +void K_Vectors::reduce_by_symmetry(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + bool& match) +{ + if (GlobalV::MY_RANK != 0) + { + return; + } + ModuleBase::TITLE("K_Vectors", "reduce_by_symmetry"); + + //=============================================== + // search in all space group operations + // if the operations does not already included + // inverse operation, double it. + //=============================================== + bool include_inv = false; + std::vector kgmatrix(48 * 2); + ModuleBase::Matrix3 inv(-1, 0, 0, 0, -1, 0, 0, 0, -1); + + ModuleBase::Matrix3 k_vec; + int nrotkm = 0; + if (!this->build_star_ops(ucell, symm, use_symm, k_vec, kgmatrix, nrotkm)) + { + match = false; + return; + } + if (nrotkm == 0) + { + return; + } + + // check whether the inverse operation is already included + for (int i = 0; i < nrotkm; ++i) + { + if (kgmatrix[i] == inv) + { + include_inv = true; + } + } + + if (symm.magnetic_nspin4) + { + // (nspin=4, magnetic) Time reversal Theta reverses the magnetization, so Theta alone is + // NOT a symmetry and the blanket "-k is always equivalent" doubling below is invalid. + // Only the antiunitary elements Theta*g with g in the moment-reversing coset belong to + // the Shubnikov group; append exactly those, keeping the index convention + // j + nrotk <-> Theta * gmatrix_anti[j] (decoded the same way in restore_dm). + // (nspin=2 is unaffected: there the antiunitary operation is plain conjugation K, which + // does not touch the spin, so D_s(-k)=D_s^*(k) holds even for a ferromagnet and the + // generic branch below stays correct.) + for (int j = 0; j < symm.nrotk_anti; ++j) + { + kgmatrix[j + symm.nrotk] = inv * symm.kgmatrix_anti[j]; + } + nrotkm = symm.nrotk + symm.nrotk_anti; + } + else if (!include_inv) + { + for (int i = 0; i < symm.nrotk; ++i) + { + kgmatrix[i + symm.nrotk] = inv * symm.kgmatrix[i]; + } + nrotkm = 2 * symm.nrotk; + } + + // convert kgmatrix to k-lattice + ModuleBase::Matrix3* kkmatrix = new ModuleBase::Matrix3[nrotkm]; + if (this->get_is_mp()) + { + symm.gmatrix_convert(kgmatrix.data(), kkmatrix, nrotkm, ucell.G, k_vec); + } + + // use operation : kgmatrix to find + // the new set kvec_d : ir_kpt + std::vector> kvec_d_ibz; + std::vector wk_ibz; + std::vector ibz2bz; + this->reduce_ibz(kgmatrix.data(), nrotkm, ucell.G, k_vec, kkmatrix, symm.epsilon, kvec_d_ibz, wk_ibz, this->ibz_index, ibz2bz); + const int nkstot_ibz = kvec_d_ibz.size(); + + delete[] kkmatrix; + + auto restrict_kpt = [&symm](ModuleBase::Vector3& kvec) { + // in (-0.5, 0.5] + kvec.x = fmod(kvec.x + 100.5 - 0.5 * symm.epsilon, 1) - 0.5 + 0.5 * symm.epsilon; + kvec.y = fmod(kvec.y + 100.5 - 0.5 * symm.epsilon, 1) - 0.5 + 0.5 * symm.epsilon; + kvec.z = fmod(kvec.z + 100.5 - 0.5 * symm.epsilon, 1) - 0.5 + 0.5 * symm.epsilon; + if (std::abs(kvec.x) < symm.epsilon) + { + kvec.x = 0.0; + } + if (std::abs(kvec.y) < symm.epsilon) + { + kvec.y = 0.0; + } + if (std::abs(kvec.z) < symm.epsilon) + { + kvec.z = 0.0; + } + return; + }; + +#ifdef __EXX + // setup kstars according to the final (max-norm) kvec_d_ibz + this->kstars.resize(nkstot_ibz); + if (ModuleSymmetry::Symmetry::symm_flag == 1) + { + ModuleBase::Vector3 kvec_rot; + for (int i = 0; i < this->nkstot; ++i) + { + int exist_number = -1; + int isym = 0; + for (int j = 0; j < nrotkm; ++j) + { + kvec_rot = this->kvec_d[i] * kgmatrix[j]; + restrict_kpt(kvec_rot); + for (int k = 0; k < nkstot_ibz; ++k) + { + if (symm.equal(kvec_rot.x, kvec_d_ibz[k].x) && symm.equal(kvec_rot.y, kvec_d_ibz[k].y) + && symm.equal(kvec_rot.z, kvec_d_ibz[k].z)) + { + isym = j; + exist_number = k; + break; + } + } + if (exist_number != -1) + { + break; + } + } + this->kstars[exist_number].insert(std::make_pair(isym, this->kvec_d[i])); + } + } +#endif + + // output in kpoints file + std::stringstream ss; + ss << " " << std::setw(40) << "nkstot" + << " = " << this->nkstot << std::setw(66) << "ibzkpt" << std::endl; + std::string table; + table += "K-POINTS REDUCTION ACCORDING TO SYMMETRY\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s%12s%12s%12s\n", + "KPT", + "DIRECT_X", + "DIRECT_Y", + "DIRECT_Z", + "IBZ", + "DIRECT_X", + "DIRECT_Y", + "DIRECT_Z"); + for (int i = 0; i < this->nkstot; ++i) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8d%12.8f%12.8f%12.8f\n", + i + 1, + this->kvec_d[i].x, + this->kvec_d[i].y, + this->kvec_d[i].z, + this->ibz_index[i] + 1, + kvec_d_ibz[this->ibz_index[i]].x, + kvec_d_ibz[this->ibz_index[i]].y, + kvec_d_ibz[this->ibz_index[i]].z); + } + ss << table << std::endl; + skpt = ss.str(); + ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Number of irreducible k-points", nkstot_ibz); + + table.clear(); + table += "\n K-POINTS REDUCTION ACCORDING TO SYMMETRY\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s%8s\n", "IBZ", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT", "ibz2bz"); + for (int ik = 0; ik < nkstot_ibz; ik++) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f%8d\n", + ik + 1, + kvec_d_ibz[ik].x, + kvec_d_ibz[ik].y, + kvec_d_ibz[ik].z, + wk_ibz[ik], + ibz2bz[ik]); + } + GlobalV::ofs_running << table << std::endl; + + // resize the kpoint container according to nkstot_ibz + if (use_symm || this->get_is_mp()) + { + this->update_use_ibz(nkstot_ibz, kvec_d_ibz, wk_ibz); + } + + return; +} diff --git a/source/source_cell/klist.h b/source/source_cell/klist.h index 4b7c906f3fb..706fbb2bf53 100644 --- a/source/source_cell/klist.h +++ b/source/source_cell/klist.h @@ -6,33 +6,26 @@ #include "source_cell/unitcell.h" #include "parallel_kpoints.h" #include "k_vector_utils.h" +#include "reciprocal_grid.h" #include /** * @brief Class for k-points management. + * + * Inherits the spin-free common reciprocal-grid functionality + * (mesh generation, coordinate conversion, weights, printing, star/IBZ + * reduction primitive) from ModuleCell::ReciprocalGrid and adds the + * spin expansion (isk, nspin doubling) and the k-point IBZ logic. */ -class K_Vectors +class K_Vectors : public ModuleCell::ReciprocalGrid { public: - std::vector> kvec_c; ///< Cartesian coordinates of k points - std::vector> kvec_d; ///< Direct coordinates of k points - std::vector> kvec_c_full; ///< Cartesian coordinates of full k mesh match with nkstot_full - - std::vector wk; ///< wk, weight of k points - - std::vector ngk; ///< ngk, number of plane waves for each k point std::vector isk; ///< distinguish spin up and down k points - int nmp[3]={0}; ///< Number of Monhorst-Pack - std::vector kl_segids; ///< index of kline segment - /// @brief equal k points to each ibz-kpont, corresponding to a certain symmetry operations. /// dim: [iks_ibz][(isym, kvec_d)] std::vector>> kstars; - bool kc_done = false; ///< flag indicating if Cartesian coordinates are calculated - bool kd_done = false; ///< flag indicating if direct coordinates are calculated - K_Vectors(){}; ~K_Vectors(){}; K_Vectors& operator=(const K_Vectors&) = default; @@ -160,31 +153,47 @@ class K_Vectors const std::vector& wk_ibz); private: - int nks = 0; ///< number of symmetry-reduced k points in this pool(processor, up+dw) - int nkstot = 0; ///< number of symmetry-reduced k points in full k mesh - int nkstot_full = 0; ///< number of k points before symmetry reduction in full k mesh - - int nspin = 0; ///< number of spin states - double koffset[3] = {0.0}; ///< used only in automatic k-points - std::string k_kword; ///< LiuXh add 20180619 - int k_nkstot = 0; ///< LiuXh add 20180619 - bool is_mp = false; ///< Monkhorst-Pack + int nspin = 0; ///< number of spin states + double koffset[3] = {0.0}; ///< used only in automatic k-points /** * @brief Resize the k-point related vectors according to the new k-point number. * - * This function resizes the vectors that store the k-point information, - * including the Cartesian and Direct coordinates of k-points, - * the weights of k-points, the index of k-points, and the number of plane waves for each k-point. + * Extends the base-class implementation so that the spin index (isk) is + * resized along with the coordinate/weight containers. * * @param kpoint_number The new number of k-points. * * @return void - * - * @note The memory recording lines are commented out. If you want to track the memory usage, - * you can uncomment these lines. */ - void renew(const int& kpoint_number); + void renew(const int& kpoint_number) override; + + /// @brief Spin multiplicity used when generating the mesh (1/2 for nspin 1/2). + int spin_factor() const override + { + return this->nspin; + } + + /** + * @brief Reduce the k-points to the irreducible Brillouin zone (IBZ). + * + * Orchestrates the K-specific parts of the IBZ reduction (Bravais-lattice + * compatibility checks, point-group construction, time-reversal / magnetic + * operation doubling, k-star bookkeeping and the printed reduction table) + * and delegates the generic folding loop to ReciprocalGrid::reduce_ibz. + * + * @param ucell unit cell + * @param symm symmetry of the system + * @param use_symm whether symmetry reduction is enabled + * @param skpt output string holding the reduction table + * @param match set to false if the reciprocal lattice is not compatible + * with the real-space lattice + */ + void reduce_by_symmetry(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + bool& match) override; /// @brief step 1 : generate kpoints @@ -237,65 +246,6 @@ class K_Vectors */ void interpolate_k_between(std::ifstream& ifk, std::vector>& kvec); - /** - * @brief Generates k-points using the Monkhorst-Pack scheme. - * - * This function generates k-points in the reciprocal space using the Monkhorst-Pack scheme. - * - * @param nmp_in the number of k-points in each dimension. - * @param koffset_in the offset for the k-points in each dimension. - * @param k_type The type of k-point. 1 means without Gamma point, 0 means with Gamma. - * - * @return void - * - * @note The function assumes that the k-points are evenly distributed in the reciprocal space. - * @note The function sets the weight of each k-point to be equal, so that the total weight of all k-points is 1. - * @note The function sets the flag kd_done to true to indicate that the k-points have been generated. - */ - void Monkhorst_Pack(const int* nmp_in, const double* koffset_in, const int tipo); - - /** - * @brief Calculates the coordinate of a k-point using the Monkhorst-Pack scheme. - * - * This function calculates the coordinate of a k-point in the reciprocal space using the Monkhorst-Pack scheme. - * The Monkhorst-Pack scheme is a method for generating k-points in the Brillouin zone. - * - * @param k_type The type of k-point. 1 means without Gamma point, 0 means with Gamma. - * @param offset The offset for the k-point. - * @param n The index of the k-point in the current dimension. - * @param dim The total number of k-points in the current dimension. - * - * @return double Returns the coordinate of the k-point. - * - * @note The function assumes that the k-points are evenly distributed in the reciprocal space. - */ - double Monkhorst_Pack_formula(const int& k_type, const double& offset, const int& n, const int& dim); - - /// @brief step 2 : set both kvec and kved; normalize weight - - // void set_both_kvec(const ModuleBase::Matrix3& G, const ModuleBase::Matrix3& R, std::string& skpt); - - /** - * @brief Normalizes the weights of the k-points. - * - * This function normalizes the weights of the k-points so that their sum is equal to the degeneracy of spin - * (degspin). - * - * @param degspin The degeneracy of spin. This is 1 for non-spin-polarized calculations and 2 for spin-polarized - * calculations. - * - * @return void - * - * @note This function should only be called by the master process (MY_RANK == 0). - * @note If the sum of the weights is zero or very small (< 1e-10), the function will set equal weights for all - * k-points and issue a warning. This allows calculations like get_wf to proceed with zero-weight k-points. - * @note The function first normalizes the weights so that their sum is 1, and then scales them by the degeneracy of - * spin. - */ - void normalize_wk(const int& degspin); - - - /// @brief step 4 : *2 kpoints /** @@ -324,6 +274,12 @@ class K_Vectors * @return this->ik2iktot[ik] */ void cal_ik_global(); + friend void KVectorUtils::kvec_ibz_kpoint(K_Vectors& kv, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + const UnitCell& ucell, + bool& match); #ifdef __MPI friend void KVectorUtils::kvec_mpi_k(K_Vectors& kvec); #endif diff --git a/source/source_cell/module_symmetry/CMakeLists.txt b/source/source_cell/module_symmetry/CMakeLists.txt index 5032268e4cd..7cdd95d129b 100644 --- a/source/source_cell/module_symmetry/CMakeLists.txt +++ b/source/source_cell/module_symmetry/CMakeLists.txt @@ -16,6 +16,7 @@ add_library( symm_rho.cpp symmetry.cpp symm_rot_spin.cpp + little_group.cpp ) if(ENABLE_COVERAGE) diff --git a/source/source_cell/module_symmetry/little_group.cpp b/source/source_cell/module_symmetry/little_group.cpp new file mode 100644 index 00000000000..dc0168315c8 --- /dev/null +++ b/source/source_cell/module_symmetry/little_group.cpp @@ -0,0 +1,53 @@ +/** + * @file little_group.cpp + * @brief Implementation of ModuleSymmetry::LittleGroup. + */ +#include "little_group.h" + +#include "symmetry.h" + +#include + +namespace ModuleSymmetry +{ + +void LittleGroup::set_q(const ModuleBase::Vector3& q, const Symmetry& symm) +{ + this->q_ = q; + this->little_group_ops_.clear(); + + // A rotation R belongs to the little group of q when R q - q is a + // reciprocal-lattice vector. In direct coordinates of the reciprocal + // lattice such a vector has integer components, so we test each component + // of (R q - q) for integrality within the symmetry tolerance. The + // row-vector convention (q * R) matches the one used by reduce_ibz. + const double eps = symm.epsilon; + for (int i = 0; i < symm.nrotk; ++i) + { + const ModuleBase::Vector3 rq = q * symm.kgmatrix[i]; + const double dx = rq.x - q.x; + const double dy = rq.y - q.y; + const double dz = rq.z - q.z; + const double fx = dx - std::floor(dx + 0.5); // signed distance to nearest integer + const double fy = dy - std::floor(dy + 0.5); + const double fz = dz - std::floor(dz + 0.5); + if (std::abs(fx) < eps && std::abs(fy) < eps && std::abs(fz) < eps) + { + this->little_group_ops_.push_back(i); + } + } + + // Placeholder: one fully-symmetric irrep (A1) per q-point. The real + // little-group representation analysis (kgmatrix + gtrans phase factors) + // is implemented in a later iteration. + this->nirr_ = 1; +} + +std::vector LittleGroup::get_mode_basis(int irrep) const +{ + (void)irrep; + // Placeholder: the projection-operator basis is not implemented yet. + return std::vector(); +} + +} // namespace ModuleSymmetry diff --git a/source/source_cell/module_symmetry/little_group.h b/source/source_cell/module_symmetry/little_group.h new file mode 100644 index 00000000000..7615e6bd084 --- /dev/null +++ b/source/source_cell/module_symmetry/little_group.h @@ -0,0 +1,77 @@ +/** + * @file little_group.h + * @brief Little group (group of the wavevector) of a q-point. + * @note Added 2026-08-14 for the DFPT q-point irreducible-representation + * analysis. The little group of a q-point is the subgroup of the + * crystal space group whose rotations R satisfy R q ≡ q modulo a + * reciprocal-lattice vector; its irreducible representations classify + * the atomic displacement patterns at that q-point (phonon irreps). + * This version exposes the interface and the little-group operation + * list; the full irrep table / projection-operator decomposition is + * added in a later iteration. + */ +#ifndef LITTLE_GROUP_H +#define LITTLE_GROUP_H + +#include "source_base/vector3.h" +#include + +namespace ModuleSymmetry +{ + +class Symmetry; + +/** + * @brief Little group (group of the wavevector) of a q-point. + * + * QList aggregates one LittleGroup per q-point. The operation list is built + * from the reciprocal-space rotation matrices (Symmetry::kgmatrix) of the + * crystal space group. + */ +class LittleGroup +{ + public: + LittleGroup() = default; + ~LittleGroup() = default; + + /** + * @brief Set the q-point and determine its little group. + * + * The little group consists of the operations R with R q - q a vector of + * integers (a reciprocal-lattice vector in direct coordinates), within the + * symmetry tolerance. + * + * @param q q-point in direct (fractional) coordinates + * @param symm symmetry of the system (kgmatrix / nrotk / epsilon) + */ + void set_q(const ModuleBase::Vector3& q, const Symmetry& symm); + + /// @brief Number of irreducible representations of the little group. + /// Placeholder: returns 1 (the fully-symmetric A1) until the + /// full irrep table is implemented. + int get_nirr() const { return nirr_; } + + /** + * @brief Representative basis modes of an irrep. + * Placeholder: empty until the projection-operator decomposition + * is implemented. + * @param irrep irrep index + */ + std::vector get_mode_basis(int irrep) const; + + /// @brief Indices (into Symmetry::kgmatrix / Symmetry::gtrans) of the + /// little-group operations. + const std::vector& get_little_group_ops() const { return little_group_ops_; } + + /// @brief The current q-point (direct coordinates). + ModuleBase::Vector3 get_q() const { return q_; } + + private: + ModuleBase::Vector3 q_; + std::vector little_group_ops_; + int nirr_ = 1; ///< placeholder: fully-symmetric A1 +}; + +} // namespace ModuleSymmetry + +#endif // LITTLE_GROUP_H diff --git a/source/source_cell/qlist.cpp b/source/source_cell/qlist.cpp index 762f52bdadf..cd13bf1c6ef 100644 --- a/source/source_cell/qlist.cpp +++ b/source/source_cell/qlist.cpp @@ -1,13 +1,17 @@ // ============================================================ -// This code is added by Mohan Chen on 2026-05-18. -// This code is currently in the design phase and has not been -// put into production yet. It may change in the future. -// Please use this code with caution. Only developers who know -// what they are doing should use this code. +// QList: q-point mesh generation and star reduction. // ============================================================ #include "qlist.h" +#include "source_base/global_function.h" +#include "source_base/global_variable.h" +#include "source_base/formatter.h" +#include "source_base/tool_quit.h" +#include +#include +#include + namespace ModuleCell { QList::QList() {} @@ -16,41 +20,390 @@ QList::~QList() {} void QList::generate_mesh(UnitCell& ucell, ModuleSymmetry::Symmetry& symm, const std::vector& mp_grid, bool use_irreps) { - (void)ucell; - (void)symm; - (void)mp_grid; - (void)use_irreps; - - nq_ = 1; - qvec_.resize(nq_); - qvec_[0] = ModuleBase::Vector3(0.0, 0.0, 0.0); - - nirr_.resize(nq_); - nirr_[0] = 1; - - irrep_modes_.resize(nq_); - irrep_modes_[0].resize(1); + if (mp_grid.size() != 3) + { + ModuleBase::WARNING_QUIT("QList::generate_mesh", "mp_grid must have three components."); + } + + this->is_mp = true; + this->nmp[0] = mp_grid[0]; + this->nmp[1] = mp_grid[1]; + this->nmp[2] = mp_grid[2]; + + // Gamma-centered Monkhorst-Pack q mesh (k_type = 0), zero offset. + const double offset[3] = {0.0, 0.0, 0.0}; + this->Monkhorst_Pack(this->nmp, offset, 0); + + this->nkstot_full = this->nkstot; + this->nks = this->nkstot; + + // Star reduction: always use symmetry, always include the -q partner. + bool match = true; + std::string skpt; + this->reduce_by_symmetry(ucell, symm, true, skpt, match); + if (!match) + { + ModuleBase::WARNING("QList::generate_mesh", + "Reciprocal lattice is incompatible with the real-space lattice. " + "Falling back to the unreduced q-point mesh."); + this->nkstot = this->nks = this->nkstot_full; + } + + // weights sum to 1 (average over the full Brillouin zone) + this->normalize_wk(1); + + // Cartesian coordinates of the reduced q-point list (from the direct ones). + // The reciprocal lattice is stored in ucell.G (columns are the reciprocal + // primitive vectors), so q_cart = q_direct * G. + this->kvec_d2c(ucell.G); + this->kc_done = true; + + // little-group irreducible-representation data (opt-in) + if (use_irreps) + { + this->get_irreps(ucell, symm); + } + else + { + this->nirr_.clear(); + this->irrep_modes_.clear(); + } } void QList::read_from_file(const std::string& filename, UnitCell& ucell) { - (void)filename; - (void)ucell; + std::ifstream ifq(filename.c_str()); + if (!ifq) + { + ModuleBase::WARNING("QList::read_from_file", "Can not find the q-points file."); + this->nkstot = this->nks = 0; + return; + } + + ifq >> std::setiosflags(std::ios::uppercase); + ifq.clear(); + ifq.seekg(0); + + // find the "Q_POINTS" (or "QPOINTS" / "Q") header, mirroring read_kpoints + std::string word; + std::string qword; + int ierr = 0; + while (ifq.good()) + { + ifq >> word; + ifq.ignore(150, '\n'); + if (word == "Q_POINTS" || word == "QPOINTS" || word == "Q") + { + ierr = 1; + break; + } + ifq.rdstate(); + } + if (ierr == 0) + { + ModuleBase::WARNING("QList::read_from_file", "symbol Q_POINTS not found."); + this->nkstot = this->nks = 0; + return; + } + + ModuleBase::GlobalFunc::READ_VALUE(ifq, this->nkstot); + this->k_nkstot = this->nkstot; + ModuleBase::GlobalFunc::READ_VALUE(ifq, qword); + this->k_kword = qword; + + const int max_qpoints = 100000; + if (this->nkstot > max_qpoints) + { + ModuleBase::WARNING("QList::read_from_file", "nkstot > MAX_QPOINTS"); + this->nkstot = this->nks = 0; + return; + } + + int q_type = 0; + if (this->nkstot == 0) // Monkhorst-Pack mesh + { + this->is_mp = true; + if (qword == "Gamma") + { + q_type = 0; + } + else if (qword == "Monkhorst-Pack" || qword == "MP" || qword == "mp") + { + q_type = 1; + } + else + { + ModuleBase::WARNING("QList::read_from_file", "neither Gamma nor Monkhorst-Pack."); + this->nkstot = this->nks = 0; + return; + } + + ifq >> this->nmp[0] >> this->nmp[1] >> this->nmp[2]; + double koffset[3] = {0.0, 0.0, 0.0}; + if (!(ifq >> koffset[0] >> koffset[1] >> koffset[2])) + { + ModuleBase::WARNING("QList::read_from_file", "Missing q-point offsets in the q-points file."); + } + this->Monkhorst_Pack(this->nmp, koffset, q_type); + } + else // explicit list or line path + { + if (qword == "Cartesian" || qword == "C") + { + this->renew(this->nkstot); + for (int i = 0; i < this->nkstot; ++i) + { + ifq >> kvec_c[i].x >> kvec_c[i].y >> kvec_c[i].z; + ModuleBase::GlobalFunc::READ_VALUE(ifq, wk[i]); + } + this->kc_done = true; + } + else if (qword == "Direct" || qword == "D") + { + this->renew(this->nkstot); + for (int i = 0; i < this->nkstot; ++i) + { + ifq >> kvec_d[i].x >> kvec_d[i].y >> kvec_d[i].z; + ModuleBase::GlobalFunc::READ_VALUE(ifq, wk[i]); + } + this->kd_done = true; + } + else if (qword == "Line_Direct" || qword == "L" || qword == "Line") + { + interpolate_q_between(ifq, kvec_d); + std::for_each(wk.begin(), wk.end(), [](double& d) { d = 1.0; }); + this->kd_done = true; + } + else if (qword == "Line_Cartesian") + { + interpolate_q_between(ifq, kvec_c); + std::for_each(wk.begin(), wk.end(), [](double& d) { d = 1.0; }); + this->kc_done = true; + } + else + { + ModuleBase::WARNING("QList::read_from_file", "neither Cartesian nor Direct qpoint."); + this->nkstot = this->nks = 0; + return; + } + } + + this->nkstot_full = this->nks = this->nkstot; + + // complement the coordinates: fill the missing representation + if (!this->kc_done && this->kd_done) + { + this->kvec_d2c(ucell.G); + this->kc_done = true; + } + else if (this->kc_done && !this->kd_done) + { + this->kvec_c2d(ucell.latvec); + this->kd_done = true; + } + + // weights: a mesh or explicit list is normalized to sum 1; a line path + // keeps its unnormalized weights (each point weight 1) + if (this->k_kword != "Line_Direct" && this->k_kword != "L" && this->k_kword != "Line" + && this->k_kword != "Line_Cartesian") + { + this->normalize_wk(1); + } + + // no symmetry reduction (no symmetry object in this interface), so no + // little-group irrep decomposition either; the DFPT driver requires at + // least the fallback fully-symmetric placeholder (nirr = 1, empty mode + // basis -> solve the full 3N displacement basis) + this->nirr_.assign(this->nkstot, 1); + this->irrep_modes_.assign(this->nkstot, std::vector>(1)); +} + +void QList::interpolate_q_between(std::ifstream& ifq, std::vector>& qvec) { + const int nqs_special = this->nkstot; + std::vector nql(nqs_special, 0); + std::vector> qs(nqs_special); + + // recalculate nkstot + this->nkstot = 0; + this->kl_segids.clear(); + this->kl_segids.shrink_to_fit(); + int qpt_segid = 0; + for (int iqs = 0; iqs < nqs_special; ++iqs) + { + ifq >> qs[iqs].x; + ifq >> qs[iqs].y; + ifq >> qs[iqs].z; + ModuleBase::GlobalFunc::READ_VALUE(ifq, nql[iqs]); + assert(nql[iqs] >= 0); + this->nkstot += nql[iqs]; + if ((nql[iqs] == 1) && (iqs != (nqs_special - 1))) + { + ++qpt_segid; + } + this->kl_segids.push_back(qpt_segid); + } + assert(nql[nqs_special - 1] == 1); + + this->renew(this->nkstot); + + int count = 0; + for (int iqs = 1; iqs < nqs_special; ++iqs) + { + double dxs = (qs[iqs].x - qs[iqs - 1].x) / nql[iqs - 1]; + double dys = (qs[iqs].y - qs[iqs - 1].y) / nql[iqs - 1]; + double dzs = (qs[iqs].z - qs[iqs - 1].z) / nql[iqs - 1]; + for (int is = 0; is < nql[iqs - 1]; ++is) + { + qvec[count].x = qs[iqs - 1].x + is * dxs; + qvec[count].y = qs[iqs - 1].y + is * dys; + qvec[count].z = qs[iqs - 1].z + is * dzs; + ++count; + } + } + qvec[count].x = qs[nqs_special - 1].x; + qvec[count].y = qs[nqs_special - 1].y; + qvec[count].z = qs[nqs_special - 1].z; + ++count; + assert(count == this->nkstot); +} + +void QList::print_qlists(std::ofstream& ofs) const { + ModuleBase::TITLE("QList", "print_qlists"); + const int nq = this->nks; + if (this->nkstot < nq) + { + ModuleBase::WARNING_QUIT("QList::print_qlists", "nkstot < nks"); + } + std::string table; + table += " Q-POINTS CARTESIAN COORDINATES\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s\n", "QPOINTS", "CARTESIAN_X", "CARTESIAN_Y", "CARTESIAN_Z", "WEIGHT"); + for (int i = 0; i < nq; i++) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", + i + 1, + this->kvec_c[i].x, + this->kvec_c[i].y, + this->kvec_c[i].z, + this->wk[i]); + } + ofs << "\n" << table << std::endl; + + table.clear(); + table += " Q-POINTS DIRECT COORDINATES\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s\n", "QPOINTS", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT"); + for (int i = 0; i < nq; i++) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", + i + 1, + this->kvec_d[i].x, + this->kvec_d[i].y, + this->kvec_d[i].z, + this->wk[i]); + } + ofs << "\n" << table << std::endl; + return; } std::vector QList::get_irrep_modes(int q_idx, int irrep_idx) const { - (void)q_idx; - (void)irrep_idx; - return std::vector(); + if (q_idx < 0 || q_idx >= static_cast(this->nirr_.size())) + { + return std::vector(); + } + if (irrep_idx < 0 || irrep_idx >= this->nirr_[q_idx]) + { + return std::vector(); + } + return this->irrep_modes_[q_idx][irrep_idx]; } -void QList::reduce(UnitCell& ucell, ModuleSymmetry::Symmetry& symm) { - (void)ucell; - (void)symm; +void QList::reduce_by_symmetry(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + bool& match) { + (void)skpt; + // q-points are spin-free: build the point-group operations and always + // double them by the time-reversal operation -q (no magnetic group). + std::vector kgmatrix(48 * 2); + ModuleBase::Matrix3 inv(-1, 0, 0, 0, -1, 0, 0, 0, -1); + + ModuleBase::Matrix3 q_vec; // k-lattice basis of the q mesh + int nrotkm = 0; + if (!this->build_star_ops(ucell, symm, use_symm, q_vec, kgmatrix, nrotkm)) + { + match = false; + return; + } + if (nrotkm == 0) + { + // no operations to apply: the mesh stays unreduced + match = true; + return; + } + + bool include_inv = false; + for (int i = 0; i < nrotkm; ++i) + { + if (kgmatrix[i] == inv) + { + include_inv = true; + break; + } + } + if (!include_inv) + { + for (int i = 0; i < nrotkm; ++i) + { + kgmatrix[i + nrotkm] = inv * kgmatrix[i]; + } + nrotkm *= 2; + } + + ModuleBase::Matrix3* kkmatrix = new ModuleBase::Matrix3[nrotkm]; + symm.gmatrix_convert(kgmatrix.data(), kkmatrix, nrotkm, ucell.G, q_vec); + + std::vector> qvec_ibz; + std::vector wk_ibz; + std::vector ibz_index; + std::vector ibz2bz; + this->reduce_ibz(kgmatrix.data(), nrotkm, ucell.G, q_vec, kkmatrix, symm.epsilon, qvec_ibz, wk_ibz, ibz_index, ibz2bz); + + delete[] kkmatrix; + + // update the reduced q-point list (no spin expansion) + const int nq_ibz = qvec_ibz.size(); + this->nkstot = this->nks = nq_ibz; + this->kvec_d.resize(this->nkstot); + this->wk.resize(this->nkstot); + for (int i = 0; i < this->nkstot; ++i) + { + this->kvec_d[i] = qvec_ibz[i]; + this->wk[i] = wk_ibz[i]; + } + this->kd_done = true; + this->kc_done = false; + + match = true; + return; } -void QList::get_irreps(UnitCell& ucell, ModuleSymmetry::Symmetry& symm) { +void QList::get_irreps(const UnitCell& ucell, const ModuleSymmetry::Symmetry& symm) { (void)ucell; - (void)symm; + + // Decompose each q-point via its little group (placeholder: one + // fully-symmetric A1 irrep per q-point; the LittleGroup projection-operator + // basis is filled in a later iteration). + this->nirr_.assign(this->nkstot, 0); + this->irrep_modes_.assign(this->nkstot, std::vector>()); + for (int iq = 0; iq < this->nkstot; ++iq) + { + this->little_group_.set_q(this->kvec_d[iq], symm); + this->nirr_[iq] = this->little_group_.get_nirr(); + this->irrep_modes_[iq].resize(this->nirr_[iq]); + for (int iirr = 0; iirr < this->nirr_[iq]; ++iirr) + { + this->irrep_modes_[iq][iirr] = this->little_group_.get_mode_basis(iirr); + } + } } } // namespace ModuleCell \ No newline at end of file diff --git a/source/source_cell/qlist.h b/source/source_cell/qlist.h index 55385ec4208..7a60e42f22b 100644 --- a/source/source_cell/qlist.h +++ b/source/source_cell/qlist.h @@ -2,25 +2,33 @@ * @file qlist.h * @brief QList class for managing q-points. * @author Mohan Chen (added on 2026-05-18) - * @note This code is currently in the design phase and has not been - * put into production yet. It may change in the future. - * Please use this code with caution. Only developers who know - * what they are doing should use this code. + * @note The q-point mesh generation and star (IBZ) reduction share the + * spin-free functionality of ModuleCell::ReciprocalGrid with + * K_Vectors; the irreducible-representation analysis is added on top. */ #ifndef QLIST_H #define QLIST_H #include "source_base/vector3.h" +#include "module_symmetry/little_group.h" #include "module_symmetry/symmetry.h" #include "unitcell.h" +#include "reciprocal_grid.h" #include namespace ModuleCell { /** * @brief QList class for managing q-points. + * + * Inherits the spin-free reciprocal-grid functionality (mesh generation, + * coordinate conversion, weights, star reduction primitive) from + * ReciprocalGrid and adds the q-specific star reduction (always including + * the time-reversal partner -q, no spin expansion) together with the + * little-group irreducible-representation data (placeholder in the current + * version). */ -class QList { +class QList : public ModuleCell::ReciprocalGrid { public: /** * @brief Default constructor. @@ -31,9 +39,9 @@ class QList { * @brief Destructor. */ ~QList(); - + /** - * @brief Generate q-point mesh. + * @brief Generate the Monkhorst-Pack q-point mesh and reduce it by star. * * @param ucell unit cell * @param symm symmetry object @@ -42,35 +50,54 @@ class QList { */ void generate_mesh(UnitCell& ucell, ModuleSymmetry::Symmetry& symm, const std::vector& mp_grid, bool use_irreps); - + /** - * @brief Read q-points from file. + * @brief Read q-points from a q-points file. + * + * Supports the same formats as the K-points file: a Monkhorst-Pack + * mesh (nkstot == 0), an explicit Direct/Cartesian list, or a + * Line_Direct/Line_Cartesian path (interpolated). No symmetry + * reduction is performed here (the interface has no symmetry object); + * use generate_mesh for a symmetry-reduced mesh. * * @param filename filename * @param ucell unit cell */ void read_from_file(const std::string& filename, UnitCell& ucell); - + + /** + * @brief Print the q-points in both Cartesian and direct coordinates. + * @param ofs output stream + */ + void print_qlists(std::ofstream& ofs) const; + /** * @brief Get the number of q-points. * @return number of q-points */ - int get_nq() const { return nq_; } - + int get_nq() const { return this->nkstot; } + /** * @brief Get q-point at given index. * @param idx q-point index - * @return q-point vector + * @return q-point vector (direct coordinates) */ - ModuleBase::Vector3 get_q(int idx) const { return qvec_[idx]; } - + ModuleBase::Vector3 get_q(int idx) const { return this->kvec_d[idx]; } + /** * @brief Get the number of irreps at given q-point. * @param idx q-point index - * @return number of irreps + * @return number of irreps (0 if no irrep data was computed) */ - int get_nirr(int idx) const { return nirr_[idx]; } - + int get_nirr(int idx) const + { + if (idx < 0 || idx >= static_cast(this->nirr_.size())) + { + return 0; + } + return nirr_[idx]; + } + /** * @brief Get irrep modes at given q-point and irrep index. * @param q_idx q-point index @@ -79,27 +106,49 @@ class QList { */ std::vector get_irrep_modes(int q_idx, int irrep_idx) const; + /** + * @brief Reduce the q-points by star (time-reversal included). + * + * Implements the pure-virtual hook of ReciprocalGrid: builds the + * reciprocal-space point-group operations via build_star_ops, always + * doubles them by -q, and folds the mesh with ReciprocalGrid::reduce_ibz. + * + * @param ucell unit cell + * @param symm symmetry object + * @param use_symm whether symmetry reduction is enabled + * @param skpt output string (unused for q-points) + * @param match set to false if the reciprocal lattice is not compatible + * with the real-space lattice + */ + void reduce_by_symmetry(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + bool& match) override; + private: - int nq_ = 0; ///< number of q-points - std::vector> qvec_; ///< q-point vectors std::vector nirr_; ///< number of irreps for each q-point std::vector>> irrep_modes_; ///< irrep modes - + ModuleSymmetry::LittleGroup little_group_; ///< little group of the current q-point + /** - * @brief Reduce q-points using symmetry. + * @brief Interpolate q-points between successive special points. * - * @param ucell unit cell - * @param symm symmetry object + * @param ifq input stream positioned at the special-point list + * @param qvec output q-point coordinates */ - void reduce(UnitCell& ucell, ModuleSymmetry::Symmetry& symm); - + void interpolate_q_between(std::ifstream& ifq, std::vector>& qvec); + /** * @brief Get irreps for each q-point. * + * Currently fills a fully-symmetric placeholder (one A1 irrep per + * q-point); the LittleGroup decomposition is implemented in Phase 3. + * * @param ucell unit cell * @param symm symmetry object */ - void get_irreps(UnitCell& ucell, ModuleSymmetry::Symmetry& symm); + void get_irreps(const UnitCell& ucell, const ModuleSymmetry::Symmetry& symm); }; } // namespace ModuleCell diff --git a/source/source_cell/reciprocal_grid.cpp b/source/source_cell/reciprocal_grid.cpp new file mode 100644 index 00000000000..c35c4981bb6 --- /dev/null +++ b/source/source_cell/reciprocal_grid.cpp @@ -0,0 +1,617 @@ +/** + * @file reciprocal_grid.cpp + * @brief Implementation of the ModuleCell::ReciprocalGrid base class. + * @note Spin-free logic migrated from K_Vectors (klist.cpp) and + * KVectorUtils (k_vector_utils.cpp) on 2026-08-14. + */ +#include "reciprocal_grid.h" + +#include "source_cell/unitcell.h" +#include "source_cell/module_symmetry/symmetry.h" +#include "source_base/global_function.h" +#include "source_base/formatter.h" +#include "source_base/global_variable.h" +#include "source_base/matrix3.h" +#include "source_base/tool_quit.h" +#include "source_base/tool_title.h" + +namespace ModuleCell +{ + +void ReciprocalGrid::renew(const int& kpoint_number) +{ + kvec_c.resize(kpoint_number); + kvec_d.resize(kpoint_number); + kvec_c_full.resize(kpoint_number); + wk.resize(kpoint_number); + ngk.resize(kpoint_number); + + return; +} + +double ReciprocalGrid::Monkhorst_Pack_formula(const int& k_type, const double& offset, const int& n, const int& dim) +{ + double coordinate = 0.0; + if (k_type == 1) + { + coordinate = (offset + 2.0 * (double)n - (double)dim - 1.0) / (2.0 * (double)dim); + } + else + { + coordinate = (offset + (double)n - 1.0) / (double)dim; + } + return coordinate; +} + +void ReciprocalGrid::Monkhorst_Pack(const int* nmp_in, const double* koffset_in, const int k_type) +{ + const int mpnx = nmp_in[0]; + const int mpny = nmp_in[1]; + const int mpnz = nmp_in[2]; + + this->nkstot = mpnx * mpny * mpnz; + // only can renew after nkstot is estimated. + this->renew(nkstot * spin_factor()); + + for (int x = 1; x <= mpnx; x++) + { + double v1 = Monkhorst_Pack_formula(k_type, koffset_in[0], x, mpnx); + if (std::abs(v1) < 1.0e-10) { + v1 = 0.0; // mohan update 2012-06-10 + } + for (int y = 1; y <= mpny; y++) + { + double v2 = Monkhorst_Pack_formula(k_type, koffset_in[1], y, mpny); + if (std::abs(v2) < 1.0e-10) { + v2 = 0.0; + } + for (int z = 1; z <= mpnz; z++) + { + double v3 = Monkhorst_Pack_formula(k_type, koffset_in[2], z, mpnz); + if (std::abs(v3) < 1.0e-10) { + v3 = 0.0; + } + // index of nks kpoint + const int i = mpnx * mpny * (z - 1) + mpnx * (y - 1) + (x - 1); + kvec_d[i].set(v1, v2, v3); + } + } + } + + const double weight = 1.0 / static_cast(nkstot); + for (int ik = 0; ik < nkstot; ik++) + { + wk[ik] = weight; + } + this->kd_done = true; + + return; +} + +void ReciprocalGrid::kvec_d2c(const ModuleBase::Matrix3& reciprocal_vec) +{ + if (this->kvec_d.size() != this->kvec_c.size()) + { + this->kvec_c.resize(this->kvec_d.size()); + } + int nks = this->kvec_d.size(); // always convert all k vectors + + for (int i = 0; i < nks; i++) + { + // mohan fixed bug 2010-1-10 + if (std::abs(this->kvec_d[i].x) < 1.0e-10) + { + this->kvec_d[i].x = 0.0; + } + if (std::abs(this->kvec_d[i].y) < 1.0e-10) + { + this->kvec_d[i].y = 0.0; + } + if (std::abs(this->kvec_d[i].z) < 1.0e-10) + { + this->kvec_d[i].z = 0.0; + } + + this->kvec_c[i] = this->kvec_d[i] * reciprocal_vec; + + // mohan add2012-06-10 + if (std::abs(this->kvec_c[i].x) < 1.0e-10) + { + this->kvec_c[i].x = 0.0; + } + if (std::abs(this->kvec_c[i].y) < 1.0e-10) + { + this->kvec_c[i].y = 0.0; + } + if (std::abs(this->kvec_c[i].z) < 1.0e-10) + { + this->kvec_c[i].z = 0.0; + } + } +} + +void ReciprocalGrid::kvec_c2d(const ModuleBase::Matrix3& latvec) +{ + if (this->kvec_d.size() != this->kvec_c.size()) + { + this->kvec_d.resize(this->kvec_c.size()); + } + int nks = this->kvec_d.size(); // always convert all k vectors + + ModuleBase::Matrix3 RT = latvec.Transpose(); + for (int i = 0; i < nks; i++) + { + // mohan fixed bug 2011-03-07 + this->kvec_d[i] = this->kvec_c[i] * RT; + } +} + +void ReciprocalGrid::set_both_kvec(const ModuleBase::Matrix3& G, const ModuleBase::Matrix3& R, std::string& skpt) +{ + if (true) // once-per-run gate (the FINAL_SCF hole is irrelevant here) + { + if (this->k_nkstot == 0) + { + this->kd_done = true; + this->kc_done = false; + } + else + { + if (this->k_kword == "Cartesian" || this->k_kword == "C") + { + this->kc_done = true; + this->kd_done = false; + } + else if (this->k_kword == "Direct" || this->k_kword == "D") + { + this->kd_done = true; + this->kc_done = false; + } + else + { + GlobalV::ofs_warning << " Error : neither Cartesian nor Direct kpoint." << std::endl; + } + } + } + + // set cartesian k vectors. + if (!this->kc_done && this->kd_done) + { + this->kvec_d2c(G); + this->kc_done = true; + } + + // set direct k vectors + else if (this->kc_done && !this->kd_done) + { + this->kvec_c2d(R); + this->kd_done = true; + } + std::string table; + table += " K-POINTS DIRECT COORDINATES\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s\n", "KPOINTS", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT"); + for (int i = 0; i < this->nkstot; i++) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", + i + 1, + this->kvec_d[i].x, + this->kvec_d[i].y, + this->kvec_d[i].z, + this->wk[i]); + } + GlobalV::ofs_running << table << std::endl; + if (GlobalV::MY_RANK == 0) + { + std::stringstream ss; + ss << " " << std::setw(40) << "nkstot now" + << " = " << this->nkstot << std::endl; + ss << table << std::endl; + skpt = ss.str(); + } + return; +} + +void ReciprocalGrid::normalize_wk(const int& degspin) +{ + if (GlobalV::MY_RANK != 0) { + return; + } + double sum = 0.0; + + for (int ik = 0; ik < nkstot; ik++) + { + sum += this->wk[ik]; + } + + // If sum of weights is zero or very small, set equal weights + if (sum < 1e-10) + { + ModuleBase::WARNING("ReciprocalGrid::normalize_wk", + "Sum of k-point weights is zero or very small. " + "Setting equal weights for all k-points."); + for (int ik = 0; ik < nkstot; ik++) + { + this->wk[ik] = 1.0 / double(nkstot); + } + sum = 1.0; + } + + for (int ik = 0; ik < nkstot; ik++) + { + this->wk[ik] /= sum; + } + + for (int ik = 0; ik < nkstot; ik++) + { + this->wk[ik] *= degspin; + } + + return; +} + +void ReciprocalGrid::print_klists(std::ofstream& ofs) const +{ + ModuleBase::TITLE("ReciprocalGrid", "print_klists"); + int nks = this->nks; + int nkstot = this->nkstot; + + if (nkstot < nks) + { + std::cout << "\n nkstot=" << nkstot; + std::cout << "\n nks=" << nks; + ModuleBase::WARNING_QUIT("print_klists", "nkstot < nks"); + } + std::string table; + table += " K-POINTS CARTESIAN COORDINATES\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s\n", "KPOINTS", "CARTESIAN_X", "CARTESIAN_Y", "CARTESIAN_Z", "WEIGHT"); + for (int i = 0; i < nks; i++) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", + i + 1, + this->kvec_c[i].x, + this->kvec_c[i].y, + this->kvec_c[i].z, + this->wk[i]); + } + ofs << "\n" << table << std::endl; + + table.clear(); + table += " K-POINTS DIRECT COORDINATES\n"; + table += FmtCore::format("%8s%12s%12s%12s%8s\n", "KPOINTS", "DIRECT_X", "DIRECT_Y", "DIRECT_Z", "WEIGHT"); + for (int i = 0; i < nks; i++) + { + table += FmtCore::format("%8d%12.8f%12.8f%12.8f%8.4f\n", + i + 1, + this->kvec_d[i].x, + this->kvec_d[i].y, + this->kvec_d[i].z, + this->wk[i]); + } + ofs << "\n" << table << std::endl; + return; +} + +void ReciprocalGrid::reduce_ibz(const ModuleBase::Matrix3* rot_ops, + int nrotkm, + const ModuleBase::Matrix3& G, + const ModuleBase::Matrix3& k_lattice, + const ModuleBase::Matrix3* kkmatrix, + double epsilon, + std::vector>& vec_ibz, + std::vector& wk_ibz, + std::vector& ibz_index, + std::vector& ibz2bz) +{ + auto equal = [epsilon](double m, double n) { return fabs(m - n) < epsilon; }; + // restrict a vector to (-0.5, 0.5] + auto restrict_kpt = [epsilon](ModuleBase::Vector3& kvec) { + kvec.x = fmod(kvec.x + 100.5 - 0.5 * epsilon, 1) - 0.5 + 0.5 * epsilon; + kvec.y = fmod(kvec.y + 100.5 - 0.5 * epsilon, 1) - 0.5 + 0.5 * epsilon; + kvec.z = fmod(kvec.z + 100.5 - 0.5 * epsilon, 1) - 0.5 + 0.5 * epsilon; + if (std::abs(kvec.x) < epsilon) + { + kvec.x = 0.0; + } + if (std::abs(kvec.y) < epsilon) + { + kvec.y = 0.0; + } + if (std::abs(kvec.z) < epsilon) + { + kvec.z = 0.0; + } + return; + }; + + // direct coordinates of points in the k-lattice + std::vector> kvec_d_k(this->nkstot); + if (this->is_mp) + { + for (int i = 0; i < this->nkstot; ++i) + { + kvec_d_k[i] = this->kvec_d[i] * G * k_lattice.Inverse(); + } + } + + int nkstot_ibz = 0; + + assert(this->nkstot > 0); + std::vector> kvec_d_ibz(this->nkstot); + std::vector wk_ibz_tmp(this->nkstot); // ibz point weight + ibz2bz.resize(this->nkstot); + + // nkstot is the total input points number. + double weight = 1.0 / static_cast(this->nkstot); + + ModuleBase::Vector3 kvec_rot; + ModuleBase::Vector3 kvec_rot_k; + + // update map k -> irreducible k + ibz_index.assign(this->nkstot_full, -1); // -1 means not in ibz list + // search in all k-points. + for (int i = 0; i < this->nkstot; ++i) + { + if (!this->is_mp) { weight = this->wk[i]; } // use the input weight, instead of 1/nkstot + + // restrict to (-0.5, 0.5] + restrict_kpt(this->kvec_d[i]); + + bool already_exist = false; + int exist_number = -1; + // search over all symmetry operations + for (int j = 0; j < nrotkm; ++j) + { + if (!already_exist) + { + kvec_rot = this->kvec_d[i] * rot_ops[j]; // wrong for total energy, but correct for nonlocal force. + restrict_kpt(kvec_rot); + if (this->is_mp) + { + kvec_rot_k = kvec_d_k[i] * kkmatrix[j]; // k-lattice rotation + kvec_rot_k = kvec_rot_k * k_lattice * G.Inverse(); // convert to recip lattice + restrict_kpt(kvec_rot_k); + + assert(equal(kvec_rot.x, kvec_rot_k.x)); + assert(equal(kvec_rot.y, kvec_rot_k.y)); + assert(equal(kvec_rot.z, kvec_rot_k.z)); + kvec_rot_k = kvec_rot_k * G * k_lattice.Inverse(); // convert back to k-lattice + } + for (int k = 0; k < nkstot_ibz; ++k) + { + if (equal(kvec_rot.x, kvec_d_ibz[k].x) && equal(kvec_rot.y, kvec_d_ibz[k].y) + && equal(kvec_rot.z, kvec_d_ibz[k].z)) + { + already_exist = true; + // find another ibz point, + // but is already in the ibz list. + // so the weight need to +1; + wk_ibz_tmp[k] += weight; + exist_number = k; + break; + } + } + } // end !already_exist + } + // if really there is no equivalent point in the list, then add it. + if (!already_exist) + { + kvec_d_ibz[nkstot_ibz] = this->kvec_d[i]; + ibz_index[i] = nkstot_ibz; + + // the weight should be averaged point weight. + wk_ibz_tmp[nkstot_ibz] = weight; + + // ibz2bz records the index of origin points. + ibz2bz[nkstot_ibz] = i; + ++nkstot_ibz; + } + else + { + double kmol_new = this->kvec_d[i].norm2(); + double kmol_old = kvec_d_ibz[exist_number].norm2(); + + ibz_index[i] = exist_number; + + // why we need this step? + // because in pw_basis.cpp, while calculate ggwfc2, + // if we want to keep the result of symmetry operation is right. + // we need to fix the number of plane wave. + // and the number of plane wave is depending on the |K+G|, + // so we need to |K|max to be the same as 'no symmetry'. + // mohan 2010-01-30 + if (kmol_new > kmol_old) + { + kvec_d_ibz[exist_number] = this->kvec_d[i]; + } + } + } + + vec_ibz.resize(nkstot_ibz); + wk_ibz.resize(nkstot_ibz); + ibz2bz.resize(nkstot_ibz); + for (int i = 0; i < nkstot_ibz; ++i) + { + vec_ibz[i] = kvec_d_ibz[i]; + wk_ibz[i] = wk_ibz_tmp[i]; + } + + return; +} + +bool ReciprocalGrid::build_star_ops(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + ModuleBase::Matrix3& k_vec, + std::vector& kgmatrix, + int& nrotkm) const +{ + // k-lattice: "pricell" of reciprocal space + // CAUTION: should fit into all k-input method, not only MP !!! + // the basis vector of reciprocal lattice: recip_vec1, recip_vec2, recip_vec3 + ModuleBase::Vector3 recip_vec1(ucell.G.e11, ucell.G.e12, ucell.G.e13); + ModuleBase::Vector3 recip_vec2(ucell.G.e21, ucell.G.e22, ucell.G.e23); + ModuleBase::Vector3 recip_vec3(ucell.G.e31, ucell.G.e32, ucell.G.e33); + ModuleBase::Vector3 k_vec1, k_vec2, k_vec3; + if (this->is_mp) + { + k_vec1 = ModuleBase::Vector3(recip_vec1.x / this->nmp[0], recip_vec1.y / this->nmp[0], recip_vec1.z / this->nmp[0]); + k_vec2 = ModuleBase::Vector3(recip_vec2.x / this->nmp[1], recip_vec2.y / this->nmp[1], recip_vec2.z / this->nmp[1]); + k_vec3 = ModuleBase::Vector3(recip_vec3.x / this->nmp[2], recip_vec3.y / this->nmp[2], recip_vec3.z / this->nmp[2]); + k_vec = ModuleBase::Matrix3(k_vec1.x, + k_vec1.y, + k_vec1.z, + k_vec2.x, + k_vec2.y, + k_vec2.z, + k_vec3.x, + k_vec3.y, + k_vec3.z); + } + + ModuleBase::Matrix3 inv(-1, 0, 0, 0, -1, 0, 0, 0, -1); + ModuleBase::Matrix3 ind(1, 0, 0, 0, 1, 0, 0, 0, 1); + + nrotkm = 0; + if (use_symm) + { + // bravais type of reciprocal lattice and k-lattice + + double recip_vec_const[6]; + double recip_vec0_const[6]; + double k_vec_const[6]; + double k_vec0_const[6]; + int recip_brav_type = 15; + int k_brav_type = 15; + std::string recip_brav_name; + std::string k_brav_name; + ModuleBase::Vector3 k_vec01 = k_vec1, k_vec02 = k_vec2, k_vec03 = k_vec3; + + // determine the Bravais type and related parameters of the lattice + symm.lattice_type(recip_vec1, + recip_vec2, + recip_vec3, + recip_vec1, + recip_vec2, + recip_vec3, + recip_vec_const, + recip_vec0_const, + recip_brav_type, + recip_brav_name, + ucell.atoms, + false, + nullptr, + 1e-6); + GlobalV::ofs_running << "\n For reciprocal-space lattice" << std::endl; + ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice type", recip_brav_type); + ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice name", recip_brav_name); + + // the map of bravis lattice from real to reciprocal space + // for example, 3(fcc) in real space matches 2(bcc) in reciprocal space + std::vector ibrav_a2b{1, 3, 2, 4, 5, 6, 7, 8, 10, 9, 11, 12, 13, 14}; + // check if the reciprocal lattice is compatible with the real space lattice + auto ibrav_match = [&](int ibrav_b) -> bool { + const int& ibrav_a = symm.real_brav; + if (ibrav_a < 1 || ibrav_a > 14) + { + return false; + } + return (ibrav_b == ibrav_a2b[ibrav_a - 1]); + }; + if (!ibrav_match(recip_brav_type)) // if not match, exit and return + { + GlobalV::ofs_running << "Error: Bravais lattice type of reciprocal lattice is not compatible with that of " + "real space lattice:" + << std::endl; + GlobalV::ofs_running << "ibrav of real space lattice: " << symm.ilattname << std::endl; + GlobalV::ofs_running << "ibrav of reciprocal lattice: " << recip_brav_name << std::endl; + GlobalV::ofs_running << "(which should be " << ibrav_a2b[symm.real_brav - 1] << ")." << std::endl; + return false; + } + + // if match, continue + if (this->is_mp) + { + symm.lattice_type(k_vec1, + k_vec2, + k_vec3, + k_vec01, + k_vec02, + k_vec03, + k_vec_const, + k_vec0_const, + k_brav_type, + k_brav_name, + ucell.atoms, + false, + nullptr, + 1e-6); + GlobalV::ofs_running << "\n For k-vectors" << std::endl; + ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice type", k_brav_type); + ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "Bravais lattice name", k_brav_name); + } + // point-group analysis of reciprocal lattice + ModuleBase::Matrix3 bsymop[48]; + int bnop = 0; + // search again + symm.lattice_type(recip_vec1, + recip_vec2, + recip_vec3, + recip_vec1, + recip_vec2, + recip_vec3, + recip_vec_const, + recip_vec0_const, + recip_brav_type, + recip_brav_name, + ucell.atoms, + false, + nullptr, + 1e-6); + ModuleBase::Matrix3 b_optlat_new(recip_vec1.x, recip_vec1.y, recip_vec1.z, + recip_vec2.x, recip_vec2.y, recip_vec2.z, + recip_vec3.x, recip_vec3.y, recip_vec3.z); + // set the crystal point-group symmetry operation + const int cal_symm_repr[2] = {0, 6}; + symm.setgroup(bsymop, bnop, recip_brav_type, cal_symm_repr); + // transform the above symmetric operation matrices between different coordinate + symm.gmatrix_convert(bsymop, bsymop, bnop, b_optlat_new, ucell.G); + + // check if all the kgmatrix are in bsymop + auto matequal = [&symm](ModuleBase::Matrix3 a, ModuleBase::Matrix3 b) { + return (symm.equal(a.e11, b.e11) && symm.equal(a.e12, b.e12) && symm.equal(a.e13, b.e13) + && symm.equal(a.e21, b.e21) && symm.equal(a.e22, b.e22) && symm.equal(a.e23, b.e23) + && symm.equal(a.e31, b.e31) && symm.equal(a.e32, b.e32) && symm.equal(a.e33, b.e33)); + }; + for (int i = 0; i < symm.nrotk; ++i) + { + bool found = false; + for (int j = 0; j < bnop; ++j) + { + if (matequal(symm.kgmatrix[i], bsymop[j])) + { + found = true; + break; + } + } + if (!found) + { + return false; + } + } + nrotkm = symm.nrotk; + for (int i = 0; i < nrotkm; ++i) + { + kgmatrix[i] = symm.kgmatrix[i]; + } + } + else if (this->is_mp) // only include for Monkhorst-Pack grid + { + nrotkm = 2; + kgmatrix[0] = ind; + kgmatrix[1] = inv; + } + + return true; +} + +} // namespace ModuleCell diff --git a/source/source_cell/reciprocal_grid.h b/source/source_cell/reciprocal_grid.h new file mode 100644 index 00000000000..fffabbb720b --- /dev/null +++ b/source/source_cell/reciprocal_grid.h @@ -0,0 +1,185 @@ +/** + * @file reciprocal_grid.h + * @brief Abstract base class for reciprocal-space point grids. + * @note Extracted from K_Vectors / KVectorUtils (2026-08-14) so that both + * k-points (K_Vectors) and q-points (QList) share the common + * spin-free functionality: mesh generation, coordinate conversion, + * weight normalization, printing and star (IBZ) reduction. + */ +#ifndef RECIPROCAL_GRID_H +#define RECIPROCAL_GRID_H + +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include +#include +#include + +class UnitCell; +namespace ModuleSymmetry +{ +class Symmetry; +} + +namespace ModuleCell +{ + +/** + * @brief Abstract base class shared by K_Vectors (electrons) and QList (phonons). + * + * This base class is deliberately spin-free and irrep-free: + * - spin expansion (isk, nspin doubling) is implemented in K_Vectors; + * - irreducible-representation analysis is implemented in QList. + */ +class ReciprocalGrid +{ + public: + /// Cartesian coordinates of the points. + std::vector> kvec_c; + /// Direct coordinates of the points. + std::vector> kvec_d; + /// Cartesian coordinates of the full (unreduced) mesh. + std::vector> kvec_c_full; + /// Weight of each point. + std::vector wk; + /// Number of plane waves for each point (filled by the PW basis). + std::vector ngk; + /// Monkhorst-Pack grid dimensions. + int nmp[3] = {0, 0, 0}; + /// Index of the k-line segment each point belongs to. + std::vector kl_segids; + + /// Whether the Cartesian coordinates have been computed. + bool kc_done = false; + /// Whether the direct coordinates have been computed. + bool kd_done = false; + + /// Number of points in the current pool (spin-free view). + int nks = 0; + /// Total number of (symmetry-reduced) points. + int nkstot = 0; + /// Total number of points before symmetry reduction. + int nkstot_full = 0; + + ReciprocalGrid() = default; + virtual ~ReciprocalGrid() = default; + ReciprocalGrid& operator=(const ReciprocalGrid&) = default; + ReciprocalGrid& operator=(ReciprocalGrid&&) = default; + + /** + * @brief Resize the point-related containers. + * + * @param kpoint_number new number of points + */ + virtual void renew(const int& kpoint_number); + + /// @brief Coordinate of a point generated by the Monkhorst-Pack scheme. + double Monkhorst_Pack_formula(const int& k_type, const double& offset, const int& n, const int& dim); + + /// @brief Generate a Monkhorst-Pack mesh. + void Monkhorst_Pack(const int* nmp_in, const double* koffset_in, const int k_type); + + /// @brief Convert direct to Cartesian coordinates. + void kvec_d2c(const ModuleBase::Matrix3& reciprocal_vec); + + /// @brief Convert Cartesian to direct coordinates. + void kvec_c2d(const ModuleBase::Matrix3& latvec); + + /** + * @brief Set both the direct and Cartesian coordinates, and print the table. + * + * @param G reciprocal lattice matrix + * @param R real space lattice matrix + * @param skpt output string holding the point table + */ + void set_both_kvec(const ModuleBase::Matrix3& G, const ModuleBase::Matrix3& R, std::string& skpt); + + /// @brief Normalize the weights so that they sum to the spin degeneracy. + void normalize_wk(const int& degspin); + + /// @brief Print the points in both Cartesian and direct coordinates. + void print_klists(std::ofstream& ofs) const; + + /** + * @brief Star (IBZ) reduction primitive shared by k-points and q-points. + * + * Rotates every point by each operation in `rot_ops`, folds equivalent + * points together and accumulates their weights. The rotation matrices in + * the k-lattice (Monkhorst-Pack) frame are provided in `kkmatrix` when + * `is_mp` is true; the k-lattice basis is `k_lattice`. + * + * @param rot_ops rotation matrices in the reciprocal-space frame + * @param nrotkm number of rotation operations + * @param G reciprocal lattice matrix + * @param k_lattice k-lattice basis matrix (valid when is_mp) + * @param kkmatrix rotation matrices in the k-lattice frame (valid when is_mp) + * @param epsilon symmetry tolerance used by restrict/equivalence checks + * @param vec_ibz output: irreducible points + * @param wk_ibz output: weight of each irreducible point + * @param ibz_index output: mapping point index -> irreducible index + * @param ibz2bz output: origin (full-mesh) index of each irreducible point + */ + void reduce_ibz(const ModuleBase::Matrix3* rot_ops, + int nrotkm, + const ModuleBase::Matrix3& G, + const ModuleBase::Matrix3& k_lattice, + const ModuleBase::Matrix3* kkmatrix, + double epsilon, + std::vector>& vec_ibz, + std::vector& wk_ibz, + std::vector& ibz_index, + std::vector& ibz2bz); + + /** + * @brief Reduce this grid according to symmetry operations. + * + * Pure virtual: implemented by K_Vectors (electron IBZ, including the + * magnetic/time-reversal handling) and by QList (q-point star reduction + * plus irreducible-representation analysis). + */ + virtual void reduce_by_symmetry(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + bool& match) = 0; + + /// Whether this is a Monkhorst-Pack grid. + bool is_mp = false; + /// Total number of points read from a point-list file (0 = auto mesh). + int k_nkstot = 0; + /// Type keyword of the point-list file. + std::string k_kword; + + protected: + /// @brief Spin-like multiplicity used by renew() (1 for q-points). + virtual int spin_factor() const { return 1; } + + /** + * @brief Build the reciprocal-space point-group operations for star reduction. + * + * Determines the Bravais lattice of the reciprocal lattice (and of the + * k-lattice for Monkhorst-Pack meshes), checks its compatibility with the + * real-space lattice, constructs the point-group operations, and verifies + * that every Symmetry::kgmatrix entry belongs to that group. + * + * @param ucell unit cell + * @param symm symmetry of the system + * @param use_symm whether symmetry reduction is enabled + * @param k_vec output: k-lattice basis matrix (valid when is_mp) + * @param kgmatrix output: rotation operations (capacity at least 96) + * @param nrotkm output: number of operations written into kgmatrix + * (0 means no reduction is possible) + * @return false if the reciprocal lattice is incompatible with the + * real-space lattice (the caller should then set match to false) + */ + bool build_star_ops(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + ModuleBase::Matrix3& k_vec, + std::vector& kgmatrix, + int& nrotkm) const; +}; + +} // namespace ModuleCell + +#endif // RECIPROCAL_GRID_H diff --git a/source/source_cell/test/CMakeLists.txt b/source/source_cell/test/CMakeLists.txt index 60a5b5f1a16..ad2bb093012 100644 --- a/source/source_cell/test/CMakeLists.txt +++ b/source/source_cell/test/CMakeLists.txt @@ -86,13 +86,31 @@ AddTest( AddTest( TARGET MODULE_CELL_klist_test LIBS base device symmetry - SOURCES klist_test.cpp ../klist.cpp ../parallel_kpoints.cpp ../k_vector_utils.cpp + SOURCES klist_test.cpp ../klist.cpp ../parallel_kpoints.cpp ../k_vector_utils.cpp ../reciprocal_grid.cpp +) + +AddTest( + TARGET MODULE_CELL_reciprocal_grid_test + LIBS base device symmetry + SOURCES reciprocal_grid_test.cpp ../reciprocal_grid.cpp +) + +AddTest( + TARGET MODULE_CELL_qlist_test + LIBS base device symmetry + SOURCES qlist_test.cpp ../qlist.cpp ../reciprocal_grid.cpp +) + +AddTest( + TARGET MODULE_CELL_little_group_test + LIBS base device symmetry + SOURCES little_group_test.cpp ) AddTest( TARGET MODULE_CELL_klist_test_para1 LIBS base device symmetry - SOURCES klist_test_para.cpp ../klist.cpp ../parallel_kpoints.cpp ../k_vector_utils.cpp + SOURCES klist_test_para.cpp ../klist.cpp ../parallel_kpoints.cpp ../k_vector_utils.cpp ../reciprocal_grid.cpp ) add_test(NAME MODULE_CELL_klist_test_para4 diff --git a/source/source_cell/test/little_group_test.cpp b/source/source_cell/test/little_group_test.cpp new file mode 100644 index 00000000000..153dc9f501d --- /dev/null +++ b/source/source_cell/test/little_group_test.cpp @@ -0,0 +1,168 @@ +#include "gtest/gtest.h" + +#include + +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/magnetism.h" +#include "source_cell/pseudo.h" +#include "source_cell/unitcell.h" +#include "source_cell/module_symmetry/little_group.h" + +#include "source_base/global_variable.h" +#include +#include + +// Linker stubs: the symmetry library needs these symbols (see klist_test.cpp). +pseudo::pseudo() +{ +} +pseudo::~pseudo() +{ +} +Atom::Atom() +{ +} +Atom::~Atom() +{ +} +Atom_pseudo::Atom_pseudo() +{ +} +Atom_pseudo::~Atom_pseudo() +{ +} +SepPot::SepPot() {} +SepPot::~SepPot() {} +UnitCell::UnitCell() +{ +} +UnitCell::~UnitCell() +{ +} +Magnetism::Magnetism() +{ +} +Magnetism::~Magnetism() +{ +} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} + +// abbreviated from module_symmetry/test/symm_test.cpp +struct atomtype_ +{ + std::string atomname; + std::vector> coordinate; +}; + +struct stru_ +{ + int ibrav; + std::string point_group; // Schoenflies symbol + std::string point_group_hm; // Hermann-Mauguin notation. + std::string space_group; + std::vector cell; + std::vector all_type; +}; + +// primitive cubic with one atom at the origin -> O_h (48 operations) +std::vector stru_lib{stru_{1, + "O_h", + "m-3m", + "Pm-3m", + std::vector{1., 0., 0., 0., 1., 0., 0., 0., 1.}, + std::vector{atomtype_{"C", + std::vector>{ + {0., 0., 0.}, + }}}}}; + +class LittleGroupTest : public testing::Test +{ + protected: + UnitCell ucell; + ModuleSymmetry::Symmetry symm; + + void SetUp() override + { + std::vector coord = stru_lib[0].all_type; + ucell.a1 = ModuleBase::Vector3(stru_lib[0].cell[0], stru_lib[0].cell[1], stru_lib[0].cell[2]); + ucell.a2 = ModuleBase::Vector3(stru_lib[0].cell[3], stru_lib[0].cell[4], stru_lib[0].cell[5]); + ucell.a3 = ModuleBase::Vector3(stru_lib[0].cell[6], stru_lib[0].cell[7], stru_lib[0].cell[8]); + ucell.ntype = stru_lib[0].all_type.size(); + ucell.atoms = new Atom[ucell.ntype]; + ucell.nat = 0; + ucell.latvec.e11 = ucell.a1.x; + ucell.latvec.e12 = ucell.a1.y; + ucell.latvec.e13 = ucell.a1.z; + ucell.latvec.e21 = ucell.a2.x; + ucell.latvec.e22 = ucell.a2.y; + ucell.latvec.e23 = ucell.a2.z; + ucell.latvec.e31 = ucell.a3.x; + ucell.latvec.e32 = ucell.a3.y; + ucell.latvec.e33 = ucell.a3.z; + ucell.GT = ucell.latvec.Inverse(); + ucell.G = ucell.GT.Transpose(); + ucell.lat0 = 1.8897261254578281; + for (int i = 0; i < coord.size(); i++) + { + ucell.atoms[i].label = coord[i].atomname; + ucell.atoms[i].na = coord[i].coordinate.size(); + ucell.atoms[i].tau.resize(ucell.atoms[i].na); + ucell.atoms[i].taud.resize(ucell.atoms[i].na); + for (int j = 0; j < ucell.atoms[i].na; j++) + { + std::vector this_atom = coord[i].coordinate[j]; + ucell.atoms[i].tau[j] = ModuleBase::Vector3(this_atom[0], this_atom[1], this_atom[2]); + ucell.atoms[i].taud[j] = ModuleBase::Vector3(0.0, 0.0, 0.0); + } + ucell.nat += ucell.atoms[i].na; + } + std::ofstream ofs_running("tmp_little_group"); + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + } + + void TearDown() override + { + delete[] ucell.atoms; + remove("tmp_little_group"); + } +}; + +TEST_F(LittleGroupTest, LittleGroupSizeAtKnownPoints) +{ + // the full space group of the primitive cubic cell + EXPECT_EQ(symm.nrotk, 48); + + // Gamma: all operations keep (0,0,0) + ModuleSymmetry::LittleGroup lg; + lg.set_q(ModuleBase::Vector3(0.0, 0.0, 0.0), symm); + EXPECT_EQ(lg.get_little_group_ops().size(), 48); + + // R point (1/2,1/2,1/2): -q is congruent to q (mod 1), so also all 48 + lg.set_q(ModuleBase::Vector3(0.5, 0.5, 0.5), symm); + EXPECT_EQ(lg.get_little_group_ops().size(), 48); + + // X point (1/2,0,0): D_4h little group, 16 operations + lg.set_q(ModuleBase::Vector3(0.5, 0.0, 0.0), symm); + EXPECT_EQ(lg.get_little_group_ops().size(), 16); + + // M point (1/2,1/2,0): D_4h little group, 16 operations + lg.set_q(ModuleBase::Vector3(0.5, 0.5, 0.0), symm); + EXPECT_EQ(lg.get_little_group_ops().size(), 16); + + // generic point: only the identity + lg.set_q(ModuleBase::Vector3(0.13, 0.27, 0.41), symm); + EXPECT_EQ(lg.get_little_group_ops().size(), 1); +} + +TEST_F(LittleGroupTest, PlaceholderIrrep) +{ + ModuleSymmetry::LittleGroup lg; + lg.set_q(ModuleBase::Vector3(0.0, 0.0, 0.0), symm); + + EXPECT_EQ(lg.get_nirr(), 1); // fully-symmetric A1 placeholder + EXPECT_TRUE(lg.get_mode_basis(0).empty()); + EXPECT_EQ(lg.get_q(), ModuleBase::Vector3(0.0, 0.0, 0.0)); +} diff --git a/source/source_cell/test/qlist_test.cpp b/source/source_cell/test/qlist_test.cpp new file mode 100644 index 00000000000..647928dcfbe --- /dev/null +++ b/source/source_cell/test/qlist_test.cpp @@ -0,0 +1,447 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#define private public +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/pseudo.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_cell/magnetism.h" +#undef private +#include "source_base/mathzone.h" +#include "source_base/parallel_global.h" +#include "source_base/global_variable.h" + +pseudo::pseudo() +{ +} +pseudo::~pseudo() +{ +} +Atom::Atom() +{ +} +Atom::~Atom() +{ +} +Atom_pseudo::Atom_pseudo() +{ +} +Atom_pseudo::~Atom_pseudo() +{ +} +SepPot::SepPot() {} +SepPot::~SepPot() {} +UnitCell::UnitCell() +{ +} +UnitCell::~UnitCell() +{ +} +Magnetism::Magnetism() +{ +} +Magnetism::~Magnetism() +{ +} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} + +/************************************************ + * unit test of class QList + ***********************************************/ + +/** + * - Tested Functions: + * - generate_mesh() + * - the Monkhorst-Pack q-point mesh is generated and reduced by star + * (time-reversal included) + * - get_nq() / get_q() + * - access the reduced q-point list + * - get_nirr() / get_irrep_modes() + * - placeholder irrep data (one fully-symmetric irrep per q-point) + * - read_from_file() + * - placeholder interface, must not crash + */ + +// abbreviated from module_symmetry/test/symm_test.cpp and klist_test.cpp +struct atomtype_ +{ + std::string atomname; + std::vector> coordinate; +}; + +struct stru_ +{ + int ibrav; + std::string point_group; // Schoenflies symbol + std::string point_group_hm; // Hermann-Mauguin notation. + std::string space_group; + std::vector cell; + std::vector all_type; +}; + +std::vector stru_lib{stru_{1, + "O_h", + "m-3m", + "Pm-3m", + std::vector{1., 0., 0., 0., 1., 0., 0., 0., 1.}, + std::vector{atomtype_{"C", + std::vector>{ + {0., 0., 0.}, + }}}}}; + +class QListTest : public testing::Test +{ + protected: + ModuleCell::QList qlist; + std::ifstream ifs; + std::ofstream ofs; + std::ofstream ofs_running; + std::string output; + + UnitCell ucell; + void construct_ucell(stru_& stru) + { + std::vector coord = stru.all_type; + ucell.a1 = ModuleBase::Vector3(stru.cell[0], stru.cell[1], stru.cell[2]); + ucell.a2 = ModuleBase::Vector3(stru.cell[3], stru.cell[4], stru.cell[5]); + ucell.a3 = ModuleBase::Vector3(stru.cell[6], stru.cell[7], stru.cell[8]); + ucell.ntype = stru.all_type.size(); + ucell.atoms = new Atom[ucell.ntype]; + ucell.nat = 0; + ucell.latvec.e11 = ucell.a1.x; + ucell.latvec.e12 = ucell.a1.y; + ucell.latvec.e13 = ucell.a1.z; + ucell.latvec.e21 = ucell.a2.x; + ucell.latvec.e22 = ucell.a2.y; + ucell.latvec.e23 = ucell.a2.z; + ucell.latvec.e31 = ucell.a3.x; + ucell.latvec.e32 = ucell.a3.y; + ucell.latvec.e33 = ucell.a3.z; + ucell.GT = ucell.latvec.Inverse(); + ucell.G = ucell.GT.Transpose(); + ucell.lat0 = 1.8897261254578281; + for (int i = 0; i < coord.size(); i++) + { + ucell.atoms[i].label = coord[i].atomname; + ucell.atoms[i].na = coord[i].coordinate.size(); + ucell.atoms[i].tau.resize(ucell.atoms[i].na); + ucell.atoms[i].taud.resize(ucell.atoms[i].na); + for (int j = 0; j < ucell.atoms[i].na; j++) + { + std::vector this_atom = coord[i].coordinate[j]; + ucell.atoms[i].tau[j] = ModuleBase::Vector3(this_atom[0], this_atom[1], this_atom[2]); + ModuleBase::Mathzone::Cartesian_to_Direct(ucell.atoms[i].tau[j].x, + ucell.atoms[i].tau[j].y, + ucell.atoms[i].tau[j].z, + ucell.a1.x, + ucell.a1.y, + ucell.a1.z, + ucell.a2.x, + ucell.a2.y, + ucell.a2.z, + ucell.a3.x, + ucell.a3.y, + ucell.a3.z, + ucell.atoms[i].taud[j].x, + ucell.atoms[i].taud[j].y, + ucell.atoms[i].taud[j].z); + } + ucell.nat += ucell.atoms[i].na; + } + } + + void ClearUcell() + { + delete[] ucell.atoms; + } +}; + +TEST_F(QListTest, GenerateMeshFullSymmetry) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_1"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + qlist.generate_mesh(ucell, symm, {8, 8, 8}, true); + + // full mesh 512 -> irreducible q-points of the primitive cubic lattice + EXPECT_EQ(qlist.nkstot_full, 512); + EXPECT_EQ(qlist.get_nq(), 35); + EXPECT_EQ(qlist.get_nq(), qlist.nkstot); + EXPECT_TRUE(qlist.is_mp); + + // weights must sum to 1 after normalization inside generate_mesh + double sum = 0.0; + for (int i = 0; i < qlist.get_nq(); ++i) + { + sum += qlist.wk[i]; + } + EXPECT_NEAR(sum, 1.0, 1e-10); + + // q-points must be unique + for (int i = 0; i < qlist.get_nq(); ++i) + { + for (int j = i + 1; j < qlist.get_nq(); ++j) + { + EXPECT_FALSE(qlist.get_q(i) == qlist.get_q(j)); + } + } + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_1"); +} + +TEST_F(QListTest, GenerateMeshSmallGrid) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_2"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + qlist.generate_mesh(ucell, symm, {2, 2, 2}, true); + + // {0,0.5}^3 under O_h folds to Gamma + X + M + R + EXPECT_EQ(qlist.nkstot_full, 8); + EXPECT_EQ(qlist.get_nq(), 4); + + // the first irreducible q-point must be Gamma (0,0,0) + EXPECT_DOUBLE_EQ(qlist.get_q(0).x, 0.0); + EXPECT_DOUBLE_EQ(qlist.get_q(0).y, 0.0); + EXPECT_DOUBLE_EQ(qlist.get_q(0).z, 0.0); + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_2"); +} + +TEST_F(QListTest, GammaOnlyGrid) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_3"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + qlist.generate_mesh(ucell, symm, {1, 1, 1}, true); + + EXPECT_EQ(qlist.nkstot_full, 1); + EXPECT_EQ(qlist.get_nq(), 1); + EXPECT_DOUBLE_EQ(qlist.wk[0], 1.0); + EXPECT_DOUBLE_EQ(qlist.get_q(0).x, 0.0); + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_3"); +} + +TEST_F(QListTest, IrrepPlaceholder) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_4"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + qlist.generate_mesh(ucell, symm, {2, 2, 2}, true); + + // placeholder: one fully-symmetric irrep per q-point, empty mode list + for (int i = 0; i < qlist.get_nq(); ++i) + { + EXPECT_EQ(qlist.get_nirr(i), 1); + EXPECT_TRUE(qlist.get_irrep_modes(i, 0).empty()); + } + + // out-of-range access must return an empty list instead of crashing + EXPECT_TRUE(qlist.get_irrep_modes(-1, 0).empty()); + EXPECT_TRUE(qlist.get_irrep_modes(qlist.get_nq(), 0).empty()); + EXPECT_TRUE(qlist.get_irrep_modes(0, 5).empty()); + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_4"); +} + +TEST_F(QListTest, CartesianCoordinatesComputed) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_cart"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + qlist.generate_mesh(ucell, symm, {2, 2, 2}, true); + + // after generate_mesh the Cartesian coordinates must be available and + // consistent with the direct ones: kvec_c = kvec_d * G + EXPECT_TRUE(qlist.kc_done); + for (int i = 0; i < qlist.get_nq(); ++i) + { + ModuleBase::Vector3 qc = qlist.kvec_d[i] * ucell.G; + EXPECT_DOUBLE_EQ(qlist.kvec_c[i].x, qc.x); + EXPECT_DOUBLE_EQ(qlist.kvec_c[i].y, qc.y); + EXPECT_DOUBLE_EQ(qlist.kvec_c[i].z, qc.z); + } + // Gamma is the first irreducible q-point + EXPECT_DOUBLE_EQ(qlist.kvec_c[0].x, 0.0); + EXPECT_DOUBLE_EQ(qlist.kvec_c[0].y, 0.0); + EXPECT_DOUBLE_EQ(qlist.kvec_c[0].z, 0.0); + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_cart"); +} + +TEST_F(QListTest, UseIrrepsSwitch) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_irreps"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + // use_irreps = false: the q mesh is still reduced, but no irrep data + qlist.generate_mesh(ucell, symm, {2, 2, 2}, false); + EXPECT_EQ(qlist.get_nq(), 4); + EXPECT_EQ(qlist.get_nirr(0), 0); // no irrep data was computed + EXPECT_TRUE(qlist.get_irrep_modes(0, 0).empty()); + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_irreps"); +} + +TEST_F(QListTest, PrintQlists) +{ + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_qlist_print"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + + qlist.generate_mesh(ucell, symm, {1, 1, 1}, false); + + std::ofstream ofs("tmp_qlist_print_out"); + qlist.print_qlists(ofs); + ofs.close(); + + // the printed table must contain both coordinate frames + std::ifstream ifs("tmp_qlist_print_out"); + std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + EXPECT_NE(content.find("Q-POINTS CARTESIAN COORDINATES"), std::string::npos); + EXPECT_NE(content.find("Q-POINTS DIRECT COORDINATES"), std::string::npos); + + ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_print"); + remove("tmp_qlist_print_out"); +} + +TEST_F(QListTest, ReadFromFileDirect) +{ + construct_ucell(stru_lib[0]); + + // explicit direct-coordinate q-point list with weights + const char* fname = "tmp_qpoints_direct"; + std::ofstream ofs(fname); + ofs << "Q_POINTS\n2\nDirect\n0.0 0.0 0.0 1.0\n0.5 0.0 0.0 1.0\n"; + ofs.close(); + + qlist.read_from_file(fname, ucell); + EXPECT_EQ(qlist.get_nq(), 2); + EXPECT_TRUE(qlist.kd_done); + EXPECT_TRUE(qlist.kc_done); + EXPECT_DOUBLE_EQ(qlist.get_q(0).x, 0.0); + EXPECT_DOUBLE_EQ(qlist.get_q(1).x, 0.5); + // weights normalized to sum 1 + EXPECT_NEAR(qlist.wk[0] + qlist.wk[1], 1.0, 1e-10); + // Cartesian = direct * G + ModuleBase::Vector3 qc = qlist.kvec_d[1] * ucell.G; + EXPECT_DOUBLE_EQ(qlist.kvec_c[1].x, qc.x); + + remove(fname); + ClearUcell(); +} + +TEST_F(QListTest, ReadFromFileMonkhorstPack) +{ + construct_ucell(stru_lib[0]); + + const char* fname = "tmp_qpoints_mp"; + std::ofstream ofs(fname); + ofs << "Q_POINTS\n0\nGamma\n2 2 2 0 0 0\n"; + ofs.close(); + + qlist.read_from_file(fname, ucell); + EXPECT_EQ(qlist.get_nq(), 8); // full MP mesh, no symmetry reduction here + EXPECT_TRUE(qlist.is_mp); + double sum = 0.0; + for (int i = 0; i < qlist.get_nq(); ++i) + { + sum += qlist.wk[i]; + } + EXPECT_NEAR(sum, 1.0, 1e-10); + + remove(fname); + ClearUcell(); +} + +TEST_F(QListTest, ReadFromFileLinePath) +{ + construct_ucell(stru_lib[0]); + + const char* fname = "tmp_qpoints_line"; + std::ofstream ofs(fname); + // G -> X segment with 4 points plus the final special point (5 total) + ofs << "Q_POINTS\n2\nLine_Direct\n0.0 0.0 0.0 4\n0.5 0.0 0.0 1\n"; + ofs.close(); + + qlist.read_from_file(fname, ucell); + EXPECT_EQ(qlist.get_nq(), 5); + EXPECT_TRUE(qlist.kd_done); + EXPECT_TRUE(qlist.kc_done); + // segment points + EXPECT_DOUBLE_EQ(qlist.get_q(0).x, 0.0); + EXPECT_DOUBLE_EQ(qlist.get_q(1).x, 0.5 / 4.0); + EXPECT_DOUBLE_EQ(qlist.get_q(2).x, 2.0 * 0.5 / 4.0); + EXPECT_DOUBLE_EQ(qlist.get_q(3).x, 3.0 * 0.5 / 4.0); + EXPECT_DOUBLE_EQ(qlist.get_q(4).x, 0.5); + // line weights are not normalized + EXPECT_DOUBLE_EQ(qlist.wk[0], 1.0); + + remove(fname); + ClearUcell(); +} + +TEST_F(QListTest, ReadFromFileMissing) +{ + // a nonexistent file yields an empty q-point list, not a crash + qlist.read_from_file("nonexistent_qpoints", ucell); + EXPECT_EQ(qlist.get_nq(), 0); +} + +TEST_F(QListTest, ReadFromFileBadHeader) +{ + construct_ucell(stru_lib[0]); + + const char* fname = "tmp_qpoints_bad"; + std::ofstream ofs(fname); + ofs << "not a q-points file\n"; + ofs.close(); + + qlist.read_from_file(fname, ucell); + EXPECT_EQ(qlist.get_nq(), 0); + + remove(fname); + ClearUcell(); +} diff --git a/source/source_cell/test/reciprocal_grid_test.cpp b/source/source_cell/test/reciprocal_grid_test.cpp new file mode 100644 index 00000000000..e3d0be2232e --- /dev/null +++ b/source/source_cell/test/reciprocal_grid_test.cpp @@ -0,0 +1,267 @@ +/** + * @file reciprocal_grid_test.cpp + * @brief Unit tests for ModuleCell::ReciprocalGrid base class. + * + * Covers the spin-free shared functionality: Monkhorst-Pack mesh generation, + * the Monkhorst-Pack coordinate formula, direct/Cartesian conversion, weight + * normalization and the star (IBZ) reduction primitive. + */ +#include "gtest/gtest.h" + +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/magnetism.h" +#include "source_cell/pseudo.h" +#include "source_cell/reciprocal_grid.h" +#include "source_cell/unitcell.h" + +#include +#include + +// Linker stubs: the symmetry library referenced by build_star_ops needs these +// symbols; the real definitions live in the cell_info object library which is +// not linked into this test. Mirror of klist_test.cpp. +pseudo::pseudo() +{ +} +pseudo::~pseudo() +{ +} +Atom::Atom() +{ +} +Atom::~Atom() +{ +} +Atom_pseudo::Atom_pseudo() +{ +} +Atom_pseudo::~Atom_pseudo() +{ +} +SepPot::SepPot() {} +SepPot::~SepPot() {} +UnitCell::UnitCell() +{ +} +UnitCell::~UnitCell() +{ +} +Magnetism::Magnetism() +{ +} +Magnetism::~Magnetism() +{ +} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} + +/** + * @brief Minimal concrete subclass exposing the pure-virtual hook. + */ +class TestGrid : public ModuleCell::ReciprocalGrid +{ + public: + void reduce_by_symmetry(const UnitCell&, + const ModuleSymmetry::Symmetry&, + bool, + std::string&, + bool&) override + { + } +}; + +class ReciprocalGridTest : public testing::Test +{ + protected: + TestGrid grid; +}; + +TEST_F(ReciprocalGridTest, Construct) +{ + EXPECT_EQ(grid.nks, 0); + EXPECT_EQ(grid.nkstot, 0); + EXPECT_EQ(grid.nkstot_full, 0); + EXPECT_FALSE(grid.kc_done); + EXPECT_FALSE(grid.kd_done); + EXPECT_FALSE(grid.is_mp); +} + +TEST_F(ReciprocalGridTest, MPFormula) +{ + // k_type=1 (MP, without Gamma) + EXPECT_DOUBLE_EQ(grid.Monkhorst_Pack_formula(1, 0.0, 1, 4), (0.0 + 2.0 - 4.0 - 1.0) / 8.0); + EXPECT_DOUBLE_EQ(grid.Monkhorst_Pack_formula(1, 0.5, 2, 4), (0.5 + 4.0 - 4.0 - 1.0) / 8.0); + // k_type=0 (Gamma-centered) + EXPECT_DOUBLE_EQ(grid.Monkhorst_Pack_formula(0, 0.0, 1, 4), 0.0); + EXPECT_DOUBLE_EQ(grid.Monkhorst_Pack_formula(0, 0.0, 4, 4), 3.0 / 4.0); +} + +TEST_F(ReciprocalGridTest, MonkhorstPackGeneration) +{ + const int nmp[3] = {2, 3, 4}; + const double offset[3] = {0.0, 0.0, 0.0}; + grid.Monkhorst_Pack(nmp, offset, 0); + + EXPECT_TRUE(grid.is_mp == false); // is_mp is not touched by Monkhorst_Pack + EXPECT_EQ(grid.nkstot, 24); + EXPECT_TRUE(grid.kd_done); + EXPECT_EQ(grid.kvec_d.size(), 24); + EXPECT_EQ(grid.wk.size(), 24); + + const double weight = 1.0 / 24.0; + for (int i = 0; i < grid.nkstot; ++i) + { + EXPECT_DOUBLE_EQ(grid.wk[i], weight); + } + + // Gamma-centered: first point is (0,0,0) + EXPECT_DOUBLE_EQ(grid.kvec_d[0].x, 0.0); + EXPECT_DOUBLE_EQ(grid.kvec_d[0].y, 0.0); + EXPECT_DOUBLE_EQ(grid.kvec_d[0].z, 0.0); + + // Last point (x=2,y=3,z=4): (0.5, 2/3, 0.75) + EXPECT_DOUBLE_EQ(grid.kvec_d[23].x, 0.5); + EXPECT_DOUBLE_EQ(grid.kvec_d[23].y, 2.0 / 3.0); + EXPECT_DOUBLE_EQ(grid.kvec_d[23].z, 0.75); +} + +TEST_F(ReciprocalGridTest, D2CConversion) +{ + const ModuleBase::Matrix3 G(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + grid.kvec_d.resize(1); + grid.kvec_d[0] = ModuleBase::Vector3(0.2, 0.3, 0.4); + grid.kvec_c.resize(1); + + grid.kvec_d2c(G); + EXPECT_DOUBLE_EQ(grid.kvec_c[0].x, 0.2); + EXPECT_DOUBLE_EQ(grid.kvec_c[0].y, 0.3); + EXPECT_DOUBLE_EQ(grid.kvec_c[0].z, 0.4); +} + +TEST_F(ReciprocalGridTest, D2CCleansNumericalNoise) +{ + const ModuleBase::Matrix3 G(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + grid.kvec_d.resize(1); + grid.kvec_d[0] = ModuleBase::Vector3(1.0e-12, 0.5, -1.0e-11); + grid.kvec_c.resize(1); + + grid.kvec_d2c(G); + EXPECT_DOUBLE_EQ(grid.kvec_d[0].x, 0.0); + EXPECT_DOUBLE_EQ(grid.kvec_d[0].z, 0.0); + EXPECT_DOUBLE_EQ(grid.kvec_c[0].x, 0.0); + EXPECT_DOUBLE_EQ(grid.kvec_c[0].z, 0.0); +} + +TEST_F(ReciprocalGridTest, D2CC2DRoundTrip) +{ + // non-trivial lattice + const ModuleBase::Matrix3 G(2.0, 0.0, 1.0, 0.0, 3.0, 0.5, 1.0, 0.0, 4.0); + // kvec_c2d uses R^T; round-trip requires R^T = G^{-1} + const ModuleBase::Matrix3 R = G.Inverse().Transpose(); + + grid.kvec_d.resize(3); + grid.kvec_d[0] = ModuleBase::Vector3(0.1, 0.2, 0.3); + grid.kvec_d[1] = ModuleBase::Vector3(0.5, 0.5, 0.5); + grid.kvec_d[2] = ModuleBase::Vector3(-0.25, 0.75, 0.125); + grid.kvec_c.resize(3); + + const ModuleBase::Vector3 d_ref[3] = {ModuleBase::Vector3(0.1, 0.2, 0.3), + ModuleBase::Vector3(0.5, 0.5, 0.5), + ModuleBase::Vector3(-0.25, 0.75, 0.125)}; + + grid.kvec_d2c(G); + grid.kvec_c2d(R); + + for (int i = 0; i < 3; ++i) + { + EXPECT_NEAR(grid.kvec_d[i].x, d_ref[i].x, 1e-12); + EXPECT_NEAR(grid.kvec_d[i].y, d_ref[i].y, 1e-12); + EXPECT_NEAR(grid.kvec_d[i].z, d_ref[i].z, 1e-12); + } +} + +TEST_F(ReciprocalGridTest, NormalizeWk) +{ + const int nmp[3] = {2, 2, 2}; + const double offset[3] = {0.0, 0.0, 0.0}; + grid.Monkhorst_Pack(nmp, offset, 0); + + grid.normalize_wk(1); + double sum = 0.0; + for (int i = 0; i < grid.nkstot; ++i) + { + sum += grid.wk[i]; + } + EXPECT_NEAR(sum, 1.0, 1e-12); + + grid.normalize_wk(2); + sum = 0.0; + for (int i = 0; i < grid.nkstot; ++i) + { + sum += grid.wk[i]; + } + EXPECT_NEAR(sum, 2.0, 1e-12); +} + +TEST_F(ReciprocalGridTest, ReduceIbzNonMp) +{ + // two points related by inversion: they must fold into one, with the + // combined weight. This exercises the -q (time-reversal) folding path. + const ModuleBase::Matrix3 G(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + const ModuleBase::Matrix3 inv(-1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, -1.0); + const ModuleBase::Matrix3 ind(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + + grid.is_mp = false; + grid.nkstot = 2; + grid.nkstot_full = 2; + grid.kvec_d.resize(2); + grid.kvec_d[0] = ModuleBase::Vector3(0.25, 0.25, 0.25); + grid.kvec_d[1] = ModuleBase::Vector3(-0.25, -0.25, -0.25); + grid.wk.resize(2); + grid.wk[0] = 0.5; + grid.wk[1] = 0.5; + + ModuleBase::Matrix3 ops[2] = {ind, inv}; + std::vector> vec_ibz; + std::vector wk_ibz; + std::vector ibz_index; + std::vector ibz2bz; + grid.reduce_ibz(ops, 2, G, G, nullptr, 1e-6, vec_ibz, wk_ibz, ibz_index, ibz2bz); + + EXPECT_EQ(vec_ibz.size(), 1); + EXPECT_DOUBLE_EQ(wk_ibz[0], 1.0); + EXPECT_EQ(ibz_index[0], 0); + EXPECT_EQ(ibz_index[1], 0); + EXPECT_EQ(ibz2bz[0], 0); +} + +TEST_F(ReciprocalGridTest, ReduceIbzKeepsDistinctPoints) +{ + // two points NOT related by any operation: both survive + const ModuleBase::Matrix3 G(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + const ModuleBase::Matrix3 ind(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + + grid.is_mp = false; + grid.nkstot = 2; + grid.nkstot_full = 2; + grid.kvec_d.resize(2); + grid.kvec_d[0] = ModuleBase::Vector3(0.25, 0.25, 0.25); + grid.kvec_d[1] = ModuleBase::Vector3(0.50, 0.50, 0.50); + grid.wk.resize(2); + grid.wk[0] = 0.5; + grid.wk[1] = 0.5; + + ModuleBase::Matrix3 ops[1] = {ind}; + std::vector> vec_ibz; + std::vector wk_ibz; + std::vector ibz_index; + std::vector ibz2bz; + grid.reduce_ibz(ops, 1, G, G, nullptr, 1e-6, vec_ibz, wk_ibz, ibz_index, ibz2bz); + + EXPECT_EQ(vec_ibz.size(), 2); + EXPECT_DOUBLE_EQ(wk_ibz[0], 0.5); + EXPECT_DOUBLE_EQ(wk_ibz[1], 0.5); + EXPECT_EQ(ibz_index[0], 0); + EXPECT_EQ(ibz_index[1], 1); +} diff --git a/source/source_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index 217a93705b6..96037a25c17 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -8,7 +8,157 @@ #include "esolver_dfpt_pw.h" +#include "source_estate/module_charge/charge.h" +#include "source_estate/module_pot/pot_xc_fdm.h" +#include "source_base/macros.h" +#include "source_base/math_polyint.h" #include "source_base/tool_quit.h" +#include "source_io/module_parameter/parameter.h" +#include "source_pw/module_dfpt/dfpt_pert.h" +#include "source_pw/module_dfpt/dfpt_pw.h" +#include "source_pw/module_dfpt/dfpt_rho.h" + +#include +#include +#include +#include +#include + +namespace { + +/** + * @brief Re/Im-split finite-difference adapter over elecstate::PotXC_FDM. + * + * The q-shifted complex density amplitude is applied twice (real and + * imaginary part around the ground-state density); the two real + * finite-difference responses delta V_xc recombine linearly into the + * complex first-order kernel (exact up to O(|drho|^2)). + */ +class XC_First_Order_FDM : public ModuleDFPT::XC_First_Order +{ + public: + XC_First_Order_FDM(ModulePW::PW_Basis* rho_basis, + const Charge* chg0, + const UnitCell* ucell) + : ucell_(ucell) + { + fdm_ = new elecstate::PotXC_FDM(rho_basis, chg0, ucell); + chg1_ = new Charge(); + chg1_->set_rhopw(rho_basis); + chg1_->allocate(chg0->nspin, false); + veff_1_.create(chg0->nspin, chg0->nrxx); + } + + ~XC_First_Order_FDM() + { + delete fdm_; + delete chg1_; + } + + void apply(const std::vector>& drho_r, + std::vector>& dvxc_r) const override + { + const int nrxx = veff_1_.nc; + if (static_cast(drho_r.size()) != nrxx) + { + ModuleBase::WARNING_QUIT("XC_First_Order_FDM", "drho_r is not on the rho grid"); + } + if (dvxc_r.size() != drho_r.size()) + { + dvxc_r.resize(drho_r.size()); + } + // central difference with a small probe amplitude: a forward + // difference Vxc[rho0 + drho] - Vxc[rho0] carries the curvature + // term ~ Vxc'' * drho^2 / 2, which is quadratic in the T2 response + // and leaks a spurious A1 component into the screened potential + const double eta = 1.0e-6; + std::vector v_plus(nrxx); + // real part: (Vxc[rho0 + eta Re drho] - Vxc[rho0 - eta Re drho]) / 2 + for (int ir = 0; ir < nrxx; ++ir) + { + chg1_->rho[0][ir] = eta * drho_r[ir].real(); + } + veff_1_.zero_out(); + fdm_->cal_v_eff(chg1_, ucell_, veff_1_); + for (int ir = 0; ir < nrxx; ++ir) + { + v_plus[ir] = veff_1_(0, ir); + } + for (int ir = 0; ir < nrxx; ++ir) + { + chg1_->rho[0][ir] = -eta * drho_r[ir].real(); + } + veff_1_.zero_out(); + fdm_->cal_v_eff(chg1_, ucell_, veff_1_); + for (int ir = 0; ir < nrxx; ++ir) + { + dvxc_r[ir] = (v_plus[ir] - veff_1_(0, ir)) / (2.0 * eta); + } + if (getenv("DFPT_XCDBG") != nullptr) + { + static int call = 0; + if (call < 4) + { + double dp_dv = 0.0; + double dp_dp = 0.0; + double dv_dv = 0.0; + for (int ir = 0; ir < nrxx; ++ir) + { + dp_dv += drho_r[ir].real() * dvxc_r[ir].real(); + dp_dp += drho_r[ir].real() * drho_r[ir].real(); + dv_dv += dvxc_r[ir].real() * dvxc_r[ir].real(); + } + int imax = 0; + double dmax = 0.0; + for (int ir = 0; ir < nrxx; ++ir) + { + if (std::abs(drho_r[ir].real()) > dmax) + { + dmax = std::abs(drho_r[ir].real()); + imax = ir; + } + } + std::cout << "XCDBG call=" << call + << " /|drho|^2=" << (dp_dv / dp_dp) + << " |dvxc|/|drho|=" << (std::sqrt(dv_dv) / std::sqrt(dp_dp)) + << " ratio@max=" << (dvxc_r[imax].real() / drho_r[imax].real()) + << " drho@max=" << drho_r[imax].real() + << std::endl; + } + ++call; + } + // imaginary part: same central difference on Im drho + for (int ir = 0; ir < nrxx; ++ir) + { + chg1_->rho[0][ir] = eta * drho_r[ir].imag(); + } + veff_1_.zero_out(); + fdm_->cal_v_eff(chg1_, ucell_, veff_1_); + for (int ir = 0; ir < nrxx; ++ir) + { + v_plus[ir] = veff_1_(0, ir); + } + for (int ir = 0; ir < nrxx; ++ir) + { + chg1_->rho[0][ir] = -eta * drho_r[ir].imag(); + } + veff_1_.zero_out(); + fdm_->cal_v_eff(chg1_, ucell_, veff_1_); + for (int ir = 0; ir < nrxx; ++ir) + { + dvxc_r[ir] += std::complex(0.0, 1.0) + * (v_plus[ir] - veff_1_(0, ir)) / (2.0 * eta); + } + } + + private: + elecstate::PotXC_FDM* fdm_ = nullptr; + Charge* chg1_ = nullptr; + mutable ModuleBase::matrix veff_1_; + const UnitCell* ucell_ = nullptr; +}; + +} // namespace namespace ModuleESolver { @@ -18,7 +168,9 @@ ESolver_DFPT_PW::ESolver_DFPT_PW() this->classname = "ESolver_DFPT_PW"; this->basisname = "PW"; gs_done_ = false; + dfpt_wired_ = false; dfpt_ = nullptr; + xc_adapter_ = nullptr; } ESolver_DFPT_PW::~ESolver_DFPT_PW() @@ -28,6 +180,11 @@ ESolver_DFPT_PW::~ESolver_DFPT_PW() delete dfpt_; dfpt_ = nullptr; } + if (xc_adapter_ != nullptr) + { + delete xc_adapter_; + xc_adapter_ = nullptr; + } } void ESolver_DFPT_PW::before_all_runners(BaseCell& basecell, const Input_para& inp) @@ -39,7 +196,24 @@ void ESolver_DFPT_PW::before_all_runners(BaseCell& basecell, const Input_para& i ESolver_KS_PW, base_device::DEVICE_CPU>::before_all_runners(ucell, inp); - init_dfpt(ucell); + // capture the (possibly autoset) ground-state scalars once; inp aliases + // the global input record and read_pseudo/ParamUpdater have run inside + // the base call + nspin_ = inp.nspin; + nelec_ = inp.nelec; + ecutwfc_ = inp.ecutwfc; + dft_plus_u_ = inp.dft_plus_u; + + // static DFPT configuration from INPUT (explicit passing, rule 1); the + // ground-state data wiring happens in init_dfpt after the SCF converges + dfpt_ = new ModuleDFPT::DFPT_PW(); + dfpt_->set_qmesh(inp.dfpt_qmesh[0], inp.dfpt_qmesh[1], inp.dfpt_qmesh[2]); + dfpt_->set_qfile(inp.dfpt_qfile); + dfpt_->set_conv_thr(inp.dfpt_conv_thr); + dfpt_->set_max_iter(inp.dfpt_max_iter); + dfpt_->set_mix_beta(inp.dfpt_mix_beta); + dfpt_->set_compute_q0(inp.dfpt_compute_q0); + dfpt_->set_loto(inp.dfpt_loto); } void ESolver_DFPT_PW::runner(BaseCell& basecell, const int istep) @@ -57,6 +231,11 @@ void ESolver_DFPT_PW::runner(BaseCell& basecell, const int istep) if (dfpt_ != nullptr) { + if (!dfpt_wired_) + { + init_dfpt(ucell); + dfpt_wired_ = true; + } dfpt_->run(); } @@ -84,21 +263,153 @@ void ESolver_DFPT_PW::init_dfpt(UnitCell& ucell) { ModuleBase::TITLE("ESolver_DFPT_PW", "init_dfpt"); - dfpt_ = new ModuleDFPT::DFPT_PW(); + if (nspin_ != 1) + { + ModuleBase::WARNING_QUIT("ESolver_DFPT_PW::init_dfpt", "DFPT currently supports nspin = 1 only"); + } + if (this->pelec == nullptr || this->pelec->charge == nullptr || this->pelec->pot == nullptr + || this->stp.psi_cpu == nullptr || this->pw_rho == nullptr || this->pw_wfc == nullptr) + { + ModuleBase::WARNING_QUIT("ESolver_DFPT_PW::init_dfpt", "ground state is not ready"); + } + if (this->pelec->charge->nrxx != this->pw_rho->nrxx) + { + ModuleBase::WARNING_QUIT("ESolver_DFPT_PW::init_dfpt", + "charge is not on the rho grid (DFPT supports NCPP)"); + } - // dfpt_->init(ucell, *this->stp.psi, this->pelec->nelec, this->inp_->ecutwfc); + // converged effective potential on the shared rho grid (row 0: nspin = 1) + const ModuleBase::matrix& veff_smooth = this->pelec->pot->get_veff_smooth(); + if (veff_smooth.nc != this->pw_rho->nrxx) + { + ModuleBase::WARNING_QUIT("ESolver_DFPT_PW::init_dfpt", "veff_smooth is not on the rho grid"); + } + std::vector veff_r(veff_smooth.nc, 0.0); + for (int ir = 0; ir < veff_smooth.nc; ++ir) + { + veff_r[ir] = veff_smooth(0, ir); + } - dfpt_->set_parameters("dfpt.in"); + // first-order XC kernel adapter around the converged ground-state density + xc_adapter_ = new XC_First_Order_FDM(this->pw_rho, this->pelec->charge, &ucell); - dfpt_->set_qmesh(1, 1, 1); + if (getenv("DFPT_VKB") != nullptr) + { + // design-phase validation: elementwise comparison of the module's + // NC projectors against the ground-state ppcell vkb at the last k + const int ik = 0; + const int npw = this->pw_wfc->npwk[ik]; + std::vector> gk(npw); + for (int ig = 0; ig < npw; ++ig) + { + gk[ig] = this->pw_wfc->getgpluskcar(ik, ig); + } + ModuleDFPT::DFPT_Pert pert; + pert.init(ucell, this->pw_rho, this->pw_wfc, this->sf); + std::vector>> vkb; + pert.build_vkb(0, 0, gk, vkb); + std::vector>> vkb1; + pert.build_vkb(0, 1, gk, vkb1); + const int nh = ucell.atoms[0].ncpp.nh; + const std::complex* gsvkb = this->ppcell.get_vkb_data(); + std::cout << "VKBCHK npw=" << npw << " nh=" << nh + << " nkb=" << this->ppcell.nkb << std::endl; + for (int mu = 0; mu < nh; ++mu) + { + std::complex dot(0.0, 0.0); + double nrm_mine = 0.0; + double nrm_gs = 0.0; + for (int ig = 0; ig < npw; ++ig) + { + const std::complex gs = gsvkb[mu * this->pw_wfc->npwk_max + ig]; + dot += std::conj(vkb[mu][ig]) * gs; + nrm_mine += std::norm(vkb[mu][ig]); + nrm_gs += std::norm(gs); + } + std::cout << "VKBCHK mu=" << mu << " =" << dot + << " |mine|^2=" << nrm_mine << " |gs|^2=" << nrm_gs << std::endl; + } + for (int mu = 0; mu < nh; ++mu) + { + std::complex dot(0.0, 0.0); + double nrm_mine = 0.0; + double nrm_gs = 0.0; + for (int ig = 0; ig < npw; ++ig) + { + const std::complex gs = gsvkb[(nh + mu) * this->pw_wfc->npwk_max + ig]; + dot += std::conj(vkb1[mu][ig]) * gs; + nrm_mine += std::norm(vkb1[mu][ig]); + nrm_gs += std::norm(gs); + } + std::cout << "VKBCHK a1 mu=" << mu << " =" << dot + << " |mine|^2=" << nrm_mine << " |gs|^2=" << nrm_gs << std::endl; + } + for (int ig = 0; ig < 6; ++ig) + { + std::cout << "VKBEL a1 mu=0 ig=" << ig << " mine=" << vkb1[0][ig] + << " gs=" << gsvkb[4 * this->pw_wfc->npwk_max + ig] + << " gcar=" << gk[ig].x << "," << gk[ig].y << "," << gk[ig].z << std::endl; + } + } - dfpt_->set_conv_thr(1e-8); - dfpt_->set_max_iter(100); + dfpt_->init(ucell, *this->stp.psi_cpu, this->pw_rho, this->pw_wfc, &this->sf, veff_r, + this->pelec->wg, this->pelec->ekb, xc_adapter_, nelec_, ecutwfc_, + dft_plus_u_ ? &this->dftu : nullptr); } void ESolver_DFPT_PW::run_post_process(UnitCell& ucell) { ModuleBase::TITLE("ESolver_DFPT_PW", "run_post_process"); + + if (dfpt_ == nullptr) + { + return; + } + // multi-q frequency report (one block per q of the list, plus the LO-TO + // corrected Gamma block along the data-layer direction when enabled); + // the tensor blocks below stay design-phase std::cout until the io + // layer integration of the data-layer consolidation stage + const int nq = dfpt_->get_nq(); + for (int q_idx = 0; q_idx < nq; ++q_idx) + { + std::cout << dfpt_->format_q_report(q_idx); + if (q_idx == 0) + { + std::cout << dfpt_->format_loto_report(); + } + } + const ModuleBase::matrix& eps = dfpt_->get_dielectric_tensor(); + if (eps.nr == 3 && eps.nc == 3) + { + std::cout << " DFPT dielectric tensor (epsilon_inf):" << std::endl; + for (int a = 0; a < eps.nr; ++a) + { + std::cout << " "; + for (int b = 0; b < eps.nc; ++b) + { + std::cout << eps(a, b) << " "; + } + std::cout << std::endl; + } + } + for (int iat = 0; iat < ucell.nat; ++iat) + { + const ModuleBase::matrix& zstar = dfpt_->get_born_charges(iat); + if (zstar.nr != 3 || zstar.nc != 3) + { + continue; + } + std::cout << " DFPT Born effective charge atom " << iat << ":" << std::endl; + for (int a = 0; a < zstar.nr; ++a) + { + std::cout << " "; + for (int b = 0; b < zstar.nc; ++b) + { + std::cout << zstar(a, b) << " "; + } + std::cout << std::endl; + } + } } } // namespace ModuleESolver diff --git a/source/source_esolver/esolver_dfpt_pw.h b/source/source_esolver/esolver_dfpt_pw.h index f8dfeb25eaf..8a2688dfadf 100644 --- a/source/source_esolver/esolver_dfpt_pw.h +++ b/source/source_esolver/esolver_dfpt_pw.h @@ -12,6 +12,10 @@ #include "esolver_ks_pw.h" #include "source_pw/module_dfpt/dfpt_pw.h" +namespace ModuleDFPT { +class XC_First_Order; +} + namespace ModuleESolver { @@ -28,10 +32,30 @@ class ESolver_DFPT_PW : public ESolver_KS_PW, base_device:: protected: ModuleDFPT::DFPT_PW* dfpt_ = nullptr; + ///< first-order XC kernel adapter over elecstate::PotXC_FDM (C7), + ///< owned here so module_dfpt stays free of pot_xc_fdm.h dependencies + ModuleDFPT::XC_First_Order* xc_adapter_ = nullptr; + bool gs_done_ = false; + bool dfpt_wired_ = false; + + ///< ground-state scalars captured from Input_para in before_all_runners + ///< (rule 1: passed explicitly instead of re-reading the global record + ///< in init_dfpt) + int nspin_ = 1; + + double nelec_ = 0.0; + + double ecutwfc_ = 0.0; + + bool dft_plus_u_ = false; + void run_gs(UnitCell& ucell); + /// wires DFPT_PW with the converged ground state; called after run_gs + /// (the injected veff/XC reference data only exist once the GS SCF is + /// done) void init_dfpt(UnitCell& ucell); void run_post_process(UnitCell& ucell); diff --git a/source/source_esolver/esolver_factory.cpp b/source/source_esolver/esolver_factory.cpp index a597a3cefb8..03e0a9dc739 100644 --- a/source/source_esolver/esolver_factory.cpp +++ b/source/source_esolver/esolver_factory.cpp @@ -2,6 +2,7 @@ #include "esolver.h" #include "esolver_ks_pw.h" +#include "esolver_dfpt_pw.h" #include "esolver_sdft_pw.h" #include "source_base/module_device/device.h" #include "source_hamilt/module_xc/general_exx_info.h" @@ -51,6 +52,10 @@ std::string determine_type(const Input_para& inp) { esolver_type = "ksdft_pw"; } + else if (PARAM.inp.esolver_type == "dfpt") + { + esolver_type = "dfpt_pw"; + } } else if (inp.basis_type == "lcao_in_pw") { @@ -158,6 +163,10 @@ ESolver* init_esolver(const Input_para& inp) return new ESolver_KS_PW, base_device::DEVICE_CPU>(); } } + else if (esolver_type == "dfpt_pw") + { + return new ESolver_DFPT_PW(); + } else if (esolver_type == "sdft_pw") { #if ((defined __CUDA) || (defined __ROCM)) diff --git a/source/source_estate/module_dm/test/CMakeLists.txt b/source/source_estate/module_dm/test/CMakeLists.txt index 14db1ee4f94..37a8a5020c3 100644 --- a/source/source_estate/module_dm/test/CMakeLists.txt +++ b/source/source_estate/module_dm/test/CMakeLists.txt @@ -10,43 +10,59 @@ endif() AddTest( TARGET MODULE_ESTATE_dm_io_test_serial - LIBS parameter base device cell_info + LIBS parameter base device cell_info symmetry SOURCES test_dm_io.cpp ../density_matrix.cpp ../density_matrix_io.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/base_matrix.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/hcontainer.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp ${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp + ${ABACUS_SOURCE_DIR}/source_cell/klist.cpp + ${ABACUS_SOURCE_DIR}/source_cell/parallel_kpoints.cpp + ${ABACUS_SOURCE_DIR}/source_cell/k_vector_utils.cpp + ${ABACUS_SOURCE_DIR}/source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_ESTATE_dm_constructor_test - LIBS parameter base device + LIBS parameter base device symmetry SOURCES test_dm_constructor.cpp ../density_matrix.cpp ../density_matrix_io.cpp tmp_mocks.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/base_matrix.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/hcontainer.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp ${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp + ${ABACUS_SOURCE_DIR}/source_cell/klist.cpp + ${ABACUS_SOURCE_DIR}/source_cell/parallel_kpoints.cpp + ${ABACUS_SOURCE_DIR}/source_cell/k_vector_utils.cpp + ${ABACUS_SOURCE_DIR}/source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_ESTATE_dm_init_test - LIBS parameter base device + LIBS parameter base device symmetry SOURCES test_dm_r_init.cpp ../density_matrix.cpp ../density_matrix_io.cpp tmp_mocks.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/base_matrix.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/hcontainer.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp ${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp + ${ABACUS_SOURCE_DIR}/source_cell/klist.cpp + ${ABACUS_SOURCE_DIR}/source_cell/parallel_kpoints.cpp + ${ABACUS_SOURCE_DIR}/source_cell/k_vector_utils.cpp + ${ABACUS_SOURCE_DIR}/source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_ESTATE_dm_cal_DMR_test - LIBS parameter base device + LIBS parameter base device symmetry SOURCES test_cal_dm_r.cpp ../density_matrix.cpp ../density_matrix_io.cpp tmp_mocks.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/base_matrix.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/hcontainer.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp ${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp + ${ABACUS_SOURCE_DIR}/source_cell/klist.cpp + ${ABACUS_SOURCE_DIR}/source_cell/parallel_kpoints.cpp + ${ABACUS_SOURCE_DIR}/source_cell/k_vector_utils.cpp + ${ABACUS_SOURCE_DIR}/source_cell/reciprocal_grid.cpp ) AddTest( diff --git a/source/source_estate/test/CMakeLists.txt b/source/source_estate/test/CMakeLists.txt index 61b5da1e51b..5e1ad9bb951 100644 --- a/source/source_estate/test/CMakeLists.txt +++ b/source/source_estate/test/CMakeLists.txt @@ -31,20 +31,22 @@ AddTest( AddTest( TARGET MODULE_ESTATE_elecstate_print - LIBS parameter base device + LIBS parameter base device symmetry SOURCES elecstate_print_test.cpp ../elecstate_print.cpp ../occupy.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_ESTATE_elecstate_base - LIBS parameter base device + LIBS parameter base device symmetry SOURCES elecstate_base_test.cpp ../elecstate.cpp ../elecstate_tools.cpp ../occupy.cpp ../../source_psi/psi.cpp ../../source_base/module_fft/fft_bundle.cpp ../../source_base/module_fft/fft_cpu.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_ESTATE_elecstate_pw - LIBS parameter planewave_serial base device + LIBS parameter planewave_serial base device symmetry SOURCES elecstate_pw_test.cpp ../elecstate_pw.cpp ../elecstate_pw_cal_tau.cpp @@ -54,16 +56,18 @@ AddTest( ../../source_lcao/module_deltaspin/spin_constrain.cpp ../../source_psi/psi.cpp ../../source_base/module_device/memory_op.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_ESTATE_elecstate_energy - LIBS parameter base device planewave_serial + LIBS parameter base device planewave_serial symmetry SOURCES elecstate_energy_test.cpp ../elecstate_energy.cpp ../fp_energy.cpp ../makov_payne.cpp ../module_pot/h_hartree_pw.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( diff --git a/source/source_hsolver/test/CMakeLists.txt b/source/source_hsolver/test/CMakeLists.txt index f4b1cf7a204..96625325041 100644 --- a/source/source_hsolver/test/CMakeLists.txt +++ b/source/source_hsolver/test/CMakeLists.txt @@ -75,9 +75,10 @@ if (ENABLE_MPI) AddTest( TARGET MODULE_HSOLVER_pw - LIBS parameter psi device base container + LIBS parameter psi device base container symmetry SOURCES test_hsolver_pw.cpp ../hsolver_pw.cpp ../hsolver_lcaopw.cpp ../diago_bpcg.cpp ../diago_dav_subspace.cpp ../diag_const_nums.cpp ../diago_iter_assist.cpp ../para_lin_tf.cpp - ../../source_estate/elecstate_tools.cpp ../../source_estate/occupy.cpp ../../source_base/module_fft/fft_bundle.cpp ../../source_base/module_fft/fft_cpu.cpp + ../../source_estate/elecstate_tools.cpp ../../source_estate/occupy.cpp ../../source_base/module_fft/fft_bundle.cpp ../../source_base/module_fft/fft_cpu.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( diff --git a/source/source_io/CMakeLists.txt b/source/source_io/CMakeLists.txt index 5ea4b871b13..2b2634f5ee5 100644 --- a/source/source_io/CMakeLists.txt +++ b/source/source_io/CMakeLists.txt @@ -112,6 +112,7 @@ add_library( module_parameter/read_input_item_sdft.cpp module_parameter/read_inp_tddft.cpp module_parameter/read_inp_bse.cpp + module_parameter/read_inp_dfpt.cpp module_parameter/read_inp_deepks.cpp module_parameter/read_inp_model.cpp module_parameter/read_inp_postproc.cpp diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 96189eadf9d..335188bc497 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -400,6 +400,15 @@ struct Input_para int exciton_slice_npoints = 200; ///< grid points per dimension for slice std::vector exciton_slice_range = {-1, 2, -1, 2}; ///< cell range: ustart uend vstart vend + // ============== #Parameters (10b.dfpt) =========================== + std::vector dfpt_qmesh = {1, 1, 1}; ///< Monkhorst-Pack q mesh for DFPT (gamma-centered) + std::string dfpt_qfile = ""; ///< file containing the DFPT q-point list; empty means dfpt_qmesh + bool dfpt_compute_q0 = false; ///< compute epsilon_inf and Born effective charges at q = 0 + bool dfpt_loto = false; ///< apply the LO-TO non-analytic correction at q = 0 + double dfpt_conv_thr = 1.0e-8; ///< convergence threshold of the DFPT first-order density + int dfpt_max_iter = 100; ///< max iterations of the DFPT first-order density mixing + double dfpt_mix_beta = 0.4; ///< mixing coefficient of the DFPT first-order density + // ============== #Parameters (11.Output) =========================== int out_stru = 1; ///< output stru file each ion step ///< 0: no output, 1: STRU format, 2: CIF format diff --git a/source/source_io/module_parameter/read_inp_dfpt.cpp b/source/source_io/module_parameter/read_inp_dfpt.cpp new file mode 100644 index 00000000000..f4eceef9d56 --- /dev/null +++ b/source/source_io/module_parameter/read_inp_dfpt.cpp @@ -0,0 +1,172 @@ +#include "source_base/tool_quit.h" +#include "read_input.h" +#include "read_input_tool.h" + +#include + +namespace ModuleIO +{ +void ReadInput::item_dfpt() +{ + const std::string category = "Density functional perturbation theory"; + { + Input_Item item("dfpt_qmesh"); + item.annotation = "Monkhorst-Pack q mesh for DFPT, should be >= 1, " + "and commensurate with the KPT mesh"; + item.category = category; + item.type = "Vector of Int (1 or 3 values)"; + item.description = "Set the Monkhorst-Pack q mesh (gamma-centered) for DFPT phonon " + "calculations. The q mesh must be commensurate with the ground-state " + "k mesh: k + q must be a point of the k list (modulo a reciprocal " + "lattice vector). For example, a 4x4x4 KPT mesh is commensurate with " + "dfpt_qmesh values of 1, 2, or 4 along each direction. This parameter " + "is ignored when dfpt_qfile is set."; + item.default_value = "1 1 1"; + item.read_value = [](const Input_Item& item, Parameter& para) { + size_t count = item.get_size(); + if (count == 1) + { + para.input.dfpt_qmesh[0] = para.input.dfpt_qmesh[1] = para.input.dfpt_qmesh[2] = intvalue; + } + else if (count == 3) + { + para.input.dfpt_qmesh[0] = std::stoi(item.str_values[0]); + para.input.dfpt_qmesh[1] = std::stoi(item.str_values[1]); + para.input.dfpt_qmesh[2] = std::stoi(item.str_values[2]); + } + else + { + ModuleBase::WARNING_QUIT("ReadInput", "dfpt_qmesh can only accept one or three values."); + } + }; + sync_intvec(input.dfpt_qmesh, 3, 1); + item.check_value = [](const Input_Item& item, const Parameter& para) { + for (int i = 0; i < 3; i++) + { + if (para.input.dfpt_qmesh[i] < 1) + { + ModuleBase::WARNING_QUIT("ReadInput", "dfpt_qmesh must be >= 1."); + } + } + }; + this->add_item(item); + } + { + Input_Item item("dfpt_qfile"); + item.annotation = "file containing the DFPT q points; empty means the " + "dfpt_qmesh Monkhorst-Pack mesh"; + item.category = category; + item.type = "String"; + item.description = "Set the file containing the q points for DFPT, in the same format " + "as the KPT file (Q_POINTS card: Gamma/Monkhorst-Pack mesh, or an " + "explicit Direct/Cartesian list; symmetry reduction is not applied " + "to file q lists). When set, it overrides dfpt_qmesh. Each q point " + "must still be commensurate with the ground-state k mesh."; + item.default_value = "\"\""; + item.read_value = [](const Input_Item& item, Parameter& para) { + if (item.get_size() == 0) + { + para.input.dfpt_qfile = ""; + } + else + { + para.input.dfpt_qfile = strvalue; + } + }; + sync_string(input.dfpt_qfile); + this->add_item(item); + } + { + Input_Item item("dfpt_compute_q0"); + item.annotation = "whether to compute epsilon_inf and Born effective " + "charges at q = 0"; + item.category = category; + item.type = "Boolean"; + item.description = "Whether to compute the macroscopic dielectric tensor " + "(epsilon_inf) and the Born effective charges at q = 0 " + "within the same DFPT run. Requires a q point at Gamma " + "(the default dfpt_qmesh 1 1 1)."; + item.default_value = "false"; + read_sync_bool(input.dfpt_compute_q0); + this->add_item(item); + } + { + Input_Item item("dfpt_loto"); + item.annotation = "whether to apply the LO-TO non-analytic correction " + "at q = 0"; + item.category = category; + item.type = "Boolean"; + item.description = "Whether to apply the Lyddane-Sachs-Teller non-analytic correction " + "to the Gamma-point dynamical matrix, which splits the longitudinal " + "and transverse optical modes. Requires dfpt_compute_q0 to be true, " + "since the correction is built from epsilon_inf and the Born " + "effective charges."; + item.default_value = "false"; + read_sync_bool(input.dfpt_loto); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.dfpt_loto && !para.input.dfpt_compute_q0) + { + ModuleBase::WARNING_QUIT("ReadInput", "dfpt_loto requires dfpt_compute_q0 to be true."); + } + }; + this->add_item(item); + } + { + Input_Item item("dfpt_conv_thr"); + item.annotation = "convergence threshold of the DFPT first-order density"; + item.category = category; + item.type = "Real"; + item.description = "Set the convergence threshold of the self-consistent DFPT cycle: " + "the iteration stops when the relative residual of the first-order " + "density ||drho_out - drho_in|| / ||drho_out|| drops below this " + "value for every displacement."; + item.default_value = "1.0e-8"; + read_sync_double(input.dfpt_conv_thr); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.dfpt_conv_thr <= 0.0) + { + ModuleBase::WARNING_QUIT("ReadInput", "dfpt_conv_thr must be > 0."); + } + }; + this->add_item(item); + } + { + Input_Item item("dfpt_max_iter"); + item.annotation = "max number of the DFPT first-order density mixing iterations"; + item.category = category; + item.type = "Integer"; + item.description = "Set the maximum number of self-consistent DFPT iterations for " + "each atomic displacement."; + item.default_value = "100"; + read_sync_int(input.dfpt_max_iter); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.dfpt_max_iter < 1) + { + ModuleBase::WARNING_QUIT("ReadInput", "dfpt_max_iter must be >= 1."); + } + }; + this->add_item(item); + } + { + Input_Item item("dfpt_mix_beta"); + item.annotation = "mixing coefficient of the DFPT first-order density"; + item.category = category; + item.type = "Real"; + item.description = "Set the plain-mixing coefficient of the first-order density in the " + "self-consistent DFPT cycle. The response Jacobian has strongly " + "negative eigenvalues on the smallest-G shells (Coulomb stiffness), " + "so beta must stay below 2 / (1 + |lambda_min|); the default 0.4 " + "keeps margin up to |lambda_min| ~ 3. A larger value accelerates " + "convergence for weakly screened systems but may diverge."; + item.default_value = "0.4"; + read_sync_double(input.dfpt_mix_beta); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.dfpt_mix_beta <= 0.0 || para.input.dfpt_mix_beta > 1.0) + { + ModuleBase::WARNING_QUIT("ReadInput", "dfpt_mix_beta must be in (0, 1]."); + } + }; + this->add_item(item); + } +} +} // namespace ModuleIO diff --git a/source/source_io/module_parameter/read_inp_sys.cpp b/source/source_io/module_parameter/read_inp_sys.cpp index 73e8c12144d..d8b87b5ebdb 100644 --- a/source/source_io/module_parameter/read_inp_sys.cpp +++ b/source/source_io/module_parameter/read_inp_sys.cpp @@ -138,7 +138,7 @@ void ReadInput::item_system() } { Input_Item item("esolver_type"); - item.annotation = "the energy solver: ksdft, sdft, ofdft, tdofdft, tddft, lj, dp, ks-lr, lr"; + item.annotation = "the energy solver: ksdft, sdft, ofdft, tdofdft, tddft, lj, dp, ks-lr, lr, dfpt"; item.category = "System variables"; item.type = "String"; item.description = R"(Choose the energy solver. @@ -151,11 +151,12 @@ void ReadInput::item_system() * dp: DeeP potential * nep: Neuroevolution Potential * ks-lr: Kohn-Sham density functional theory + LR-TDDFT (Under Development Feature) -* lr: LR-TDDFT with given KS orbitals (Under Development Feature))"; +* lr: LR-TDDFT with given KS orbitals (Under Development Feature) +* dfpt: density functional perturbation theory (Under Development Feature))"; item.default_value = "ksdft"; read_sync_string(input.esolver_type); item.check_value = [](const Input_Item& item, const Parameter& para) { - const std::vector esolver_types = { "ksdft", "sdft", "ofdft", "tdofdft", "tddft", "lj", "dp", "nep", "lr", "ks-lr" }; + const std::vector esolver_types = { "ksdft", "sdft", "ofdft", "tdofdft", "tddft", "lj", "dp", "nep", "lr", "ks-lr", "dfpt" }; if (std::find(esolver_types.begin(), esolver_types.end(), para.input.esolver_type) == esolver_types.end()) { const std::string warningstr = nofound_str(esolver_types, "esolver_type"); diff --git a/source/source_io/module_parameter/read_input.cpp b/source/source_io/module_parameter/read_input.cpp index 0c1c642939d..a3d0359baaa 100644 --- a/source/source_io/module_parameter/read_input.cpp +++ b/source/source_io/module_parameter/read_input.cpp @@ -175,6 +175,7 @@ ReadInput::ReadInput(const int& rank) this->item_tdofdft(); this->item_lr_tddft(); this->item_bse(); + this->item_dfpt(); this->item_output(); this->item_postprocess(); this->item_model(); diff --git a/source/source_io/module_parameter/read_input.h b/source/source_io/module_parameter/read_input.h index bfe9a0edcab..37484a21247 100644 --- a/source/source_io/module_parameter/read_input.h +++ b/source/source_io/module_parameter/read_input.h @@ -131,6 +131,8 @@ class ReadInput void item_lr_tddft(); // items for BSE void item_bse(); + // items for density functional perturbation theory + void item_dfpt(); // items for output void item_output(); // items for postprocess diff --git a/source/source_io/test/CMakeLists.txt b/source/source_io/test/CMakeLists.txt index 4849f0a6621..e6a4eb35373 100644 --- a/source/source_io/test/CMakeLists.txt +++ b/source/source_io/test/CMakeLists.txt @@ -64,7 +64,7 @@ AddTest( TARGET MODULE_IO_write_eig_occ_test LIBS parameter base device symmetry SOURCES write_eig_occ_test.cpp ../module_output/band_parallel_output.cpp ../module_energy/write_eig_occ.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/klist.cpp ../../source_cell/k_vector_utils.cpp - ../../source_cell/cif_io.cpp + ../../source_cell/cif_io.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( @@ -76,13 +76,13 @@ AddTest( AddTest( TARGET MODULE_IO_write_dos_pw LIBS parameter base device symmetry - SOURCES write_dos_pw_test.cpp ../module_dos/cal_dos.cpp ../module_dos/write_dos_pw.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/klist.cpp ../module_energy/nscf_fermi_surf.cpp ../../source_cell/k_vector_utils.cpp + SOURCES write_dos_pw_test.cpp ../module_dos/cal_dos.cpp ../module_dos/write_dos_pw.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/klist.cpp ../module_energy/nscf_fermi_surf.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_IO_print_info LIBS parameter base device symmetry cell_info - SOURCES print_info_test.cpp ../module_output/print_info.cpp ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp + SOURCES print_info_test.cpp ../module_output/print_info.cpp ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( @@ -175,8 +175,9 @@ add_test(NAME MODULE_IO_read_wfc_pw_test_parallel AddTest( TARGET MODULE_IO_read_wf2rho_pw_test - LIBS parameter base device planewave psi + LIBS parameter base device planewave psi symmetry SOURCES read_wf2rho_pw_test.cpp ../module_wf/read_wfc_pw.cpp ../module_wf/read_wf2rho_pw.cpp ../../source_basis/module_pw/test/test_tool.cpp ../../source_estate/module_charge/charge_mpi.cpp ../module_wf/write_wfc_pw.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) add_test(NAME MODULE_IO_read_wf2rho_pw_parallel @@ -229,8 +230,9 @@ add_test(NAME MODULE_IO_orb_io_test_parallel AddTest( TARGET MODULE_IO_write_dmk - LIBS parameter base device cell_info + LIBS parameter base device cell_info symmetry SOURCES ../module_dm/test/write_dmk_test.cpp ../module_dm/write_dmk.cpp ../../source_cell/ucell_io.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) add_test( diff --git a/source/source_io/test_serial/CMakeLists.txt b/source/source_io/test_serial/CMakeLists.txt index 856d63910ed..309fc0c542e 100644 --- a/source/source_io/test_serial/CMakeLists.txt +++ b/source/source_io/test_serial/CMakeLists.txt @@ -14,6 +14,7 @@ add_library( ../module_parameter/read_input_item_sdft.cpp ../module_parameter/read_inp_tddft.cpp ../module_parameter/read_inp_bse.cpp + ../module_parameter/read_inp_dfpt.cpp ../module_parameter/read_inp_deepks.cpp ../module_parameter/read_inp_model.cpp ../module_parameter/read_inp_postproc.cpp @@ -55,8 +56,9 @@ AddTest( AddTest( TARGET MODULE_IO_write_bands - LIBS parameter base device + LIBS parameter base device symmetry SOURCES write_bands_test.cpp ../module_output/band_parallel_output.cpp ../module_energy/write_bands.cpp + ../../source_cell/klist.cpp ../../source_cell/parallel_kpoints.cpp ../../source_cell/k_vector_utils.cpp ../../source_cell/reciprocal_grid.cpp ) AddTest( diff --git a/source/source_lcao/module_deepks/test/CMakeLists.txt b/source/source_lcao/module_deepks/test/CMakeLists.txt index 7e93f9951b3..06ffbd03a97 100644 --- a/source/source_lcao/module_deepks/test/CMakeLists.txt +++ b/source/source_lcao/module_deepks/test/CMakeLists.txt @@ -28,6 +28,7 @@ set(DEEPKS_UNIT_COMMON_SOURCES ../../../source_cell/klist.cpp ../../../source_cell/parallel_kpoints.cpp ../../../source_cell/k_vector_utils.cpp + ../../../source_cell/reciprocal_grid.cpp ../../setup_nonlocal.cpp ../../../source_cell/pseudo.cpp ../../../source_cell/read_pp.cpp diff --git a/source/source_lcao/module_deltaspin/test/CMakeLists.txt b/source/source_lcao/module_deltaspin/test/CMakeLists.txt index 65504369d13..48cea7c4b91 100644 --- a/source/source_lcao/module_deltaspin/test/CMakeLists.txt +++ b/source/source_lcao/module_deltaspin/test/CMakeLists.txt @@ -11,20 +11,28 @@ AddTest( AddTest( TARGET MODULE_LCAO_deltaspin_spin_constrain_test - LIBS base device parameter + LIBS base device parameter symmetry SOURCES spin_constrain_test.cpp ../spin_constrain.cpp ../../../source_basis/module_ao/parallel_orbitals.cpp + ../../../source_cell/klist.cpp + ../../../source_cell/parallel_kpoints.cpp + ../../../source_cell/k_vector_utils.cpp + ../../../source_cell/reciprocal_grid.cpp ) AddTest( TARGET MODULE_LCAO_deltaspin_template_helpers - LIBS base device parameter + LIBS base device parameter symmetry SOURCES template_helpers_test.cpp ../spin_constrain.cpp ../template_helpers.cpp ../lambda_loop_helper.cpp ../basic_funcs.cpp + ../../../source_cell/klist.cpp + ../../../source_cell/parallel_kpoints.cpp + ../../../source_cell/k_vector_utils.cpp + ../../../source_cell/reciprocal_grid.cpp ) AddTest( diff --git a/source/source_lcao/test/CMakeLists.txt b/source/source_lcao/test/CMakeLists.txt index 99ed22aabe7..df31e44ab9b 100644 --- a/source/source_lcao/test/CMakeLists.txt +++ b/source/source_lcao/test/CMakeLists.txt @@ -5,7 +5,7 @@ abacus_disable_feature_definitions(__ROCM) if(ENABLE_LCAO) AddTest( TARGET MODULE_LCAO_init_dm_from_file_test - LIBS parameter base device + LIBS parameter base device symmetry SOURCES test_init_dm_from_file.cpp tmp_mocks.cpp ${ABACUS_SOURCE_DIR}/source_estate/module_dm/density_matrix.cpp ${ABACUS_SOURCE_DIR}/source_estate/module_dm/density_matrix_io.cpp @@ -21,6 +21,10 @@ AddTest( ${ABACUS_SOURCE_DIR}/source_io/module_dm/write_dmr.cpp ${ABACUS_SOURCE_DIR}/source_cell/ucell_io.cpp ${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/output_hcontainer.cpp + ${ABACUS_SOURCE_DIR}/source_cell/klist.cpp + ${ABACUS_SOURCE_DIR}/source_cell/parallel_kpoints.cpp + ${ABACUS_SOURCE_DIR}/source_cell/k_vector_utils.cpp + ${ABACUS_SOURCE_DIR}/source_cell/reciprocal_grid.cpp ) AddTest( diff --git a/source/source_pw/module_dfpt/CMakeLists.txt b/source/source_pw/module_dfpt/CMakeLists.txt index a4b60dfb7a7..55ef7853867 100644 --- a/source/source_pw/module_dfpt/CMakeLists.txt +++ b/source/source_pw/module_dfpt/CMakeLists.txt @@ -3,23 +3,27 @@ set(MODULE_NAME module_dfpt) set(SOURCES dfpt_pw.cpp dfpt_pw_data.cpp + dfpt_kq_basis.cpp dfpt_pert.cpp dfpt_stern.cpp dfpt_rho.cpp dfpt_phon.cpp dfpt_q0.cpp dfpt_metal.cpp + dfpt_hamilt_shift.cpp ) set(HEADERS dfpt_pw.h dfpt_pw_data.h + dfpt_kq_basis.h dfpt_pert.h dfpt_stern.h dfpt_rho.h dfpt_phon.h dfpt_q0.h dfpt_metal.h + dfpt_hamilt_shift.h ) add_library(${MODULE_NAME} ${SOURCES} ${HEADERS}) @@ -36,4 +40,13 @@ target_link_libraries(${MODULE_NAME} psi elecstate module_pwdft -) \ No newline at end of file +) + +if (BUILD_TESTING) + if(ENABLE_MPI) + add_subdirectory(test) + # serial (__MPI-off) tests for the FFT-driving kernels; must be built + # inside ENABLE_MPI so the shared prebuilt test libs stay available + add_subdirectory(test_serial) + endif() +endif() \ No newline at end of file diff --git a/source/source_pw/module_dfpt/README.md b/source/source_pw/module_dfpt/README.md deleted file mode 100644 index c3133c1195e..00000000000 --- a/source/source_pw/module_dfpt/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# DFPT-PW Module - -## Overview - -This module implements Density Functional Perturbation Theory (DFPT) for -plane-wave basis set in ABACUS. It allows calculation of phonon frequencies, -dielectric tensor, Born effective charges, and related properties. - -**Note:** This code is currently in the design phase and has not been -put into production yet. It may change in the future. Please use with caution. - -## Directory Structure - -``` -module_dfpt/ -├── README.md # This file -├── dfpt_pw.h # DFPT-PW main interface class -├── dfpt_pw.cpp # DFPT-PW implementation -├── dfpt_pw_data.h # PW-specific DFPT data container -├── dfpt_pw_data.cpp -├── dfpt_pert.h # Perturbation construction -├── dfpt_pert.cpp -├── dfpt_stern.h # Sternheimer equation solver -├── dfpt_stern.cpp -├── dfpt_rho.h # First-order density handling -├── dfpt_rho.cpp -├── dfpt_phon.h # Phonon/dynamical matrix -├── dfpt_phon.cpp -├── dfpt_q0.h # q=0 special handling -├── dfpt_q0.cpp -├── dfpt_metal.h # Metal system handling -├── dfpt_metal.cpp -└── CMakeLists.txt # Build configuration -``` - -## Design Philosophy - -### 1. Separation of Concerns -- **Data Layer**: `DFPT_PW_Data` stores all DFPT-related data -- **Algorithm Layer**: Individual classes handle specific algorithms -- **Interface Layer**: `DFPT_PW` provides a clean API to ESolver - -### 2. Encapsulation -- All data members in `DFPT_PW_Data` are private -- Access is through getter/setter methods -- Pimpl idiom used to hide implementation details from ESolver - -### 3. Reusability -- Uses existing ABACUS components: Psi, Charge_Mixing, Monkhorst-Pack -- q-point management via `ModuleCell::QList` -- Conjugate gradient via existing HSolver - -### 4. KISS Principle -- Short, descriptive function and variable names -- Minimal dependencies between components -- Clear separation of PW-specific code - -## Module Dependencies - -``` -DFPT_PW - ├── DFPT_PW_Data # Data container - ├── DFPT_Pert # Perturbation construction - ├── DFPT_Stern # Sternheimer solver - ├── DFPT_Rho # Density handling - ├── DFPT_Phon # Phonon calculation - ├── DFPT_Q0 # q=0 special handling - ├── DFPT_Metal # Metal system handling - └── ModuleCell::QList # q-point management -``` - -## Key Features - -1. **Monochromatic Perturbation**: Handles q≠0 perturbations -2. **q=0 Specialization**: Computes dielectric tensor, Born charges, LO-TO splitting -3. **Metal System Support**: Handles smearing, Fermi level correction -4. **Phonon Calculation**: Assembles dynamical matrix, computes frequencies -5. **Symmetry Support**: Uses irreducible representations for efficiency - -## Usage - -```cpp -// In ESolver -ModuleDFPT::DFPT_PW dfpt; -dfpt.init(ucell, psi, nelec, ecutwfc); -dfpt.set_qmesh(4, 4, 4); -dfpt.set_conv_thr(1e-8); -dfpt.run(); - -// Get results -std::vector freq = dfpt.get_phonon_freq(q_idx); -ModuleBase::matrix eps = dfpt.get_dielectric_tensor(); -``` - -## Development Status - -- **Phase**: Design phase -- **Author**: Mohan Chen -- **Date**: 2026-05-18 -- **Status**: Not yet production-ready - -## Future Work - -1. Implement core Sternheimer solver -2. Add proper error handling -3. Complete unit tests -4. Optimize parallelization -5. Add LCAO support (separate module) \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp new file mode 100644 index 00000000000..3321ee541e7 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp @@ -0,0 +1,214 @@ +// ============================================================ +// This code is added by Mohan Chen on 2026-05-18. +// This code is currently in design phase and has not been +// put into production yet. It may change in the future. +// Please use this code with caution. Only developers who know +// what they are doing should use this code. +// ============================================================ + +#include "dfpt_hamilt_shift.h" + +#include "dfpt_pert.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_basis/module_pw/pw_basis_k.h" +#include "source_cell/unitcell.h" + +#include +#include +#include + +namespace ModuleDFPT { + +DFPT_HamiltShift::DFPT_HamiltShift(const UnitCell& ucell, + ModulePW::PW_Basis* pw_rho, + ModulePW::PW_Basis_K* pw_wfc, + const std::vector& veff_r, + const DFPT_Pert* pert) + : ucell_(&ucell), + pw_rho_(pw_rho), + pw_wfc_(pw_wfc), + pert_(pert), + veff_r_(veff_r), + tpiba2_(ucell.tpiba2), + nrxx_(pw_rho != nullptr ? pw_rho->nrxx : 0) { + for (int it = 0; it < ucell_->ntype; ++it) { + const pseudo& ncpp = ucell_->atoms[it].ncpp; + if (ncpp.tvanp || ncpp.has_so) { + ModuleBase::WARNING_QUIT("DFPT_HamiltShift", + "the shifted Sternheimer operator is implemented for " + "normal-conserving separable pseudopotentials only."); + } + // projector -> (radial beta index, m channel) tables, matching + // build_vkb / dVnl_dtau + std::vector ib; + std::vector m; + int mu = 0; + for (int ibeta = 0; ibeta < ncpp.nbeta; ++ibeta) { + const int l = ncpp.lll[ibeta]; + for (int im = 0; im < 2 * l + 1; ++im) { + if (mu < ncpp.nh) { + ib.push_back(ibeta); + m.push_back(im); + } + ++mu; + } + } + mu_ib_.push_back(ib); + mu_m_.push_back(m); + } +} + +DFPT_HamiltShift::~DFPT_HamiltShift() {} + +void DFPT_HamiltShift::set_context(const ModuleBase::Vector3& q_cart, int k_idx) { + kq_.init(pw_wfc_, pw_rho_, q_cart, k_idx); + ik_cache_ = k_idx; + const int npw = kq_.get_npwk(); + + // k+q G index -> charge-grid G index (both bases share the FFT cell) + kq2rho_.assign(npw, -1); + for (int igl = 0; igl < npw; ++igl) { + kq2rho_[igl] = kq_.get_ig_rho(igl); + } + + // cache the beta projectors of every atom on the k+q list + std::vector> gk(npw); + for (int igl = 0; igl < npw; ++igl) { + gk[igl] = kq_.get_gpluskq(igl); + } + vkb_cache_.assign(ucell_->nat, std::vector>>()); + for (int iat = 0; iat < ucell_->nat; ++iat) { + const int it = ucell_->iat2it[iat]; + const int ia = ucell_->iat2ia[iat]; + if (ucell_->atoms[it].ncpp.nh == 0) { + continue; + } + pert_->build_vkb(it, ia, gk, vkb_cache_[iat]); + } + + x_recip_.assign(pw_rho_->npw, std::complex(0.0, 0.0)); + x_r_.assign(nrxx_, std::complex(0.0, 0.0)); +} + +void DFPT_HamiltShift::set_shift(double shift) { + shift_ = shift; +} + +int DFPT_HamiltShift::dimension() const { + return kq_.get_npwk(); +} + +void DFPT_HamiltShift::apply(const std::complex* x, std::complex* y) const { + const int npw = kq_.get_npwk(); + if (npw <= 0 || x == nullptr || y == nullptr) { + return; + } + // kinetic part minus the eigenvalue shift + for (int igl = 0; igl < npw; ++igl) { + y[igl] = (tpiba2_ * kq_.get_gk2(igl) - shift_) * x[igl]; + } + // local effective potential: phase-free FFT convolution on the shared + // grid (the k+q Bloch phases cancel in the product, real_space_dv conv.) + std::fill(x_recip_.begin(), x_recip_.end(), std::complex(0.0, 0.0)); + for (int igl = 0; igl < npw; ++igl) { + if (kq2rho_[igl] >= 0) { + x_recip_[kq2rho_[igl]] = x[igl]; + } + } + pw_rho_->recip2real(x_recip_.data(), x_r_.data()); + for (int ir = 0; ir < nrxx_; ++ir) { + x_r_[ir] *= veff_r_[ir]; + } + pw_rho_->real2recip(x_r_.data(), x_recip_.data()); + for (int igl = 0; igl < npw; ++igl) { + if (kq2rho_[igl] >= 0) { + y[igl] += x_recip_[kq2rho_[igl]]; + } + } + // nonlocal part with the cached k+q projectors + for (int iat = 0; iat < ucell_->nat; ++iat) { + const int it = ucell_->iat2it[iat]; + const int nh = ucell_->atoms[it].ncpp.nh; + if (nh == 0) { + continue; + } + const std::vector>>& vkb = vkb_cache_[iat]; + becp_.assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int igl = 0; igl < npw; ++igl) { + becp_[mu] += std::conj(vkb[mu][igl]) * x[igl]; + } + } + dbecp_.assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int nu = 0; nu < nh; ++nu) { + if (mu_m_[it][mu] != mu_m_[it][nu]) { + continue; + } + dbecp_[mu] += ucell_->atoms[it].ncpp.dion(mu_ib_[it][mu], mu_ib_[it][nu]) * becp_[nu]; + } + } + for (int mu = 0; mu < nh; ++mu) { + for (int igl = 0; igl < npw; ++igl) { + y[igl] += vkb[mu][igl] * dbecp_[mu]; + } + } + } +} + +double DFPT_HamiltShift::debug_t_vnl(const std::vector>& x) const { + const int npw = kq_.get_npwk(); + double ekin = 0.0; + for (int igl = 0; igl < npw; ++igl) { + ekin += tpiba2_ * kq_.get_gk2(igl) * std::norm(x[igl]); + } + double vnl = 0.0; + for (int iat = 0; iat < ucell_->nat; ++iat) { + const int it = ucell_->iat2it[iat]; + const int nh = ucell_->atoms[it].ncpp.nh; + if (nh == 0) { + continue; + } + const std::vector>>& vkb = vkb_cache_[iat]; + becp_.assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int igl = 0; igl < npw; ++igl) { + becp_[mu] += std::conj(vkb[mu][igl]) * x[igl]; + } + } + dbecp_.assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int nu = 0; nu < nh; ++nu) { + if (mu_m_[it][mu] != mu_m_[it][nu]) { + continue; + } + dbecp_[mu] += ucell_->atoms[it].ncpp.dion(mu_ib_[it][mu], mu_ib_[it][nu]) * becp_[nu]; + } + } + for (int mu = 0; mu < nh; ++mu) { + vnl += std::real(std::conj(becp_[mu]) * dbecp_[mu]); + } + } + return ekin + vnl; +} + +double DFPT_HamiltShift::debug_v_wfc(const std::vector>& x) const { + const int npw = kq_.get_npwk(); + std::vector> ur(nrxx_, std::complex(0.0, 0.0)); + pw_wfc_->recip2real(x.data(), ur.data(), ik_cache_); + for (int ir = 0; ir < nrxx_; ++ir) { + ur[ir] *= veff_r_[ir]; + } + std::vector> xg(pw_wfc_->npwk[ik_cache_], std::complex(0.0, 0.0)); + pw_wfc_->real2recip(ur.data(), xg.data(), ik_cache_); + std::complex dot(0.0, 0.0); + const int n = std::min(static_cast(xg.size()), npw); + for (int igl = 0; igl < n; ++igl) { + dot += std::conj(x[igl]) * xg[igl]; + } + return dot.real(); +} + +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_hamilt_shift.h b/source/source_pw/module_dfpt/dfpt_hamilt_shift.h new file mode 100644 index 00000000000..bd5aaa35ea5 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.h @@ -0,0 +1,107 @@ +// ============================================================ +// This code is added by Mohan Chen on 2026-05-18. +// This code is currently in design phase and has not been +// put into production yet. It may change in the future. +// Please use this code with caution. Only developers who know +// what they are doing should use this code. +// ============================================================ + +#ifndef DFPT_HAMILT_SHIFT_H +#define DFPT_HAMILT_SHIFT_H + +#include "dfpt_kq_basis.h" +#include "dfpt_stern.h" +#include "source_base/vector3.h" +#include +#include + +namespace ModulePW { +class PW_Basis; +class PW_Basis_K; +} + +class UnitCell; + +namespace ModuleDFPT { + +class DFPT_Pert; + +/** + * @brief Production adapter of the shifted Sternheimer operator (C7). + * + * Applies y = (H(k+q) - eps_n) x on the k+q plane-wave basis, with the + * ground-state Hamiltonian assembled from module_dfpt primitives instead + * of the GS operator chain (which is index-bound to the GS k list): + * - kinetic tpiba^2 |G+k+q|^2 (diagonal, DFPT_KQ_Basis) + * - local V_eff FFT convolution with the real-space effective + * potential injected by the esolver after the GS SCF + * (veff_smooth row, shared FFT grid) + * - nonlocal V_nl separable projectors from DFPT_Pert::build_vkb at + * the k+q center, cached per (k,q) context + * - shift -eps_n fixed before each solve + * All transforms are phase-free on the shared FFT grid (the same + * convention as DFPT_Pert::real_space_dv), and the k+q scatter/gather + * goes through the (ix,iy,iz) FFT-cell reverse map (C1 finding: the rho + * and wfc stick encodings are not interchangeable). + */ +class DFPT_HamiltShift : public DFPT_Stern::LinearOperator { +public: + DFPT_HamiltShift(const UnitCell& ucell, + ModulePW::PW_Basis* pw_rho, + ModulePW::PW_Basis_K* pw_wfc, + const std::vector& veff_r, + const DFPT_Pert* pert); + ~DFPT_HamiltShift(); + + DFPT_HamiltShift(const DFPT_HamiltShift&) = delete; + DFPT_HamiltShift& operator=(const DFPT_HamiltShift&) = delete; + + /// Fix the (k, q) context and cache the k+q projector set; the + /// eigenvalue shift (Ry) is set per solve through set_shift. + void set_context(const ModuleBase::Vector3& q_cart, int k_idx); + void set_shift(double shift); + int dimension() const override; + + void apply(const std::complex* x, std::complex* y) const override; + + /// debug: without the veff convolution (design-phase + /// validation diagnostics) + double debug_t_vnl(const std::vector>& x) const; + + /// debug: through the ground-state wfc-basis k-indexed FFT + /// path (validation of the rho-grid scatter/gather convolution) + double debug_v_wfc(const std::vector>& x) const; + +private: + const UnitCell* ucell_ = nullptr; + ModulePW::PW_Basis* pw_rho_ = nullptr; + ModulePW::PW_Basis_K* pw_wfc_ = nullptr; + const DFPT_Pert* pert_ = nullptr; + std::vector veff_r_; + double tpiba2_ = 0.0; + int nrxx_ = 0; + + DFPT_KQ_Basis kq_; + double shift_ = 0.0; + ///< k+q list index -> rho-grid ig (-1 when the cell position carries no + /// rho G; cannot happen with ecutrho >= 4 ecutwfc but kept defensive) + std::vector kq2rho_; + + ///< projector bookkeeping per type (mu -> (beta index, m channel)) + std::vector> mu_ib_; + std::vector> mu_m_; + ///< cached beta projectors per atom on the k+q list + std::vector>>> vkb_cache_; + + ///< apply-time scratch (apply is const) + mutable std::vector> x_recip_; + mutable std::vector> x_r_; + mutable std::vector> becp_; + mutable std::vector> dbecp_; + + int ik_cache_ = 0; +}; + +} // namespace ModuleDFPT + +#endif // DFPT_HAMILT_SHIFT_H diff --git a/source/source_pw/module_dfpt/dfpt_kq_basis.cpp b/source/source_pw/module_dfpt/dfpt_kq_basis.cpp new file mode 100644 index 00000000000..143c813afc9 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_kq_basis.cpp @@ -0,0 +1,152 @@ +// ============================================================ +// This code is added by Mohan Chen on 2026-05-18. +// This code is currently in design phase and has not been +// put into production yet. +// It may change in the future. +// Please use this code with caution. +// Only developers who know +// what they are doing should use this code. +// ============================================================ + +#include "dfpt_kq_basis.h" + +#include "source_base/global_function.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_basis/module_pw/pw_basis_k.h" + +#include + +namespace ModuleDFPT { + +DFPT_KQ_Basis::DFPT_KQ_Basis() {} + +DFPT_KQ_Basis::~DFPT_KQ_Basis() {} + +void DFPT_KQ_Basis::init(const ModulePW::PW_Basis_K* pw_wfc, + const ModulePW::PW_Basis* pw_rho, + const ModuleBase::Vector3& q_cart, + int ik) +{ + pw_wfc_ = pw_wfc; + npwk_ = 0; + ig_rho_.clear(); + gk2_.clear(); + gcar_.clear(); + + if (pw_wfc_ == nullptr || pw_rho == nullptr) + { + return; + } + + // DFPT couples k and k+q symmetrically; the perturbation wavevector is + // generally incommensurate with the gamma-ladder, so the ground-state + // basis must be a full complex basis (see class documentation). + if (pw_wfc_->gamma_only) + { + ModuleBase::WARNING_QUIT("DFPT_KQ_Basis", + "DFPT requires a complex (gamma_only=false) wavefunction basis for k+q. " + "Please disable gamma_only for the wavefunction basis used by DFPT."); + } + + // the two bases exchange G vectors through the shared FFT cell position + if (pw_wfc_->nx != pw_rho->nx || pw_wfc_->ny != pw_rho->ny + || pw_wfc_->nz != pw_rho->nz) + { + ModuleBase::WARNING_QUIT("DFPT_KQ_Basis", + "DFPT requires the wavefunction and charge FFT grids to share " + "their dimensions."); + } + + const ModuleBase::Vector3 k_c = pw_wfc_->kvec_c[ik]; + kplusq_c_ = k_c + q_cart; + + // rho-grid reverse map of the shared FFT cell, used to attach the + // charge-basis index of every enumerated k+q plane wave + std::vector ig_of_cell(pw_rho->nxyz, -1); + for (int ig = 0; ig < pw_rho->npw; ++ig) + { + const int isz = pw_rho->ig2isz[ig]; + const int iz = isz % pw_rho->nz; + const int is = isz / pw_rho->nz; + const int ixy = pw_rho->is2fftixy[is]; + const int ix = ixy / pw_rho->fftny; + const int iy = ixy % pw_rho->fftny; + ig_of_cell[(ix * pw_rho->ny + iy) * pw_rho->nz + iz] = ig; + } + + std::set taken; + auto try_push = [&](const int ix_in, const int iy_in, const int iz_in, + const ModuleBase::Matrix3& gbase, + const int ig_rho_hint) { + int ix = ix_in; + int iy = iy_in; + int iz = iz_in; + if (ix >= static_cast(pw_wfc_->nx / 2) + 1) + { + ix -= pw_wfc_->nx; + } + if (iy >= static_cast(pw_wfc_->ny / 2) + 1) + { + iy -= pw_wfc_->ny; + } + if (iz >= static_cast(pw_wfc_->nz / 2) + 1) + { + iz -= pw_wfc_->nz; + } + const ModuleBase::Vector3 gcar + = ModuleBase::Vector3(ix, iy, iz) * gbase; + const ModuleBase::Vector3 gpluskq = gcar + kplusq_c_; + const double gk2 = gpluskq * gpluskq; + if (gk2 > pw_wfc_->gk_ecut) + { + return; + } + const int cell = ((ix_in * pw_wfc_->ny) + iy_in) * pw_wfc_->nz + iz_in; + if (!taken.insert(cell).second) + { + return; + } + gcar_.push_back(gcar); + gk2_.push_back(gk2); + ig_rho_.push_back(ig_rho_hint >= 0 ? ig_rho_hint : ig_of_cell[cell]); + }; + + // first reservoir: the wavefunction G grid (covers the ground-state + // k-mesh balls) + for (int ig = 0; ig < pw_wfc_->npw; ++ig) + { + const int isz = pw_wfc_->ig2isz[ig]; + const int iz = isz % pw_wfc_->nz; + const int is = isz / pw_wfc_->nz; + const int ixy = pw_wfc_->is2fftixy[is]; + const int ix = ixy / pw_wfc_->fftny; + const int iy = ixy % pw_wfc_->fftny; + try_push(ix, iy, iz, pw_wfc_->G, -1); + } + // completing reservoir: the charge G grid; its ball radius + // (2*sqrt(ecutwfc) by construction) covers every q-shifted ball for q + // inside the first Brillouin zone, which the wavefunction grid does not + for (int ig = 0; ig < pw_rho->npw; ++ig) + { + const int isz = pw_rho->ig2isz[ig]; + const int iz = isz % pw_rho->nz; + const int is = isz / pw_rho->nz; + const int ixy = pw_rho->is2fftixy[is]; + const int ix = ixy / pw_rho->fftny; + const int iy = ixy % pw_rho->fftny; + try_push(ix, iy, iz, pw_rho->G, ig); + } + npwk_ = static_cast(gcar_.size()); +} + +void DFPT_KQ_Basis::clear() +{ + pw_wfc_ = nullptr; + kplusq_c_ = ModuleBase::Vector3(); + npwk_ = 0; + ig_rho_.clear(); + gk2_.clear(); + gcar_.clear(); +} + +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_kq_basis.h b/source/source_pw/module_dfpt/dfpt_kq_basis.h new file mode 100644 index 00000000000..7e26307c742 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_kq_basis.h @@ -0,0 +1,94 @@ +// ============================================================ +// This code is added by Mohan Chen on 2026-05-18. +// This code is currently in design phase and has not been +// put into production yet. +// It may change in the future. +// Please use this code with caution. +// Only developers who know +// what they are doing should use this code. +// ============================================================ + +#ifndef DFPT_KQ_BASIS_H +#define DFPT_KQ_BASIS_H + +#include "source_base/vector3.h" +#include + +namespace ModulePW { +class PW_Basis; +class PW_Basis_K; +} + +namespace ModuleDFPT { + +/** + * @brief Plane-wave basis at the perturbation wavevector k+q. + * + * C0: for every (ik, q) pair the first-order response (the Sternheimer + * solution dpsi) lives in the k+q plane-wave basis. Rather than building a + * full PW_Basis_K (new FFT grids, MP redistribution) per q, this helper + * enumerates the G vectors of two already-initialized ground-state bases at + * the shifted center k+q: every G with |G + (k+q)|^2 <= gk_ecut is kept. + * + * Two reservoirs are needed because the wavefunction G grid is distributed + * with the ball radius (sqrt(gk_ecut) + max|k_mesh|)^2, which does not cover + * the q-shifted balls (the k-mesh knows nothing about q). The dense charge + * grid, built with ecutrho >= 4*ecutwfc, has radius 2*sqrt(gk_ecut) and + * covers sqrt(gk_ecut) + |k+q| for every q inside the first Brillouin + * zone, so it completes the enumeration. + * + * Preconditions: + * - The ground-state k-basis must be complex (gamma_only=false): DFPT + * couples k and k+q symmetrically and the q-perturbation breaks the + * gamma-only half-space reduction. + * - Both bases must share the same FFT grid dimensions (the k+q G vectors + * are exchanged between them through the shared FFT cell position). + */ +class DFPT_KQ_Basis { +public: + DFPT_KQ_Basis(); + ~DFPT_KQ_Basis(); + + /** + * @brief Enumerate the local (per-processor) k+q plane-wave basis. + * @param pw_wfc ground-state k-dependent plane-wave basis (complex) + * @param pw_rho ground-state charge-density plane-wave basis + * @param q_cart perturbation wavevector in Cartesian coordinates + * @param ik index of the ground-state k point + */ + void init(const ModulePW::PW_Basis_K* pw_wfc, + const ModulePW::PW_Basis* pw_rho, + const ModuleBase::Vector3& q_cart, + int ik); + + void clear(); + + bool is_valid() const { return pw_wfc_ != nullptr; } + ///< number of k+q plane waves on this processor + int get_npwk() const { return npwk_; } + ///< index of the G vector in the charge-density basis (-1 if the shared + ///< FFT cell position carries no local rho-grid G) + int get_ig_rho(int igl) const { return ig_rho_[igl]; } + ///< G in Cartesian coordinates + ModuleBase::Vector3 get_gcar(int igl) const { return gcar_[igl]; } + ///< G + (k+q) in Cartesian coordinates + ModuleBase::Vector3 get_gpluskq(int igl) const { return gcar_[igl] + kplusq_c_; } + ///< |G + (k+q)|^2 in units of 1/lat0^2 + double get_gk2(int igl) const { return gk2_[igl]; } + ///< k+q wavevector in Cartesian coordinates + ModuleBase::Vector3 get_kplusq() const { return kplusq_c_; } + const std::vector& get_gk2_all() const { return gk2_; } + const std::vector>& get_gcar_all() const { return gcar_; } + +private: + const ModulePW::PW_Basis_K* pw_wfc_ = nullptr; ///< ground-state k-basis + ModuleBase::Vector3 kplusq_c_; ///< k+q in Cartesian coordinates + int npwk_ = 0; ///< number of k+q plane waves + std::vector ig_rho_; ///< k+q index -> charge-grid G index + std::vector gk2_; ///< |G + (k+q)|^2 + std::vector> gcar_;///< G in Cartesian coordinates +}; + +} // namespace ModuleDFPT + +#endif // DFPT_KQ_BASIS_H diff --git a/source/source_pw/module_dfpt/dfpt_metal.cpp b/source/source_pw/module_dfpt/dfpt_metal.cpp index 3131a5186d2..73b6652b5aa 100644 --- a/source/source_pw/module_dfpt/dfpt_metal.cpp +++ b/source/source_pw/module_dfpt/dfpt_metal.cpp @@ -7,6 +7,7 @@ // ============================================================ #include "dfpt_metal.h" +#include "source_base/tool_quit.h" namespace ModuleDFPT { @@ -21,9 +22,17 @@ void DFPT_Metal::init(double sigma, const std::string& smearing_type) { void DFPT_Metal::dfdeps(const ModuleBase::matrix& eig, double efermi, ModuleBase::matrix& dfdeps) { + // C4 interface reservation: the metallic DFPT branch (smearing + // derivatives, Fermi-level shift dmu and the occupation-response part of + // the first-order density) is intentionally NOT implemented in this + // design-phase iteration; insulating systems only. The data members + // (sigma_, smearing_type_) and the is_metal_/dmu_ slots of DFPT_PW_Data + // are already in place for the future implementation. (void)eig; (void)efermi; (void)dfdeps; + ModuleBase::WARNING_QUIT("DFPT_Metal", + "metallic DFPT (dfdeps) is not supported in the design phase"); } void DFPT_Metal::compute_dmu(int q_idx, const psi::Psi>& psi, @@ -34,17 +43,21 @@ void DFPT_Metal::compute_dmu(int q_idx, const psi::Psi>& ps (void)wg; (void)dfdeps; (void)data; + ModuleBase::WARNING_QUIT("DFPT_Metal", + "metallic DFPT (compute_dmu) is not supported in the design phase"); } void DFPT_Metal::compute_drho_metal(int q_idx, const psi::Psi>& psi, - const ModuleBase::matrix& wg, const ModuleBase::matrix& dfdeps, - double dmu, DFPT_PW_Data& data) { + const ModuleBase::matrix& wg, const ModuleBase::matrix& dfdeps, + double dmu, DFPT_PW_Data& data) { (void)q_idx; (void)psi; (void)wg; (void)dfdeps; (void)dmu; (void)data; + ModuleBase::WARNING_QUIT("DFPT_Metal", + "metallic DFPT (compute_drho_metal) is not supported in the design phase"); } double DFPT_Metal::fd_dfdeps(double e, double efermi) { @@ -59,4 +72,4 @@ double DFPT_Metal::gauss_dfdeps(double e, double efermi) { return 0.0; } -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index 4c086cbf5c3..dbd58c8491c 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -8,6 +8,20 @@ #include "dfpt_pert.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" +#include "source_base/math_integral.h" +#include "source_base/math_sphbes.h" +#include "source_base/truncated_func.h" +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_pw/module_pwdft/stru_fac.h" + +#include +#include +#include +#include + namespace ModuleDFPT { DFPT_Pert::DFPT_Pert() {} @@ -22,43 +36,769 @@ void DFPT_Pert::init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, sf_ = &sf; } +void DFPT_Pert::atom_index(int atom_idx, int& it, int& ia) const { + it = 0; + ia = atom_idx; + for (int it_type = 0; it_type < ucell_->ntype; ++it_type) { + if (ia < ucell_->atoms[it_type].na) { + it = it_type; + return; + } + ia -= ucell_->atoms[it_type].na; + } + // out of range: leave it/ia at the last type / last picture and let the + // caller guard; dV requests with invalid indices simply produce nothing. + ia = -1; +} + +void DFPT_Pert::rho_gvec(int ig, ModuleBase::Vector3& gcar) const { + const int isz = pw_rho_->ig2isz[ig]; + int iz = isz % pw_rho_->nz; + const int is = isz / pw_rho_->nz; + const int ixy = pw_rho_->is2fftixy[is]; + int ix = ixy / pw_rho_->fftny; + int iy = ixy % pw_rho_->fftny; + if (ix >= int(pw_rho_->nx / 2) + 1) { ix -= pw_rho_->nx; } + if (iy >= int(pw_rho_->ny / 2) + 1) { iy -= pw_rho_->ny; } + if (iz >= int(pw_rho_->nz / 2) + 1) { iz -= pw_rho_->nz; } + gcar = ModuleBase::Vector3(ix, iy, iz) * ucell_->G; +} + +double DFPT_Pert::vloc_at_g(int it, double g2) const { + // g2 is the squared magnitude in bohr^-2 units. + const Atom* atom = &ucell_->atoms[it]; + const double zv = atom->ncpp.zv; + if (atom->coulomb_potential) { + // analytic Coulomb local potential (vl_pw.cpp::vloc_coulomb) + return -zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_->omega / g2; + } + // numeric pseudopotential: mirror vl_pw.cpp::vloc_of_g at the requested + // magnitude instead of interpolating the precomputed shell table. This + // keeps the rho-grid kernel consistent with the ground-state local + // potential for every magnitude |Delta+q|. + const int msh = atom->ncpp.msh; + const double fac = zv * ModuleBase::e2; + std::vector aux(msh); + const double g = std::sqrt(g2); + if (g < 1.0e-8) { + double v0 = 0.0; + for (int ir = 0; ir < msh; ++ir) { + aux[ir] = atom->ncpp.r[ir] * (atom->ncpp.r[ir] * atom->ncpp.vloc_at[ir] + fac); + } + ModuleBase::Integral::Simpson_Integral(msh, aux.data(), atom->ncpp.rab.data(), v0); + return v0 * ModuleBase::FOUR_PI / ucell_->omega; + } + for (int ir = 0; ir < msh; ++ir) { + aux[ir] = (atom->ncpp.r[ir] * atom->ncpp.vloc_at[ir] + fac * std::erf(atom->ncpp.r[ir])) + * std::sin(g * atom->ncpp.r[ir]) / g; + } + double v = 0.0; + ModuleBase::Integral::Simpson_Integral(msh, aux.data(), atom->ncpp.rab.data(), v); + // erf(r)-compensating gaussian subtraction (same as vloc_of_g) + v -= fac * ModuleBase::truncated_exp(-g2 * 0.25) / g2; + return v * ModuleBase::FOUR_PI / ucell_->omega; +} + +void DFPT_Pert::dVloc_dtau(int atom_idx, int dir, + const ModuleBase::Vector3& q, + std::vector>& dv) { + if (pw_rho_ == nullptr || pw_rho_->gamma_only) { + ModuleBase::WARNING_QUIT("DFPT_Pert::dVloc_dtau", + "DFPT requires a complex (gamma_only=false) real-space basis."); + } + int it = 0; + int ia = 0; + atom_index(atom_idx, it, ia); + if (ia < 0) { + return; + } + const ModuleBase::Vector3& tau = ucell_->atoms[it].tau[ia]; + const int npw = pw_rho_->npw; + dv.assign(npw, std::complex(0.0, 0.0)); + ModuleBase::Vector3 gcar; + for (int ig = 0; ig < npw; ++ig) { + rho_gvec(ig, gcar); + const ModuleBase::Vector3 w = gcar + q; // Delta + q, 2*pi/lat0 units + const double w2 = w * w; + // the Delta == -q component carries no displacement gradient (constant + // potential shift) and is dropped, consistently with the G=0 handling + // of the ground-state local potential. + if (w2 < 1.0e-12) { + continue; + } + const double g_bohr2 = w2 * ucell_->tpiba2; + const double vloc = vloc_at_g(it, g_bohr2); + // GS structure-factor convention (stru_fac.cpp: ci_tpi = + // NEG_IMAG_UNIT * 2pi): exp(-i 2pi (g.tau)), with g in 1/lat0 units + // and tau in lat0 units; 2pi/lat0 = tpiba only multiplies the + // magnitude (vl_pw.cpp: qnorm = |g| * tpiba). + const double arg = -ModuleBase::TWO_PI * (w * tau); + const std::complex phase(std::cos(arg), std::sin(arg)); + // dV_loc / d tau_direction = -i g_dir * Vloc * exp(-i (Delta+q).tau) + const std::complex iw_dir = + std::complex(0.0, -1.0) * (ucell_->tpiba * w[dir]); + dv[ig] = iw_dir * vloc * phase; + } +} + void DFPT_Pert::build_dv(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) { + // the local first-order potential is assembled on the rho grid in reciprocal + // space (reciprocal coefficients indexed by the rho-basis ig), then brought + // to the shared real-space grid where apply_dv performs the convolution. + if (pw_rho_ == nullptr) { + return; + } + const ModuleBase::Vector3 q_cart = data.get_qvec(q_idx) * ucell_->G; + std::vector> dv_recip; + dVloc_dtau(atom_idx, dir, q_cart, dv_recip); + data.set_dv_recip_c(q_idx, 0, dv_recip); + + std::vector> dv_real(pw_rho_->nrxx); + pw_rho_->recip2real(dv_recip.data(), dv_real.data()); + data.set_dv_rc(q_idx, 0, dv_real); + data.set_pert_atom(atom_idx); + data.set_pert_dir(dir); + + // DFT+U perturbation reservation (U0): append the first-order Hubbard + // potential dV_U when a DFT+U provider is wired. Physical implementation + // lands in C1 (frozen projector term) and C3 (occupation response). + if (data.with_u()) { + build_dv_u(q_idx, atom_idx, dir, data); + } +} + +void DFPT_Pert::real_space_dv(int q_idx, int k_idx, + const psi::Psi>& psi, + DFPT_PW_Data& data, + const DFPT_KQ_Basis& kq, + std::vector>>& dv_psi) const { + const std::vector> dv_rc = data.get_dv_rc(q_idx, 0); + if (dv_rc.empty() || dv_rc.size() != static_cast(pw_rho_->nrxx)) { + return; + } + apply_vr_core(k_idx, dv_rc, psi, kq, dv_psi); +} + +void DFPT_Pert::apply_vr(int q_idx, int k_idx, + const std::vector>& v_rc, + const psi::Psi>& psi, + const ModuleBase::Vector3& q_cart, + std::vector>>& dv_psi) const { (void)q_idx; - (void)atom_idx; - (void)dir; - (void)data; + if (pw_rho_ == nullptr || pw_wfc_ == nullptr + || v_rc.size() != static_cast(pw_rho_->nrxx)) { + dv_psi.clear(); + return; + } + DFPT_KQ_Basis kq; + kq.init(pw_wfc_, pw_rho_, q_cart, k_idx); + apply_vr_core(k_idx, v_rc, psi, kq, dv_psi); +} + +void DFPT_Pert::apply_vr_core(int k_idx, + const std::vector>& v_rc, + const psi::Psi>& psi, + const DFPT_KQ_Basis& kq, + std::vector>>& dv_psi) const { + const int nbands = psi.get_nbands(); + const int npwk_kq = kq.get_npwk(); + std::vector> u_r(pw_rho_->nrxx); + std::vector> d_r(pw_rho_->nrxx); + std::vector> d_recip(pw_rho_->npw); + dv_psi.assign(nbands, std::vector>(npwk_kq, std::complex(0.0, 0.0))); + for (int iband = 0; iband < nbands; ++iband) { + pw_wfc_->recip2real(&psi(k_idx, iband, 0), u_r.data(), k_idx); + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + d_r[ir] = u_r[ir] * v_rc[ir]; + } + pw_rho_->real2recip(d_r.data(), d_recip.data()); + std::vector> dpsi(npwk_kq, std::complex(0.0, 0.0)); + for (int igl = 0; igl < npwk_kq; ++igl) { + const int ig_rho = kq.get_ig_rho(igl); + if (ig_rho >= 0) { + dpsi[igl] = d_recip[ig_rho]; + } + } + dv_psi[iband] = dpsi; + } } void DFPT_Pert::apply_dv(int q_idx, int k_idx, const psi::Psi>& psi, DFPT_PW_Data& data) { - (void)q_idx; - (void)k_idx; - (void)psi; - (void)data; + const int atom_idx = data.get_pert_atom(); + const int dir = data.get_pert_dir(); + const ModuleBase::Vector3 q_cart = data.get_qvec(q_idx) * ucell_->G; + + DFPT_KQ_Basis kq; + kq.init(pw_wfc_, pw_rho_, q_cart, k_idx); + + const int nbands = psi.get_nbands(); + std::vector>> dv_psi(nbands); + + // local contribution: dVloc(r) * psi on the shared real-space grid + real_space_dv(q_idx, k_idx, psi, data, kq, dv_psi); + + // nonlocal contribution: dVnl/dtau |psi> (per displaced atom) + std::vector>> dv_psi_nl; + dVnl_dtau(atom_idx, dir, q_cart, psi, k_idx, dv_psi_nl); + if (dv_psi_nl.size() == static_cast(nbands)) { + for (int iband = 0; iband < nbands; ++iband) { + if (dv_psi[iband].size() != dv_psi_nl[iband].size()) { + continue; + } + for (size_t i = 0; i < dv_psi[iband].size(); ++i) { + dv_psi[iband][i] += dv_psi_nl[iband][i]; + } + } + } + + for (int iband = 0; iband < nbands; ++iband) { + data.set_dpsi(q_idx, k_idx, iband, dv_psi[iband]); + } } -void DFPT_Pert::build_efield(const ModuleBase::Vector3& field, DFPT_PW_Data& data) { - (void)field; - (void)data; +// --------------------------------------------------------------------------- +// nonlocal first-order potential (normal-conserving separable case) +// --------------------------------------------------------------------------- + +double DFPT_Pert::radial_vq(int it, int ib, double g) const { + const pseudo& ncpp = ucell_->atoms[it].ncpp; + const int l = ncpp.lll[ib]; + int kkbeta = ncpp.kkbeta; + if (kkbeta > 0 && (kkbeta % 2 == 0)) { + --kkbeta; + } + std::vector jl(kkbeta); + std::vector aux(kkbeta); + ModuleBase::Sphbes::Spherical_Bessel(kkbeta, ncpp.r.data(), g, l, jl.data()); + for (int ir = 0; ir < kkbeta; ++ir) { + aux[ir] = ncpp.betar(ib, ir) * jl[ir] * ncpp.r[ir]; + } + double v = 0.0; + ModuleBase::Integral::Simpson_Integral(kkbeta, aux.data(), ncpp.rab.data(), v); + // tab convention from vnl_pw.cpp: (4pi/sqrt(Omega)) * integral + return v * ModuleBase::FOUR_PI / std::sqrt(ucell_->omega); } -void DFPT_Pert::dVloc_dtau(int atom_idx, int dir, const ModuleBase::Vector3& q, - std::vector>& dv) { - (void)atom_idx; - (void)dir; - (void)q; - (void)dv; +double DFPT_Pert::real_ylm(int l, int m, const ModuleBase::Vector3& ghat) const { + // orthonormal real spherical harmonics Y_{l,m} for l <= 2 with the + // standard convention, m in [-l, l]: + // Y_{l,0} = sqrt((2l+1)/4pi) P_l^0(cos0) + // Y_{l,m>0} = sqrt(2 (2l+1)/4pi (l-m)!/(l+m)!) P_l^m(cos0) cos(m phi) + // Y_{l,m<0} = sqrt(2 (2l+1)/4pi (l-|m|)!/(l+|m|)!) P_l^{|m|}(cos0) sin(|m| phi) + // with the associated Legendre convention P_1^1 = -sin0, P_2^1 = -3 sin0 cos0, + // P_2^2 = 3 sin^2 0. The ABACUS GS vkb applies an additional (-1)^|m| phase + // for the m>0 channels; exact GS parity is reconciled in the diamond + // end-to-end test (C7), while the C1 identity test is convention-independent. + const double x = ghat.x; + const double y = ghat.y; + const double z = ghat.z; + const double r = std::sqrt(x * x + y * y + z * z); + if (r < 1.0e-12) { + return (l == 0) ? 0.5 * std::sqrt(1.0 / ModuleBase::PI) : 0.0; + } + const double nx = x / r; + const double ny = y / r; + const double nz = z / r; + switch (l) { + case 0: { + return 0.5 * std::sqrt(1.0 / ModuleBase::PI); + } + case 1: { + switch (m) { + case -1: return -0.5 * std::sqrt(3.0 / ModuleBase::PI) * ny; + case 0: return 0.5 * std::sqrt(3.0 / ModuleBase::PI) * nz; + case 1: return -0.5 * std::sqrt(3.0 / ModuleBase::PI) * nx; + } + break; + } + case 2: { + switch (m) { + case -2: return 0.5 * std::sqrt(15.0 / ModuleBase::PI) * nx * ny; + case -1: return -0.5 * std::sqrt(15.0 / ModuleBase::PI) * nz * ny; + case 0: return 0.25 * std::sqrt(5.0 / ModuleBase::PI) * (3.0 * nz * nz - 1.0); + case 1: return -0.5 * std::sqrt(15.0 / ModuleBase::PI) * nz * nx; + case 2: return 0.25 * std::sqrt(15.0 / ModuleBase::PI) * (nx * nx - ny * ny); + } + break; + } + default: { + ModuleBase::WARNING_QUIT("DFPT_Pert::real_ylm", + "real_ylm implemented for l<=2 only (DFPT NC path)."); + } + } + return 0.0; +} + +void DFPT_Pert::build_vkb(int it, int ia, + const std::vector>& gk, + std::vector>>& vkb) const { + // per-type projector bookkeeping mirrors the ground-state vnl_pw.cpp layout: + // every radial beta (nbeta) with angular momentum l spins out (2l+1) + // projectors with combined index lm = l^2 + m, m in 0..2l (i.e. the real + // harmonic m channels -l..l walked as m' = (-1)^(m+1) ceil... ABACUS ylm + // block: m=0, +1, -1, +2, -2, ...). We use the signed m' directly. + const pseudo& ncpp = ucell_->atoms[it].ncpp; + const int nh = ncpp.nh; + const int ngk = static_cast(gk.size()); + const ModuleBase::Vector3& tau = ucell_->atoms[it].tau[ia]; + vkb.assign(nh, std::vector>(ngk, std::complex(0.0, 0.0))); + if (nh == 0) { + return; + } + int mu = 0; + for (int ib = 0; ib < ncpp.nbeta; ++ib) { + const int l = ncpp.lll[ib]; + if (l > 2) { + ModuleBase::WARNING_QUIT("DFPT_Pert::build_vkb", + "DFPT NC projector path implemented for l<=2 only."); + } + const std::complex pref = + std::pow(std::complex(0.0, -1.0), l); // (-i)^l + for (int m = 0; m < 2 * l + 1; ++m) { + // ABACUS real-harmonic walk over the m channels of this radial beta: + // m=0 -> m'=0; m=1 -> m'=+1; m=2 -> m'=-1; m=3 -> m'=+2; m=4 -> m'=-2 + const int mr = (m == 0) ? 0 : ((m % 2 == 1) ? (m + 1) / 2 : -(m / 2)); + for (int ig = 0; ig < ngk; ++ig) { + const ModuleBase::Vector3& G = gk[ig]; // k(+q)+G, 2*pi/lat0 + const double gnorm = std::sqrt(G * G) * ucell_->tpiba; // bohr^-1 + // real_ylm handles the |G|=0 point itself (Y_00 is + // direction-independent; l>0 channels vanish there together + // with vq), so the raw vector is passed directly. + const double ylm = real_ylm(l, mr, G); + const double vq = radial_vq(it, ib, gnorm); + // GS structure-factor convention (stru_fac.cpp get_sk / + // eigts, ci_tpi = -2pi i): exp(-i 2pi (gk.tau)) + const double arg = -ModuleBase::TWO_PI * (G * tau); + const std::complex phase(std::cos(arg), std::sin(arg)); + vkb[mu][ig] = pref * ylm * vq * phase; + } + ++mu; + } + } +} + +void DFPT_Pert::grad_real_ylm(int l, int m, const ModuleBase::Vector3& ghat, + double grad[3]) const { + // analytic gradients of the real_ylm polynomials (l <= 2), consistent + // with the conventions documented above real_ylm + const double x = ghat.x; + const double y = ghat.y; + const double z = ghat.z; + const double c1 = 0.5 * std::sqrt(3.0 / ModuleBase::PI); + const double c2 = 0.5 * std::sqrt(15.0 / ModuleBase::PI); + const double c20 = 0.25 * std::sqrt(5.0 / ModuleBase::PI); + grad[0] = grad[1] = grad[2] = 0.0; + switch (l) { + case 0: + return; + case 1: + switch (m) { + case -1: grad[1] = -c1; return; + case 0: grad[2] = c1; return; + case 1: grad[0] = -c1; return; + } + break; + case 2: + switch (m) { + case -2: grad[0] = c2 * y; grad[1] = c2 * x; return; + case -1: grad[1] = -c2 * z; grad[2] = -c2 * y; return; + case 0: grad[2] = 6.0 * c20 * z; return; + case 1: grad[0] = -c2 * z; grad[2] = -c2 * x; return; + case 2: grad[0] = 2.0 * c20 * x; grad[1] = -2.0 * c20 * y; return; + } + break; + default: + ModuleBase::WARNING_QUIT("DFPT_Pert::grad_real_ylm", + "grad_real_ylm implemented for l<=2 only (DFPT NC path)."); + } +} + +void DFPT_Pert::build_vkb_dk(int it, int ia, int dir, + const std::vector>& gk, + std::vector>>& vkb, + std::vector>>& dvkb) const { + const pseudo& ncpp = ucell_->atoms[it].ncpp; + const int nh = ncpp.nh; + const int ngk = static_cast(gk.size()); + const ModuleBase::Vector3& tau = ucell_->atoms[it].tau[ia]; + if (static_cast(vkb.size()) != nh + || static_cast(vkb[0].size()) != ngk) { + ModuleBase::WARNING_QUIT("DFPT_Pert::build_vkb_dk", + "vkb must be built on the same gk list first."); + } + dvkb.assign(nh, std::vector>(ngk, std::complex(0.0, 0.0))); + if (nh == 0) { + return; + } + const double dg = 1.0e-4; // bohr^-1, radial central-difference step + int mu = 0; + for (int ib = 0; ib < ncpp.nbeta; ++ib) { + const int l = ncpp.lll[ib]; + const std::complex pref = + std::pow(std::complex(0.0, -1.0), l); // (-i)^l + for (int m = 0; m < 2 * l + 1; ++m) { + const int mr = (m == 0) ? 0 : ((m % 2 == 1) ? (m + 1) / 2 : -(m / 2)); + for (int ig = 0; ig < ngk; ++ig) { + const ModuleBase::Vector3& G = gk[ig]; + const double gmag = std::sqrt(G * G); // 2*pi/lat0 units + const double gnorm = gmag * ucell_->tpiba; // bohr^-1 + const double vq0 = radial_vq(it, ib, gnorm); + const double dvq = (radial_vq(it, ib, gnorm + dg) + - radial_vq(it, ib, std::max(0.0, gnorm - dg))) + / (dg * (gnorm > dg ? 2.0 : 1.0)); + const double arg = -ModuleBase::TWO_PI * (G * tau); + const std::complex phase(std::cos(arg), std::sin(arg)); + const std::complex dphase = + std::complex(0.0, -ModuleBase::TWO_PI * tau[dir]) * phase; + double dy[3] = {0.0, 0.0, 0.0}; + double ylm = 0.0; + if (gmag > 1.0e-10) { + const ModuleBase::Vector3 ghat = G * (1.0 / gmag); + ylm = real_ylm(l, mr, ghat); + grad_real_ylm(l, mr, ghat, dy); + const double gdir[3] = {ghat.x, ghat.y, ghat.z}; + // chain rule dghat/dk_dir = (e_dir - ghat*ghat_dir)/|G| + double dylm_dir = 0.0; + for (int c = 0; c < 3; ++c) { + dylm_dir += dy[c] * ((c == dir ? 1.0 : 0.0) - gdir[c] * gdir[dir]); + } + dylm_dir /= gmag; + // radial chain: dg/dk_dir = tpiba * ghat_dir + const double dradial = dvq * ucell_->tpiba * gdir[dir]; + dvkb[mu][ig] = pref * phase * (dylm_dir * vq0 + ylm * dradial) + + pref * ylm * vq0 * dphase; + } else { + // degenerate |G| = 0: only the l = 0 channel survives + // (real_ylm convention); keep only the phase term + ylm = (l == 0) ? 0.5 * std::sqrt(1.0 / ModuleBase::PI) : 0.0; + dvkb[mu][ig] = pref * ylm * vq0 * dphase; + } + } + ++mu; + } + } } -void DFPT_Pert::dVnl_dtau(int atom_idx, int dir, const ModuleBase::Vector3& q, +void DFPT_Pert::dVnl_dtau(int atom_idx, int dir, + const ModuleBase::Vector3& q_cart, const psi::Psi>& psi, int k_idx, std::vector>>& dv_psi) { + int it = 0; + int ia = 0; + atom_index(atom_idx, it, ia); + if (ia < 0) { + return; + } + const pseudo& ncpp = ucell_->atoms[it].ncpp; + if (ncpp.tvanp || ncpp.has_so) { + // the separable NC path documented in C1; ultrasoft and spin-orbit + // projectors are deferred (their D and augmentation have |k+q| shifts + // that need the USPP machinery). + ModuleBase::WARNING_QUIT("DFPT_Pert::dVnl_dtau", + "DFPT nonlocal first-order potential is implemented " + "for normal-conserving separable pseudopotentials only."); + } + const int nh = ncpp.nh; + + // projector -> (radial beta index, m channel) table, matching build_vkb. + std::vector mu_ib(nh, 0); + std::vector mu_m(nh, 0); + int mu_idx = 0; + for (int ib = 0; ib < ncpp.nbeta; ++ib) { + const int l = ncpp.lll[ib]; + for (int m = 0; m < 2 * l + 1; ++m) { + if (mu_idx < nh) { + mu_ib[mu_idx] = ib; + mu_m[mu_idx] = m; + } + ++mu_idx; + } + } + + // incoming k basis: G = k + G' (pw_wfc k-basis index) + const int npwk = pw_wfc_->npwk[k_idx]; + std::vector> gk_in(npwk); + for (int ig = 0; ig < npwk; ++ig) { + gk_in[ig] = pw_wfc_->getgpluskcar(k_idx, ig); + } + std::vector>> vkb_in; + build_vkb(it, ia, gk_in, vkb_in); + + // outgoing k+q basis + DFPT_KQ_Basis kq; + kq.init(pw_wfc_, pw_rho_, q_cart, k_idx); + const int npwk_kq = kq.get_npwk(); + std::vector> gk_out(npwk_kq); + for (int igl = 0; igl < npwk_kq; ++igl) { + gk_out[igl] = kq.get_gpluskq(igl); + } + std::vector>> vkb_out; + build_vkb(it, ia, gk_out, vkb_out); + + const int nbands = psi.get_nbands(); + dv_psi.assign(nbands, std::vector>(npwk_kq, std::complex(0.0, 0.0))); + + for (int iband = 0; iband < nbands; ++iband) { + // becp_nu(k) = sum_G' conj(vkb_in[nu][G']) psi(G') + std::vector> becp(nh, std::complex(0.0, 0.0)); + for (int nu = 0; nu < nh; ++nu) { + for (int ig = 0; ig < npwk; ++ig) { + becp[nu] += std::conj(vkb_in[nu][ig]) * psi(k_idx, iband, ig); + } + } + // dcbecp = D * becp with D_{mu,nu} = dion(ib_mu, ib_nu) delta_{m_mu, m_nu} + std::vector> dcbecp(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int nu = 0; nu < nh; ++nu) { + if (mu_m[mu] != mu_m[nu]) { + continue; + } + dcbecp[mu] += ncpp.dion(mu_ib[mu], mu_ib[nu]) * becp[nu]; + } + } + // term A: i (k+q+G'')_dir * (Vnl |psi>) on the k+q basis + std::vector> term_a(npwk_kq, std::complex(0.0, 0.0)); + for (int igl = 0; igl < npwk_kq; ++igl) { + std::complex vnlpsi(0.0, 0.0); + for (int mu = 0; mu < nh; ++mu) { + vnlpsi += vkb_out[mu][igl] * dcbecp[mu]; + } + term_a[igl] = std::complex(0.0, 1.0) * (ucell_->tpiba * gk_out[igl][dir]) * vnlpsi; + } + // term B: Vnl [i (k+G')_dir |psi>] + std::vector> becp_dpsi(nh, std::complex(0.0, 0.0)); + for (int nu = 0; nu < nh; ++nu) { + for (int ig = 0; ig < npwk; ++ig) { + const std::complex dpsi_ig = + std::complex(0.0, 1.0) * (ucell_->tpiba * gk_in[ig][dir]) * psi(k_idx, iband, ig); + becp_dpsi[nu] += std::conj(vkb_in[nu][ig]) * dpsi_ig; + } + } + std::vector> dcbecp_dpsi(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int nu = 0; nu < nh; ++nu) { + if (mu_m[mu] != mu_m[nu]) { + continue; + } + dcbecp_dpsi[mu] += ncpp.dion(mu_ib[mu], mu_ib[nu]) * becp_dpsi[nu]; + } + } + std::vector> term_b(npwk_kq, std::complex(0.0, 0.0)); + for (int igl = 0; igl < npwk_kq; ++igl) { + std::complex vnl_dpsi(0.0, 0.0); + for (int mu = 0; mu < nh; ++mu) { + vnl_dpsi += vkb_out[mu][igl] * dcbecp_dpsi[mu]; + } + term_b[igl] = vnl_dpsi; + } + for (int igl = 0; igl < npwk_kq; ++igl) { + // GS exp(-2pi gk.tau) projector convention: dVnl/dtau_dir + // |psi> = -i (k+q+G'')_dir (Vnl|psi>) + Vnl[i (k+G')_dir |psi>] + dv_psi[iband][igl] = term_b[igl] - term_a[igl]; + } + } +} + +void DFPT_Pert::build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) { + // C1 frozen term of the first-order Hubbard potential: + // |dphi(k+q)/dtau> V_eff + adjoint + // The provider is only usable when its occupation matrices are + // initialized (u_active()); DFPT_PW::init additionally rejects a wired + // provider outright (every U hook is a no-op U0 reservation), so this + // guard is defense in depth; the diamond DFT+U test (C7) will exercise + // this path once OnsiteProjector integration on the DFPT k+q basis is + // finalized. + if (!data.u_active()) { + return; + } + (void)q_idx; (void)atom_idx; (void)dir; - (void)q; - (void)psi; - (void)k_idx; - (void)dv_psi; + // TODO(C7): Onsite_Proj_tools on the DFPT k+q basis; occupation response + // (|phi(k+q)> U(diag*delta - docc) ) lands in C3 after docc. +} + +void DFPT_Pert::d2vloc_r(int atom_idx, int da, int db, + std::vector>& dv2_r) const { + if (pw_rho_ == nullptr) { + return; + } + int it = 0; + int ia = 0; + atom_index(atom_idx, it, ia); + if (ia < 0) { + dv2_r.clear(); + return; + } + const ModuleBase::Vector3& tau = ucell_->atoms[it].tau[ia]; + const int npw = pw_rho_->npw; + std::vector> dv2_recip(npw, std::complex(0.0, 0.0)); + ModuleBase::Vector3 gcar; + for (int ig = 0; ig < npw; ++ig) { + rho_gvec(ig, gcar); + // QE ground truth (dynmat_us.f90): the mixed (+q,-q) second-order + // local potential is the integer-G, q-independent kernel + // -tpiba^2 G_da G_db vloc(|G|) exp(-i G.tau); the (+q,-q) dressings + // collapse the carrier to 0 for every q, not only when 2q is + // reciprocal. + const ModuleBase::Vector3 w = gcar; + const double w2 = w * w; + if (w2 < 1.0e-12) { + continue; + } + const double vloc = vloc_at_g(it, w2 * ucell_->tpiba2); + const double arg = -ModuleBase::TWO_PI * (w * tau); + const std::complex phase(std::cos(arg), std::sin(arg)); + // (-i g_da)(-i g_db) = -g_da g_db + dv2_recip[ig] = -(ucell_->tpiba * w[da]) * (ucell_->tpiba * w[db]) * vloc * phase; + } + dv2_r.assign(pw_rho_->nrxx, std::complex(0.0, 0.0)); + pw_rho_->recip2real(dv2_recip.data(), dv2_r.data()); +} + +void DFPT_Pert::apply_d2vnl(int atom_idx, int da, int db, + const ModuleBase::Vector3& q_eff, + bool include_middle, + const psi::Psi>& psi, int k_idx, + std::vector>>& d2v_psi) const { + int it = 0; + int ia = 0; + atom_index(atom_idx, it, ia); + if (ia < 0) { + return; + } + const pseudo& ncpp = ucell_->atoms[it].ncpp; + if (ncpp.tvanp || ncpp.has_so) { + ModuleBase::WARNING_QUIT("DFPT_Pert::apply_d2vnl", + "DFPT second-order nonlocal potential is implemented " + "for normal-conserving separable pseudopotentials only."); + } + const int nh = ncpp.nh; + const int nbands = psi.get_nbands(); + + // projector -> (radial index, m channel) table, matching build_vkb + std::vector mu_ib(nh, 0); + std::vector mu_m(nh, 0); + int mu_idx = 0; + for (int ib = 0; ib < ncpp.nbeta; ++ib) { + const int l = ncpp.lll[ib]; + for (int m = 0; m < 2 * l + 1; ++m) { + if (mu_idx < nh) { + mu_ib[mu_idx] = ib; + mu_m[mu_idx] = m; + } + ++mu_idx; + } + } + + // incoming k basis and outgoing k+q basis projectors (same atom) + const int npwk = pw_wfc_->npwk[k_idx]; + std::vector> gk_in(npwk); + for (int ig = 0; ig < npwk; ++ig) { + gk_in[ig] = pw_wfc_->getgpluskcar(k_idx, ig); + } + std::vector>> vkb_in; + build_vkb(it, ia, gk_in, vkb_in); + DFPT_KQ_Basis kq; + kq.init(pw_wfc_, pw_rho_, q_eff, k_idx); + const int npwk_kq = kq.get_npwk(); + std::vector> gk_out(npwk_kq); + for (int igl = 0; igl < npwk_kq; ++igl) { + gk_out[igl] = kq.get_gpluskq(igl); + } + std::vector>> vkb_out; + build_vkb(it, ia, gk_out, vkb_out); + + d2v_psi.assign(nbands, std::vector>(npwk_kq, std::complex(0.0, 0.0))); + for (int iband = 0; iband < nbands; ++iband) { + // becp and its (k+G')-weighted variants: becp_x = sum x(G') |beta>> becp(nh, std::complex(0.0, 0.0)); + std::vector> becp_a(nh, std::complex(0.0, 0.0)); + std::vector> becp_b(nh, std::complex(0.0, 0.0)); + std::vector> becp_ab(nh, std::complex(0.0, 0.0)); + for (int nu = 0; nu < nh; ++nu) { + for (int ig = 0; ig < npwk; ++ig) { + const std::complex vc = std::conj(vkb_in[nu][ig]) * psi(k_idx, iband, ig); + const double kp_da = ucell_->tpiba * gk_in[ig][da]; + const double kp_db = ucell_->tpiba * gk_in[ig][db]; + becp[nu] += vc; + becp_a[nu] += kp_da * vc; + becp_b[nu] += kp_db * vc; + becp_ab[nu] += kp_da * kp_db * vc; + } + } + // D contraction with the same-m selection rule as dVnl_dtau + std::vector> d0(nh, std::complex(0.0, 0.0)); + std::vector> da_(nh, std::complex(0.0, 0.0)); + std::vector> db_(nh, std::complex(0.0, 0.0)); + std::vector> dab(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int nu = 0; nu < nh; ++nu) { + if (mu_m[mu] != mu_m[nu]) { + continue; + } + const double dij = ncpp.dion(mu_ib[mu], mu_ib[nu]); + d0[mu] += dij * becp[nu]; + da_[mu] += dij * becp_a[nu]; + db_[mu] += dij * becp_b[nu]; + dab[mu] += dij * becp_ab[nu]; + } + } + // chi(G'') = sum_mu vkb_out,mu [ -kq_da kq_db d0 - dab + // + (include_middle ? kq_da db_ + kq_db da_ : 0) ]_mu + // QE ground truth (dynmat_us.f90 + phq_init.f90): the KB second-order + // term pairs gammap (integer-G (k+G)_da(k+G)_db derivative of beta) + // with becp1 = and the same-atom alphap_a* alphap_b + // middle product; everything is built at k with integer-G momentum + // factors, so the caller passes q_eff = 0 and the kernel is + // q-independent for every q. + for (int igl = 0; igl < npwk_kq; ++igl) { + const double kq_da = ucell_->tpiba * gk_out[igl][da]; + const double kq_db = ucell_->tpiba * gk_out[igl][db]; + std::complex chi(0.0, 0.0); + for (int mu = 0; mu < nh; ++mu) { + chi += vkb_out[mu][igl] * (-kq_da * kq_db * d0[mu] - dab[mu]); + if (include_middle) { + chi += vkb_out[mu][igl] * (kq_da * db_[mu] + kq_db * da_[mu]); + } + } + d2v_psi[iband][igl] = chi; + } + } +} + +void DFPT_Pert::build_efield(const ModuleBase::Vector3& field, DFPT_PW_Data& data) { + // first-order electric-field potential: delta V(r) = - r . E (q=0 limit, + // position operator in the periodic cell). Computed directly on the shared + // real-space grid. Only relevant for the Q0 dielectric response (C6). + if (pw_rho_ == nullptr) { + return; + } + if (pw_rho_->gamma_only) { + ModuleBase::WARNING_QUIT("DFPT_Pert::build_efield", + "DFPT requires a complex (gamma_only=false) real-space basis."); + } + std::vector> dv_real(pw_rho_->nrxx, std::complex(0.0, 0.0)); + const ModuleBase::Matrix3& latvec = ucell_->latvec; + const double lat0 = ucell_->lat0; + // shared real-space grid layout (serial pool): ir = (ix*ny + iy)*nz + iz, + // i.e. z runs fastest (verified against the impulse response of the FFT). + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + const int iz = ir % pw_rho_->nz; + const int rem = ir / pw_rho_->nz; + const int iy = rem % pw_rho_->ny; + const int ix = rem / pw_rho_->ny; + const double fx = ix / static_cast(pw_rho_->nx); + const double fy = iy / static_cast(pw_rho_->ny); + const double fz = iz / static_cast(pw_rho_->nz); + ModuleBase::Vector3 r; + r.x = (fx * latvec.e11 + fy * latvec.e12 + fz * latvec.e13) * lat0; + r.y = (fx * latvec.e21 + fy * latvec.e22 + fz * latvec.e23) * lat0; + r.z = (fx * latvec.e31 + fy * latvec.e32 + fz * latvec.e33) * lat0; + dv_real[ir] = -(field * r); // -e r.E (e absorbed in field convention) + } + data.set_dv_rc(0, 0, dv_real); } } // namespace ModuleDFPT \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index fb5e27ff357..86124f53a64 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -9,12 +9,14 @@ #ifndef DFPT_PERT_H #define DFPT_PERT_H +#include "dfpt_kq_basis.h" #include "dfpt_pw_data.h" #include "source_cell/unitcell.h" #include "source_psi/psi.h" #include "source_basis/module_pw/pw_basis.h" #include "source_basis/module_pw/pw_basis_k.h" -#include "source_pw/module_pwdft/stru_fac.h" + +class Structure_Factor; namespace ModuleDFPT { @@ -23,8 +25,13 @@ class DFPT_Pert { DFPT_Pert(); ~DFPT_Pert(); - void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, + void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, ModulePW::PW_Basis_K* pw_wfc, Structure_Factor& sf); + + /// C5: read access to the ground-state wfc basis for the dynamical-matrix + /// contractions in DFPT_Phon::accumulate_electron. + ModulePW::PW_Basis_K* get_pw_wfc() const { return pw_wfc_; } + ModulePW::PW_Basis* get_pw_rho() const { return pw_rho_; } void build_dv(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data); @@ -33,20 +40,141 @@ class DFPT_Pert { void build_efield(const ModuleBase::Vector3& field, DFPT_PW_Data& data); + /// C5: real-space kernel of the same-atom second-order LOCAL potential + /// d2V_loc(r) = d^2 V_loc / d tau_{da} d tau_{db} (both displacement + /// dressings e^{i q.R} multiply on the SAME atom, so the cell sum + /// collapses to G = 2q mod integers: the kernel is nonzero only when 2q + /// is reciprocal, in which case every integer G survives with its own + /// phase and the kernel equals the plain q=0 one, + /// -w_da w_db Vloc(|w|) exp(i w.tau)). The caller gates on 2q reciprocal + /// and skips otherwise. Returned on the shared real-space grid; its + /// expectation value with |u(r)|^2 enters the electronic dynamical + /// matrix (anharmonic term). + void d2vloc_r(int atom_idx, int da, int db, + std::vector>& dv2_r) const; + + /// C5: same-atom second-order NONLOCAL potential acting on psi, + /// chi_n(G'') = (d^2 Vnl / d tau_{da} d tau_{db}) |psi_n> on the + /// q_eff-shifted basis (q_eff = q when q is itself a reciprocal vector, + /// otherwise 2q: the second-order potential carries wavevector 2q, and + /// the |d beta>& q_eff, + bool include_middle, + const psi::Psi>& psi, int k_idx, + std::vector>>& d2v_psi) const; + + /// Build the beta-projector array (in the ABACUS vkb convention) for a + /// single atom on an arbitrary k-shifted reciprocal vector list: + /// vkb[mu][ielem] = (-i)^l * Ylm(Ghat) * (4pi/sqrt(Omega) * + /// integral beta(r) j_l(g r) r dr) * exp(i G.tau) + /// with G in 2*pi/lat0 units, g = |G| * tpiba (bohr^-1), tau in bohr. + /// Usable for both the incoming k basis (G = k+G') and the outgoing DFPT + /// k+q basis (G = k+q+G''), so the atomic phase is correct on either side. + /// Public since C6: DFPT_Q0 reuses it for the velocity operator. + void build_vkb(int it, int ia, + const std::vector>& gk, + std::vector>>& vkb) const; + + /// C6: analytic derivative of the beta projector with respect to the + /// list shift k_dir (the same shift build_vkb is evaluated at), three + /// terms: the atomic phase (i 2pi tau_dir), the radial chain rule + /// (vq'(g) * tpiba * Ghat_dir, central finite difference of radial_vq) + /// and the real-harmonic direction derivative (grad_real_ylm chain + /// (e_dir - ghat ghat_dir)/|G|). Feeds the dV_nl/dk part of the + /// velocity operator in DFPT_Q0::pos_matrix. + void build_vkb_dk(int it, int ia, int dir, + const std::vector>& gk, + std::vector>>& vkb, + std::vector>>& dvkb) const; + + /// radial part (4pi/sqrt(Omega)) Integral beta(r) j_l(g r) r dr at g (bohr^-1) + double radial_vq(int it, int ib, double g) const; + + /// C7: apply a complex real-space potential on the shared FFT grid to + /// every band of psi (k basis), delivering |v psi> on the k+q basis. + /// The potential is the q-shifted complex periodic amplitude (the same + /// convention as dv_rc); the DFPT self-consistent loop uses it for the + /// screened response potential (Hartree + XC) of the mixed density. + void apply_vr(int q_idx, int k_idx, + const std::vector>& v_rc, + const psi::Psi>& psi, + const ModuleBase::Vector3& q_cart, + std::vector>>& dv_psi) const; + private: UnitCell* ucell_ = nullptr; ModulePW::PW_Basis* pw_rho_ = nullptr; ModulePW::PW_Basis_K* pw_wfc_ = nullptr; Structure_Factor* sf_ = nullptr; - + + /// C1: first-order LOCAL potential dVloc_dtau (per displaced atom). + /// Grid helper: reconstruct the cartesian reciprocal vector (in 2*pi/lat0 + /// units) of rho-grid index ig from the shared FFT-grid (ix,iy,iz) mapping. + void rho_gvec(int ig, ModuleBase::Vector3& gcar) const; + /// The local pseudopotential Vloc(g^2) at an arbitrary magnitude: + /// Coulomb atoms use the analytic form, numeric pseudopotentials reuse the + /// radial-mesh Fourier transform of vl_pw.cpp::vloc_of_g at |g| themselves. + double vloc_at_g(int it, double g2) const; + /// linear atom index -> (type, picture) of ucell_. + void atom_index(int atom_idx, int& it, int& ia) const; + + /// First-order asymmetric-part local potential on the rho grid: + /// dVloc_dtau(Delta) = -i (Delta+q).direction * Vloc(|Delta+q|) + /// * exp(-i (Delta+q).tau_atom) * ... + /// (GS structure-factor convention exp(-2pi g.tau); the sign/coefficient + /// is the exact derivative of the local potential with respect to the + /// atomic displacement). void dVloc_dtau(int atom_idx, int dir, const ModuleBase::Vector3& q, std::vector>& dv); + /// C1: first-order NONLOCAL potential acting on psi (normal-conserving + /// separable case), for one displaced atom in direction dir. + /// Uses the identity (GS exp(-2pi gk.tau) projector convention) + /// dVnl/dtau_a |psi> = -i (k+q+G'')_a * (Vnl |psi>) + /// + Vnl[ i (k+G')_a |psi> ] + /// so only two applications of the ground-state nonlocal operator on the + /// DFPT k+q outgoing basis are needed (dsVnl contribution per pair is + /// i (q+G''-G')_a times the zero-order matrix element). + /// USPP/ultrasoft and spin-orbit projectors are rejected for now. void dVnl_dtau(int atom_idx, int dir, const ModuleBase::Vector3& q, const psi::Psi>& psi, int k_idx, - std::vector>>& dv_psi); + std::vector>>& dv_psi); + + /// real spherical harmonic Y_{l,m}(g_hat), orthonormal convention, l<=2. + double real_ylm(int l, int m, const ModuleBase::Vector3& ghat) const; + /// gradient of real_ylm with respect to the unit vector ghat, l<=2 + /// (dY/dghat returned per cartesian component). + void grad_real_ylm(int l, int m, const ModuleBase::Vector3& ghat, + double grad[3]) const; + + /// General (nonlocal and local) part of apply_dv for the compartments that + /// live in real space (local potential); the |psi> product requires the + /// shared real-space grid of pw_rho_/pw_wfc_. + void real_space_dv(int q_idx, int k_idx, + const psi::Psi>& psi, + DFPT_PW_Data& data, + const DFPT_KQ_Basis& kq, + std::vector>>& dv_psi) const; + + /// shared core of real_space_dv / apply_vr: phase-free cyclic convolution + /// of v_rc with every band of psi, scattered/gathered between the k+q + /// list and the rho grid through the FFT-cell triple. + void apply_vr_core(int k_idx, + const std::vector>& v_rc, + const psi::Psi>& psi, + const DFPT_KQ_Basis& kq, + std::vector>>& dv_psi) const; + + /// first-order Hubbard potential dV_U (U0 reservation, C1 frozen term). + void build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data); }; } // namespace ModuleDFPT -#endif // DFPT_PERT_H +#endif // DFPT_PERT_H \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 8659237d67a..9807307113b 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -8,64 +8,692 @@ #include "dfpt_phon.h" +#include "dfpt_kq_basis.h" +#include "dfpt_pert.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" +#include "source_base/module_external/lapack_connector.h" +#include "source_base/tool_quit.h" +#include "source_base/truncated_func.h" +#include "source_basis/module_pw/pw_basis.h" + +#include +#include +#include +#include +#include +#include +#include + namespace ModuleDFPT { DFPT_Phon::DFPT_Phon() {} DFPT_Phon::~DFPT_Phon() {} -void DFPT_Phon::init(UnitCell& ucell) { +namespace { + +// signed frequencies: omega = sgn(e) sqrt(|e|), converted to cm^-1 +// sqrt(Ry/(bohr^2 amu)) in cm^-1 = sqrt(RYDBERG_SI/amu_kg)/(bohr*2pi*c) +std::vector signed_freqs_cm1(const std::vector& eigs) { + const double amu_kg = 1.0e-3 / ModuleBase::NA; + const double ry_bohr2_amu_to_cm1 = std::sqrt(ModuleBase::RYDBERG_SI / amu_kg) + / (ModuleBase::BOHR_RADIUS_SI * ModuleBase::TWO_PI + * 2.99792458e10); + std::vector freq(eigs.size(), 0.0); + for (size_t i = 0; i < eigs.size(); ++i) { + freq[i] = ((eigs[i] >= 0.0) ? 1.0 : -1.0) * std::sqrt(std::abs(eigs[i])) + * ry_bohr2_amu_to_cm1; + } + return freq; +} + +} // namespace + +void DFPT_Phon::init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, DFPT_Pert* pert) { ucell_ = &ucell; + pw_rho_ = pw_rho; + pert_ = pert; } +// --------------------------------------------------------------------------- +// Ewald ion-ion force constants (C5) +// --------------------------------------------------------------------------- + +void DFPT_Phon::ion_ion(const ModuleBase::Vector3& q_frac, + ModuleBase::ComplexMatrix& dyn) { + const int nat = ucell_->nat; + const int nat3 = 3 * nat; + const double lat0 = ucell_->lat0; + const ModuleBase::Matrix3& latvec = ucell_->latvec; + + // total ionic charge + double charge = 0.0; + for (int it = 0; it < ucell_->ntype; ++it) { + charge += ucell_->atoms[it].na * ucell_->atoms[it].ncpp.zv; + } + + // choose the screening alpha so that the G-sum tail is converged inside + // the rho grid (the erfc envelope bounds the exp(-G^2/4alpha) tail); + // ggecut counts |G_max|^2 in 1/lat0^2 units (pw_basis.h), so the bohr^2 + // cutoff is ggecut * tpiba2 + double alpha = 1.1; + double upperbound = 0.0; + do { + alpha *= 0.9; + if (alpha < 1.0e-4) { + ModuleBase::WARNING_QUIT("DFPT_Phon::ion_ion", + "Can't find optimal Ewald alpha."); + } + upperbound = 2.0 * charge * charge + * std::sqrt(2.0 * alpha / ModuleBase::TWO_PI) + * ModuleBase::truncated_erfc( + std::sqrt(pw_rho_->ggecut * ucell_->tpiba2 / 4.0 / alpha)); + } while (upperbound > 1.0e-6); + ewald_alpha_ = alpha; + // erfc(alpha R) < 1e-16 well inside 6/sqrt(alpha) + ewald_rcut_ = 6.0 / std::sqrt(alpha); + + const ModuleBase::Vector3 q_cart = q_frac * ucell_->G; + + // ---------------- reciprocal-space part ---------------- + // Poisson pair identity (validated against direct sums): + // sum_L h(R) e^{i2pi q.L} = sum_L h_erfc(R) e^{i2pi q.L} + // + (4pi/Omega) sum_{|G+q|>0} (G+q)_a (G+q)_b / |G+q|^2 + // exp(-|G+q|^2/4a) e^{i2pi (G+q).(tau_a-tau_b)} + // so the G part enters D with the + sign while the erfc part carries -. + // The on-site diagonal (both second derivatives act on tau_a in cell 0) + // is phase-free: it is accumulated from Gamma-phase (G-only) pair terms + // as -sqrt(Mb/Ma) times the pair element. sq/s0 accumulate the self-image + // phase difference of the same-atom images (validated element-wise + // against finite differences of the erfc-split Ewald energy in a + // q-commensurate supercell): + // D_ii(q) - D_ii(0) = (Za^2 e2 / Ma) [ sum_{L!=0} h(L)(1 - cos(2pi q.L)) + // + (4pi/Omega)(sq - s0) ], + // where sq sums (G+q)(G+q)/|G+q|^2 exp(-|G+q|^2/4a) over all grid G + // (the G = 0 member contributes through w = q) and s0 the same kernel + // at q = 0. The alpha independence of this combination was verified + // numerically; at q = 0 both differences vanish and the acoustic sum + // rule holds exactly by construction. + double sq[3][3] = {{0.0}}; + double s0[3][3] = {{0.0}}; + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + const ModuleBase::Vector3& gcart = pw_rho_->gcar[ig]; + const ModuleBase::Vector3 w = gcart + q_cart; + const double w2 = w * w; + const double g2 = gcart * gcart; + if (w2 < 1.0e-12) { + // G + q = 0 (only possible at q = 0 with G = 0): excluded, as in + // the q = 0 G part below; its isotropic delta/3 limit belongs to + // the direction-averaged q -> 0 behavior, not the exact q = 0 + // matrix + continue; + } + const double w2_bohr = w2 * ucell_->tpiba2; + const double gauss = ModuleBase::truncated_exp(-w2_bohr / (4.0 * alpha)); + for (int da = 0; da < 3; ++da) { + for (int db = 0; db < 3; ++db) { + sq[da][db] += w[da] * w[db] / w2 * gauss; + } + } + double gauss_g = 0.0; + if (g2 > 1.0e-12) { + gauss_g = ModuleBase::truncated_exp(-g2 * ucell_->tpiba2 / (4.0 * alpha)); + for (int da = 0; da < 3; ++da) { + for (int db = 0; db < 3; ++db) { + s0[da][db] += gcart[da] * gcart[db] / g2 * gauss_g; + } + } + } + for (int ia = 0; ia < nat; ++ia) { + const int ita = ucell_->iat2it[ia]; + const int iia = ucell_->iat2ia[ia]; + const double za = ucell_->atoms[ita].ncpp.zv; + const double ma = ucell_->atoms[ita].mass; + const ModuleBase::Vector3& ta = ucell_->atoms[ita].tau[iia]; + for (int ib = 0; ib < nat; ++ib) { + if (ib == ia) { + continue; + } + const int itb = ucell_->iat2it[ib]; + const int iib = ucell_->iat2ia[ib]; + const double zb = ucell_->atoms[itb].ncpp.zv; + const double mb = ucell_->atoms[itb].mass; + const ModuleBase::Vector3& tb = ucell_->atoms[itb].tau[iib]; + const double arg = ModuleBase::TWO_PI * (w * (ta - tb)); + const std::complex phase(std::cos(arg), std::sin(arg)); + const double pref = ModuleBase::FOUR_PI / ucell_->omega + * za * zb * ModuleBase::e2 * gauss + / (std::sqrt(ma * mb) * w2); + // Gamma-phase on-site piece (G-only kernel, G != 0) + std::complex phase0(1.0, 0.0); + double pref0 = 0.0; + if (g2 > 1.0e-12) { + const double arg0 = ModuleBase::TWO_PI * (gcart * (ta - tb)); + phase0 = std::complex(std::cos(arg0), std::sin(arg0)); + pref0 = ModuleBase::FOUR_PI / ucell_->omega + * za * zb * ModuleBase::e2 * gauss_g + / (std::sqrt(ma * mb) * g2); + } + for (int da = 0; da < 3; ++da) { + for (int db = 0; db < 3; ++db) { + const std::complex elem = pref * w[da] * w[db] * phase; + dyn(3 * ia + da, 3 * ib + db) += elem; + // on-site diagonal: phase-free (Gamma) accumulation, + // Phi_ii = -Phi_ij => -sqrt(Mb/Ma) on the pair term + dyn(3 * ia + da, 3 * ia + db) + -= pref0 * gcart[da] * gcart[db] * phase0 + * std::sqrt(mb / ma); + } + } + } + } + } + // self-image G-space phase difference on the diagonal + for (int ia = 0; ia < nat; ++ia) { + const double za = ucell_->atoms[ucell_->iat2it[ia]].ncpp.zv; + const double ma = ucell_->atoms[ucell_->iat2it[ia]].mass; + const double f2 = za * za * ModuleBase::e2 / ma; + for (int da = 0; da < 3; ++da) { + for (int db = 0; db < 3; ++db) { + dyn(3 * ia + da, 3 * ia + db) + += f2 * ModuleBase::FOUR_PI / ucell_->omega + * (sq[da][db] - s0[da][db]); + } + } + } + + // ---------------- real-space part ---------------- + // h_ab(R) = d^2/dR_a dR_b [ erfc(sqrt(alpha) R) / R ] + // = erfc(sqrt(alpha) R) (3 Ra Rb - delta R^2)/R^5 + // + (2 sqrt(alpha)/sqrt(pi)) e^{-alpha R^2} + // [ 2 alpha Ra Rb/R^2 + 3 Ra Rb/R^4 - delta/R^2 ] + // D^R_ab = -(1/sqrt(MaMb)) ZaZb e2 h(R = tau_b + l - tau_a) e^{i2pi q.l} + // ranges of the lattice-vector shells (rows of latvec are the lattice + // translations in lat0 units) + const double row_e[3][3] = {{latvec.e11, latvec.e12, latvec.e13}, + {latvec.e21, latvec.e22, latvec.e23}, + {latvec.e31, latvec.e32, latvec.e33}}; + int nmax[3] = {0, 0, 0}; + for (int d = 0; d < 3; ++d) { + const ModuleBase::Vector3 a1(row_e[d][0], row_e[d][1], row_e[d][2]); + const double len = std::sqrt(a1 * a1) * lat0; // bohr + nmax[d] = static_cast(std::ceil(ewald_rcut_ / len)) + 1; + } + for (int ia = 0; ia < nat; ++ia) { + const int ita = ucell_->iat2it[ia]; + const int iia = ucell_->iat2ia[ia]; + const double za = ucell_->atoms[ita].ncpp.zv; + const double ma = ucell_->atoms[ita].mass; + for (int ib = 0; ib < nat; ++ib) { + const int itb = ucell_->iat2it[ib]; + const int iib = ucell_->iat2ia[ib]; + const double zb = ucell_->atoms[itb].ncpp.zv; + const double mb = ucell_->atoms[itb].mass; + const ModuleBase::Vector3 dt = + ucell_->atoms[itb].tau[iib] - ucell_->atoms[ita].tau[iia]; + if (ib == ia) { + // self-image phase difference: the on-site i-i energy is + // L-independent while the cross-cell i-i force constants carry + // e^{i2pi q.L}, so D_ii receives + // -(Za^2 e2/Ma) sum_{L!=0} h_erfc(L) (e^{i2pi q.L} - 1); + // the imaginary part cancels over the +-L symmetric sphere + // (h is even) and L = 0 carries e^{i2pi q.0} - 1 = 0 + for (int n1 = -nmax[0]; n1 <= nmax[0]; ++n1) { + for (int n2 = -nmax[1]; n2 <= nmax[1]; ++n2) { + for (int n3 = -nmax[2]; n3 <= nmax[2]; ++n3) { + if (n1 == 0 && n2 == 0 && n3 == 0) { + continue; + } + const ModuleBase::Vector3 lvec( + n1 * latvec.e11 + n2 * latvec.e21 + n3 * latvec.e31, + n1 * latvec.e12 + n2 * latvec.e22 + n3 * latvec.e32, + n1 * latvec.e13 + n2 * latvec.e23 + n3 * latvec.e33); + const ModuleBase::Vector3 r = lvec * lat0; + const double r2 = r * r; + if (r2 > ewald_rcut_ * ewald_rcut_) { + continue; + } + const double rlen = std::sqrt(r2); + const double sar = std::sqrt(alpha); + const double e2a = ModuleBase::truncated_exp(-alpha * r2); + const double f = 2.0 * sar / std::sqrt(ModuleBase::PI) * e2a; + const double er = ModuleBase::truncated_erfc(sar * rlen); + const double ph_arg = ModuleBase::TWO_PI + * (q_frac.x * n1 + q_frac.y * n2 + + q_frac.z * n3); + const double wcos = std::cos(ph_arg) - 1.0; + const double f2 = za * za * ModuleBase::e2 / ma; + for (int da = 0; da < 3; ++da) { + for (int db = 0; db < 3; ++db) { + const double delta = (da == db) ? 1.0 : 0.0; + const double h = er * (3.0 * r[da] * r[db] - delta * r2) + / (rlen * r2 * r2) + + f * (2.0 * alpha * r[da] * r[db] / r2 + + 3.0 * r[da] * r[db] / (r2 * r2) + - delta / r2); + dyn(3 * ia + da, 3 * ia + db) -= f2 * h * wcos; + } + } + } + } + } + continue; + } + for (int n1 = -nmax[0]; n1 <= nmax[0]; ++n1) { + for (int n2 = -nmax[1]; n2 <= nmax[1]; ++n2) { + for (int n3 = -nmax[2]; n3 <= nmax[2]; ++n3) { + const ModuleBase::Vector3 lvec( + n1 * latvec.e11 + n2 * latvec.e21 + n3 * latvec.e31, + n1 * latvec.e12 + n2 * latvec.e22 + n3 * latvec.e32, + n1 * latvec.e13 + n2 * latvec.e23 + n3 * latvec.e33); + ModuleBase::Vector3 r = (lvec + dt) * lat0; // bohr + const double r2 = r * r; + if (r2 > ewald_rcut_ * ewald_rcut_) { + continue; + } + const double rlen = std::sqrt(r2); + const double r3 = r2 * rlen; + const double sar = std::sqrt(alpha); + const double e2a = ModuleBase::truncated_exp(-alpha * r2); + const double f = 2.0 * sar / std::sqrt(ModuleBase::PI) * e2a; + const double er = ModuleBase::truncated_erfc(sar * rlen); + const double ph_arg = ModuleBase::TWO_PI + * (q_frac.x * n1 + q_frac.y * n2 + q_frac.z * n3); + const std::complex phase(std::cos(ph_arg), std::sin(ph_arg)); + const double zab2 = za * zb * ModuleBase::e2 / std::sqrt(ma * mb); + for (int da = 0; da < 3; ++da) { + for (int db = 0; db < 3; ++db) { + const double delta = (da == db) ? 1.0 : 0.0; + // d^2/dR_a dR_b [erfc(sqrt(alpha) R)/R], + // validated against central finite differences + const double h = er * (3.0 * r[da] * r[db] - delta * r2) / (r3 * r2) + + f * (2.0 * alpha * r[da] * r[db] / r2 + + 3.0 * r[da] * r[db] / (r2 * r2) + - delta / r2); + dyn(3 * ia + da, 3 * ib + db) -= zab2 * h * phase; + // on-site diagonal Phi_ii^R = sum_{j != i} + // Z_iZ_j sum_L h(r_ij + L): phase-free (both + // derivatives act on tau_a in cell 0), i.e. + // -sqrt(Mb/Ma) times the pair term + dyn(3 * ia + da, 3 * ia + db) + += zab2 * std::sqrt(mb / ma) * h; + } + } + } + } + } + } + } + + // The Gaussian self constant -Z^2 sqrt(2 alpha/pi) and the h_erf contact + // -4 alpha^{3/2}/(3 sqrt(pi)) delta_ab are tau-independent and cancel in + // the (e^{i2pi q.L} - 1) differences; the diagonal is carried by the + // phase-free cross-atom accumulation plus the self-image phase terms + // (both G and R pieces above). At q = 0 all phase differences vanish and + // the acoustic sum rule holds exactly by construction. +} + +// --------------------------------------------------------------------------- +// electronic contribution (2n+1 theorem) +// --------------------------------------------------------------------------- + +void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, + const psi::Psi>& psi, + const ModuleBase::matrix& wg, DFPT_PW_Data& data) { + if (pert_ == nullptr || pw_rho_ == nullptr || ucell_ == nullptr) { + return; + } + const int nat = ucell_->nat; + const int nat3 = 3 * nat; + if (accum_q_ != q_idx || dynmat_accum_.nr != nat3) { + dynmat_accum_ = ModuleBase::ComplexMatrix(nat3, nat3, true); + accum_q_ = q_idx; + } + const int rowb = 3 * atom_idx + dir; + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + + // stash the converged dpsi of this displacement (apply_dv reuses the slot): + // prefer the per-displacement store of the two-pass flow; fall back to + // the working slots for the legacy interleaved call order + std::vector>>> dpsib + = data.get_dpsi_disp(atom_idx, dir); + if (dpsib.empty() || static_cast(dpsib.size()) < nk + || (nk > 0 && static_cast(dpsib[0].size()) < nbands)) { + dpsib.assign(nk, std::vector>>(nbands)); + for (int ik = 0; ik < nk; ++ik) { + for (int ib = 0; ib < nbands; ++ib) { + dpsib[ik][ib] = data.get_dpsi(q_idx, ik, ib); + } + } + } + + for (int iat = 0; iat < nat; ++iat) { + for (int idir = 0; idir < 3; ++idir) { + const int cola = 3 * iat + idir; + // ---- term 2 over all k,n ---- + // Hermitian (2n+1) accumulation: the row element gets X_ba and + // the transposed element gets conj(X_ba); the self-consistent + // response of dpsi^b already contains the screening, and the + // Hartree-xc kernel quadratic term cancels the + // cross terms by the variational identity, so only the bare + // external perturbation appears here + pert_->build_dv(q_idx, iat, idir, data); + std::complex cross(0.0, 0.0); + for (int ik = 0; ik < nk; ++ik) { + pert_->apply_dv(q_idx, ik, psi, data); + for (int ib = 0; ib < nbands; ++ib) { + if (!dfpt_band_occupied(wg, ik, ib)) { + continue; + } + const std::vector> rhs = data.get_dpsi(q_idx, ik, ib); + const std::vector>& sol = dpsib[ik][ib]; + if (rhs.size() != sol.size() || sol.empty()) { + continue; + } + std::complex dot(0.0, 0.0); + for (size_t i = 0; i < sol.size(); ++i) { + dot += std::conj(sol[i]) * rhs[i]; + } + cross += wg(ik, ib) * dot; + } + } + const double mass_norm + = std::sqrt(ucell_->atoms[ucell_->iat2it[atom_idx]].mass + * ucell_->atoms[ucell_->iat2it[iat]].mass); + dynmat_accum_(rowb, cola) += cross / mass_norm; + dynmat_accum_(cola, rowb) += std::conj(cross) / mass_norm; + + // ---- same-atom anharmonic term ---- + // QE ground truth (dynmat_us.f90 + phq_init.f90): the mixed + // (+q, -q) second-order potential of the local part is + // -Omega tpiba^2 G_a G_b vloc(|G|) Re[rho(G) e^{-iG tau_s}] + // (integer G, no q), and the KB nonlocal part is the same-atom + // block deff[gammap*becp1 + becp1*gammap + alphap_a*alphap_b + + // alphap_b*alphap_a] with becp1/alphap/gammap all built from + // vkb_k and (k+G) factors (integer G, no q). The (+q,-q) + // dressings collapse to an integer-G carrier for every q, so + // this term is q-independent and must never be gated on 2q + // commensurability (the old gate silently dropped it for + // 2q not reciprocal, e.g. q=(0.25,0,0), and produced + // imaginary phonon branches). + const ModuleBase::Vector3 q_eff_cart(0.0, 0.0, 0.0); + // the same-atom d2 middle term is always included (its + // q-independence is established QE ground truth; the old + // 2q-commensurability gate and the D2MID A/B knob are gone) + const bool include_middle = true; + if (iat == atom_idx && cola >= rowb) { + std::vector> dv2_r; + pert_->d2vloc_r(atom_idx, idir, dir, dv2_r); + if (static_cast(dv2_r.size()) != pw_rho_->nrxx) { + dv2_r.assign(pw_rho_->nrxx, std::complex(0.0, 0.0)); + } + std::vector>> chi; + std::complex d2sum(0.0, 0.0); + std::vector> u_r(pw_rho_->nrxx); + std::vector> x_r(pw_rho_->nrxx); + std::vector> x_recip(pw_rho_->npw, std::complex(0.0, 0.0)); + for (int ik = 0; ik < nk; ++ik) { + pert_->apply_d2vnl(atom_idx, idir, dir, q_eff_cart, include_middle, psi, ik, chi); + // k+q_eff scatter map for this k (must match apply_d2vnl) + DFPT_KQ_Basis kq; + kq.init(pert_->get_pw_wfc(), pert_->get_pw_rho(), q_eff_cart, ik); + const int npwk_kq = kq.get_npwk(); + for (int ib = 0; ib < nbands; ++ib) { + if (!dfpt_band_occupied(wg, ik, ib)) { + continue; + } + pert_->get_pw_wfc()->recip2real(&psi(ik, ib, 0), u_r.data(), ik); + if (static_cast(chi.size()) == nbands + && static_cast(chi[ib].size()) == npwk_kq) { + std::fill(x_recip.begin(), x_recip.end(), std::complex(0.0, 0.0)); + for (int igl = 0; igl < npwk_kq; ++igl) { + const int ig_rho = kq.get_ig_rho(igl); + if (ig_rho >= 0) { + x_recip[ig_rho] = chi[ib][igl]; + } + } + pw_rho_->recip2real(x_recip.data(), x_r.data()); + } + else { + std::fill(x_r.begin(), x_r.end(), std::complex(0.0, 0.0)); + } + std::complex expect(0.0, 0.0); + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + expect += std::conj(u_r[ir]) * u_r[ir] * dv2_r[ir] + + std::conj(u_r[ir]) * x_r[ir]; + } + d2sum += wg(ik, ib) * expect / static_cast(pw_rho_->nxyz); + } + } + const double inv_m + = 1.0 / ucell_->atoms[ucell_->iat2it[atom_idx]].mass; + dynmat_accum_(rowb, cola) += d2sum * inv_m; + if (cola != rowb) { + dynmat_accum_(cola, rowb) += std::conj(d2sum) * inv_m; + } + } + } + } + + // restore the converged dpsi of this displacement + for (int ik = 0; ik < nk; ++ik) { + for (int ib = 0; ib < nbands; ++ib) { + if (!dpsib[ik][ib].empty()) { + data.set_dpsi(q_idx, ik, ib, dpsib[ik][ib]); + } + } + } +} + +// --------------------------------------------------------------------------- +// assemble / diagonalize / LO-TO / sum rule +// --------------------------------------------------------------------------- + void DFPT_Phon::assemble(int q_idx, DFPT_PW_Data& data) { - int nat = ucell_->nat; - ModuleBase::matrix dynmat(3 * nat, 3 * nat); - dynmat.zero_out(); - - ModuleBase::Vector3 q = data.get_qvec(q_idx); - ion_ion(q, dynmat); - electron(q_idx, data, dynmat); - - data.set_dynmat(q_idx, dynmat); + if (ucell_ == nullptr) { + return; + } + const int nat = ucell_->nat; + const int nat3 = 3 * nat; + ModuleBase::ComplexMatrix dyn(nat3, nat3, true); + if (pw_rho_ != nullptr) { + ion_ion(data.get_qvec(q_idx), dyn); + } + if (accum_q_ == q_idx && dynmat_accum_.nr == nat3) { + for (int i = 0; i < nat3; ++i) { + for (int j = 0; j < nat3; ++j) { + dyn(i, j) += dynmat_accum_(i, j); + } + } + } + // DFT+U dynamical-matrix term (U0 reservation, implemented with the C7/U1 + // Plus_U wiring): sum_nk w_nk [ + frozen second-order term]. + if (data.with_u()) { + dftu_onsite(q_idx, data); + } + // Hermitian symmetrization (rows filled by independent solves) + for (int i = 0; i < nat3; ++i) { + for (int j = i + 1; j < nat3; ++j) { + const std::complex avg + = 0.5 * (dyn(i, j) + std::conj(dyn(j, i))); + dyn(i, j) = avg; + dyn(j, i) = std::conj(avg); + } + } + data.set_dynmat(q_idx, dyn); + dynmat_accum_ = ModuleBase::ComplexMatrix(); + accum_q_ = -1; } void DFPT_Phon::diagonalize(int q_idx, DFPT_PW_Data& data) { - ModuleBase::matrix dynmat = data.get_dynmat(q_idx); - int nat = ucell_->nat; - - std::vector freq(3 * nat, 0.0); - for (int i = 0; i < 3 * nat; ++i) { - freq[i] = static_cast(i); - } - + const int nat = ucell_->nat; + const int nat3 = 3 * nat; + ModuleBase::ComplexMatrix dyn = data.get_dynmat(q_idx); + if (dyn.nr != nat3) { + return; + } + + // eigenvalues of the complex Hermitian dynamical matrix (Ry/bohr^2/amu) + std::vector w(nat3, 0.0); + std::vector rwork(std::max(1, 3 * nat3 - 2), 0.0); + std::vector> work(1); + int info = 0; + LapackConnector::zheev('N', 'U', nat3, dyn, nat3, w.data(), work.data(), -1, + rwork.data(), &info); + work.resize(std::max(1, static_cast(work[0].real()))); + LapackConnector::zheev('N', 'U', nat3, dyn, nat3, w.data(), work.data(), + static_cast(work.size()), rwork.data(), &info); + + // signed frequencies: omega = sgn(e) sqrt(|e|), converted to cm^-1 + // sqrt(Ry/(bohr^2 amu)) in cm^-1 = sqrt(RYDBERG_SI/amu_kg)/(bohr*2pi*c) + const double amu_kg = 1.0e-3 / ModuleBase::NA; + const double ry_bohr2_amu_to_cm1 = std::sqrt(ModuleBase::RYDBERG_SI / amu_kg) + / (ModuleBase::BOHR_RADIUS_SI * ModuleBase::TWO_PI + * 2.99792458e10); + std::vector freq(nat3, 0.0); + for (int i = 0; i < nat3; ++i) { + const double e = w[i]; + freq[i] = ((e >= 0.0) ? 1.0 : -1.0) * std::sqrt(std::abs(e)) * ry_bohr2_amu_to_cm1; + } data.set_phon_freq(q_idx, freq); } -void DFPT_Phon::add_loto(DFPT_PW_Data& data) { - (void)data; +void DFPT_Phon::add_loto(const ModuleBase::Vector3& qhat, DFPT_PW_Data& data) { + const int nat = ucell_->nat; + const int nat3 = 3 * nat; + ModuleBase::ComplexMatrix dyn = data.get_dynmat(0); + if (dyn.nr != nat3) { + return; + } + const ModuleBase::matrix eps = data.get_dielectric(); + if (eps.nr != 3 || eps.nc != 3) { + return; // no dielectric tensor stored yet (C6 not run) + } + const double qeq = qhat.x * (qhat.x * eps(0, 0) + qhat.y * eps(1, 0) + qhat.z * eps(2, 0)) + + qhat.y * (qhat.x * eps(0, 1) + qhat.y * eps(1, 1) + qhat.z * eps(2, 1)) + + qhat.z * (qhat.x * eps(0, 2) + qhat.y * eps(1, 2) + qhat.z * eps(2, 2)); + if (std::abs(qeq) < 1.0e-10) { + return; + } + const double pref = ModuleBase::FOUR_PI * ModuleBase::e2 / ucell_->omega / qeq; + for (int ia = 0; ia < nat; ++ia) { + const double ma = ucell_->atoms[ucell_->iat2it[ia]].mass; + const ModuleBase::matrix za = data.get_born(ia); + if (za.nr != 3 || za.nc != 3) { + continue; + } + for (int ib = 0; ib < nat; ++ib) { + const double mb = ucell_->atoms[ucell_->iat2it[ib]].mass; + const ModuleBase::matrix zb = data.get_born(ib); + for (int da = 0; da < 3; ++da) { + // (qhat Z*_a)_da = sum_gamma qhat_gamma Z_a(da,gamma) + const double qza = qhat.x * za(da, 0) + qhat.y * za(da, 1) + qhat.z * za(da, 2); + for (int db = 0; db < 3; ++db) { + const double qzb = qhat.x * zb(db, 0) + qhat.y * zb(db, 1) + qhat.z * zb(db, 2); + dyn(3 * ia + da, 3 * ib + db) += pref * qza * qzb / std::sqrt(ma * mb); + } + } + } + } + data.set_dynmat(0, dyn); } -bool DFPT_Phon::check_sum_rule(int q_idx, DFPT_PW_Data& data) const { - (void)q_idx; - (void)data; - return true; +void DFPT_Phon::diagonalize_loto(DFPT_PW_Data& data) { + const int nat3 = 3 * ucell_->nat; + // the stored Gamma matrix already carries the non-analytic term added + // by add_loto; the copy below is destroyed by the solver, the stored + // one stays available for the plain report + ModuleBase::ComplexMatrix dyn = data.get_dynmat(0); + if (dyn.nr != nat3) { + return; + } + std::vector w(nat3, 0.0); + std::vector rwork(std::max(1, 3 * nat3 - 2), 0.0); + std::vector> work(1); + int info = 0; + LapackConnector::zheev('N', 'U', nat3, dyn, nat3, w.data(), work.data(), -1, + rwork.data(), &info); + work.resize(std::max(1, static_cast(work[0].real()))); + LapackConnector::zheev('N', 'U', nat3, dyn, nat3, w.data(), work.data(), + static_cast(work.size()), rwork.data(), &info); + data.set_phon_freq_loto(signed_freqs_cm1(w)); } -void DFPT_Phon::ion_ion(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn) { - (void)q; - (void)dyn; +std::string DFPT_Phon::format_q_report(int q_idx, const DFPT_PW_Data& data) const { + const ModuleBase::Vector3 qd = data.get_qvec(q_idx); + const std::vector freq = data.get_phon_freq(q_idx); + std::ostringstream os; + os << " DFPT phonon frequencies at q #" << q_idx << " = (" + << std::fixed << std::setprecision(6) + << qd.x << " " << qd.y << " " << qd.z + << ") (direct) in cm^-1:" << "\n"; + for (size_t im = 0; im < freq.size(); ++im) { + os << " mode " << std::setw(3) << im << " : " + << std::fixed << std::setprecision(6) << freq[im] << " cm^-1" << "\n"; + } + return os.str(); } -void DFPT_Phon::electron(int q_idx, DFPT_PW_Data& data, ModuleBase::matrix& dyn) { - (void)q_idx; - (void)data; - (void)dyn; +std::string DFPT_Phon::format_loto_report(const DFPT_PW_Data& data) const { + const std::vector freq = data.get_phon_freq_loto(); + if (freq.empty()) { + return std::string(); + } + const ModuleBase::Vector3 dir = data.get_loto_dir(); + std::ostringstream os; + os << " DFPT LO-TO corrected frequencies at q #0 along q->0 direction (" + << std::fixed << std::setprecision(6) + << dir.x << " " << dir.y << " " << dir.z + << ") in cm^-1:" << "\n"; + for (size_t im = 0; im < freq.size(); ++im) { + os << " mode " << std::setw(3) << im << " : " + << std::fixed << std::setprecision(6) << freq[im] << " cm^-1" << "\n"; + } + return os.str(); } -void DFPT_Phon::ewald_sum(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn) { - (void)q; - (void)dyn; +bool DFPT_Phon::check_sum_rule(int q_idx, DFPT_PW_Data& data) const { + const ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); + if (std::abs(q_frac.x) > 1.0e-8 || std::abs(q_frac.y) > 1.0e-8 + || std::abs(q_frac.z) > 1.0e-8) { + return true; // only applies at Gamma + } + const int nat3 = 3 * ucell_->nat; + ModuleBase::ComplexMatrix dyn = data.get_dynmat(q_idx); + if (dyn.nr != nat3) { + return false; + } + double max_elem = 0.0; + for (int i = 0; i < nat3; ++i) { + for (int j = 0; j < nat3; ++j) { + max_elem = std::max(max_elem, std::abs(dyn(i, j))); + } + } + if (max_elem < 1.0e-12) { + return true; + } + for (int i = 0; i < nat3; ++i) { + std::complex colsum(0.0, 0.0); + for (int j = 0; j < nat3; ++j) { + colsum += dyn(i, j); + } + if (std::abs(colsum) > 1.0e-6 * max_elem) { + return false; + } + } + return true; +} + +void DFPT_Phon::dftu_onsite(int q_idx, DFPT_PW_Data& data) { + // Reserved DFT+U contribution to the dynamical matrix (U0). + // The physical implementation lands with the Plus_U production wiring: + // sum_nk w_nk [ + frozen second-order U term + // (~ becp * V_eff * dbecp_f contractions) ], accumulated into the + // dynamical matrix. dV_U itself is assembled by DFPT_Pert::build_dv_u. + (void)q_idx; + (void)data; } -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_phon.h b/source/source_pw/module_dfpt/dfpt_phon.h index 6e343b8cd09..25c99a355e2 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.h +++ b/source/source_pw/module_dfpt/dfpt_phon.h @@ -11,37 +11,105 @@ #include "dfpt_pw_data.h" #include "source_cell/unitcell.h" +#include "source_psi/psi.h" + +#include + +namespace ModulePW { +class PW_Basis; +} namespace ModuleDFPT { +class DFPT_Pert; + +/** + * @brief Dynamical matrix of DFPT (C5). + * + * assemble() merges the Ewald ion-ion force constants with the electronic + * contribution accumulated by accumulate_electron(); diagonalize() solves + * the complex Hermitian eigenproblem through LapackConnector::zheev and + * stores frequencies as signed values omega_i = sgn(e_i) sqrt(|e_i|) in + * cm^-1 (negative = imaginary frequency). + * + * Electronic contribution (2n+1 theorem, insulating case): + * D_ab = D^Ewald_ab + * + 2 sum_kn wg Re + * + sum_kn wg (same atom a,b only) + * with dpsi^b the converged Sternheimer solution for displacement b and + * dV^a_ext the BARE first-order external potential of displacement a; the + * row D[b][*] is filled right after displacement b converges, so the dpsi + * storage never needs a direction dimension (data-layer refactor reserved + * for phase B). + */ class DFPT_Phon { public: DFPT_Phon(); ~DFPT_Phon(); - void init(UnitCell& ucell); + void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, DFPT_Pert* pert); void assemble(int q_idx, DFPT_PW_Data& data); + + /// Fill the D[b][*] row of the electronic dynamical-matrix contribution + /// for the converged displacement (atom_idx, dir); psi/wg are the + /// ground-state wavefunctions and occupations. Requires a wired + /// DFPT_Pert (init); a null pert leaves the row untouched. + void accumulate_electron(int q_idx, int atom_idx, int dir, + const psi::Psi>& psi, + const ModuleBase::matrix& wg, DFPT_PW_Data& data); void diagonalize(int q_idx, DFPT_PW_Data& data); + + /// Diagonalize the LO-TO corrected Gamma dynamical matrix (after + /// add_loto has merged the non-analytic term into data.dynmat(0)) and + /// store the signed frequencies (cm^-1) into data.phon_freq_loto; the + /// uncorrected data.phon_freq(0) of the plain diagonalize stays intact. + void diagonalize_loto(DFPT_PW_Data& data); + + /// Human-readable per-q frequency report: a header carrying the q index + /// and the direct q coordinates plus one signed-frequency line per mode. + /// Deterministic fixed-precision formatting; consumed by the esolver + /// post-processing output and pinned by the format regression test. + std::string format_q_report(int q_idx, const DFPT_PW_Data& data) const; + + /// Report of the LO-TO corrected Gamma frequencies along + /// data.loto_dir(); returns an empty string unless the corrected + /// frequencies have been computed (add_loto + diagonalize_loto). + std::string format_loto_report(const DFPT_PW_Data& data) const; - void add_loto(DFPT_PW_Data& data); + /// Non-analytic (LO-TO) term along the q->0 direction qhat (unit vector, + /// Cartesian): D_NAC = (4 pi e^2/Omega) (qhat Z*_a)(qhat Z*_b) / + /// (qhat eps_inf qhat) / sqrt(M_a M_b). Uses the dielectric tensor and + /// Born charges stored in data (set by DFPT_Q0, C6). + void add_loto(const ModuleBase::Vector3& qhat, DFPT_PW_Data& data); + /// Acoustic sum rule check at q=Gamma: max_a |sum_b D_ab| relative to + /// the largest matrix element; returns true when below 1e-6 (or away + /// from Gamma, where the rule does not apply). bool check_sum_rule(int q_idx, DFPT_PW_Data& data) const; private: UnitCell* ucell_ = nullptr; + ModulePW::PW_Basis* pw_rho_ = nullptr; + DFPT_Pert* pert_ = nullptr; double ewald_alpha_ = 0.0; double ewald_rcut_ = 0.0; - void ion_ion(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn); + /// Ewald ion-ion force constants C^ewald_ab(q) (G-space + real-space + + /// Gaussian self term), mass-reduced by 1/sqrt(M_a M_b). + void ion_ion(const ModuleBase::Vector3& q_frac, ModuleBase::ComplexMatrix& dyn); - void electron(int q_idx, DFPT_PW_Data& data, ModuleBase::matrix& dyn); - - void ewald_sum(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn); + /// DFT+U contribution to the dynamical matrix (U0 reservation). + void dftu_onsite(int q_idx, DFPT_PW_Data& data); + + /// accumulated electronic rows of the current q (merged and cleared by + /// assemble) + ModuleBase::ComplexMatrix dynmat_accum_; + int accum_q_ = -1; }; } // namespace ModuleDFPT -#endif // DFPT_PHON_H \ No newline at end of file +#endif // DFPT_PHON_H diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 0a635d6dd45..ecc68474686 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -1,6 +1,6 @@ // ============================================================ // This code is added by Mohan Chen on 2026-05-18. -// This code is currently in the design phase and has not been +// This code is currently in design phase and has not been // put into production yet. It may change in the future. // Please use this code with caution. Only developers who know // what they are doing should use this code. @@ -14,15 +14,32 @@ #include "dfpt_phon.h" #include "dfpt_q0.h" #include "dfpt_metal.h" +#include "dfpt_hamilt_shift.h" +#include "dfpt_kq_basis.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" +#include #include "source_cell/qlist.h" +#include "source_pw/module_pwdft/stru_fac.h" + +#include +#include +#include +#include +#include +#include +#include namespace ModuleDFPT { class DFPT_PW::Impl { public: Impl() {} - ~Impl() {} - + ~Impl() + { + delete hamilt_; + } + DFPT_PW_Data data_; DFPT_Pert pert_; DFPT_Stern stern_; @@ -31,15 +48,54 @@ class DFPT_PW::Impl { DFPT_Q0 q0_; DFPT_Metal metal_; ModuleCell::QList qlist_; - + DFPT_HamiltShift* hamilt_ = nullptr; + psi::Psi> gs_psi_; UnitCell* ucell_ = nullptr; + ModulePW::PW_Basis* pw_rho_ = nullptr; + ModulePW::PW_Basis_K* pw_wfc_ = nullptr; + Structure_Factor* sf_ = nullptr; + std::vector veff_r_; + ModuleBase::matrix wg_; + ModuleBase::matrix eig_; + const XC_First_Order* xc_ = nullptr; double nelec_ = 0.0; double ecutwfc_ = 0.0; - + const Plus_U_Base* dftu_ = nullptr; + + ///< occupied states at k+q on the k+q G list, [ik][occ m][igl]; + /// rebuilt per q (they depend on q and k only) + std::vector>>> occ_kq_; + ///< remembers the (q_idx, ik) the shifted operator was last cached at + int last_q_ = -1; + int last_ik_ = -1; + std::vector ikq_of_k_; + int nqx_ = 1, nqy_ = 1, nqz_ = 1; + std::string qfile_; double conv_thr_ = 1e-8; int max_iter_ = 100; + double mix_beta_ = 0.4; + + bool wired() const { return pw_rho_ != nullptr && pw_wfc_ != nullptr; } + + /// occupied-state projector set at k+q for every k of this q (commensurate + /// q: kvec_d[ik] + q must be a k point of the ground-state list mod lattice) + void build_occ_kq(int q_idx); + + /// one self-consistent Sternheimer cycle for the displacement (iat, idir) + /// at q; returns the achieved density residual (zero when unwired) + double solve_displacement(int q_idx, int iat, int idir); + + /// position legs Y^a_{k,v} = P_c x_a|psi_{k,v}> of the q = 0 mesh + /// (velocity-rhs Sternheimer solves, one per direction; stashed through + /// data as the exact position leg of the screened Born charges) + void solve_pos_resp(int q_idx); + + /// E-field SCF response dpsi^E,a of the q = 0 mesh (QE solve_e + + /// dfpt_kernel form: fixed point on the rhs -(Y^a + dV_sc^E,a|psi>) + /// with the screening assembly of solve_displacement) + void solve_efield_resp(int q_idx); }; DFPT_PW::DFPT_PW() : pimpl_(new Impl()) {} @@ -49,57 +105,798 @@ DFPT_PW::~DFPT_PW() { } void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, - double nelec, double ecutwfc) { + ModulePW::PW_Basis* pw_rho, ModulePW::PW_Basis_K* pw_wfc, + Structure_Factor* sf, const std::vector& veff_r, + const ModuleBase::matrix& wg, const ModuleBase::matrix& eig, + const XC_First_Order* xc, + double nelec, double ecutwfc, const Plus_U_Base* dftu) { pimpl_->ucell_ = &ucell; pimpl_->gs_psi_ = psi; + pimpl_->pw_rho_ = pw_rho; + pimpl_->pw_wfc_ = pw_wfc; + pimpl_->sf_ = sf; + pimpl_->veff_r_ = veff_r; + pimpl_->wg_ = wg; + pimpl_->eig_ = eig; + + // Metallic-sampling guard: the Sternheimer/projector flow treats every + // band as either fully occupied or empty and carries no d(mu)/dtau + // response, so a sampling whose smearing Fermi level cuts a band (wg + // strictly between 0 and the full reference) yields force constants + // wrong at the 100% level while still converging cleanly. Reject it + // explicitly (C4 defers metallic DFPT); negligible gauss tails + // (relative weight < 1e-3) are tolerated as the insulator limit. + for (int ik = 0; ik < wg.nr; ++ik) { + const double wref = wg(ik, 0); + if (wref <= 0.0) { + continue; + } + for (int ib = 0; ib < wg.nc; ++ib) { + const double rel = wg(ik, ib) / wref; + if (rel > 1.0e-3 && rel < 1.0 - 1.0e-3) { + std::stringstream msg; + msg << "fractional band occupation at (ik=" << ik + << ", ib=" << ib << ", wg=" << wg(ik, ib) + << "): metallic DFPT (smearing occupations crossing the" + " Fermi level) is not supported; reduce smearing sigma" + " or use an insulating k sampling."; + ModuleBase::WARNING_QUIT("DFPT_PW::init", msg.str()); + } + } + } + pimpl_->xc_ = xc; pimpl_->nelec_ = nelec; pimpl_->ecutwfc_ = ecutwfc; - - std::vector mp_grid = {pimpl_->nqx_, pimpl_->nqy_, pimpl_->nqz_}; - pimpl_->qlist_.generate_mesh(ucell, ucell.symm, mp_grid, true); - + pimpl_->dftu_ = dftu; + + // DFT+U guard: the ground state now supports PW-basis DFT+U and wires a + // provider when dft_plus_u is enabled, but every DFPT U hook + // (DFPT_Rho::cal_docc, DFPT_Pert::build_dv_u, DFPT_Q0 born/docc + // contractions, DFPT_Phon::dftu_onsite) is a no-op reservation (U0). + // Running anyway would converge cleanly while silently dropping the + // whole first-order U response, so reject explicitly until U1 lands + // (same fail-loud pattern as the metallic-sampling guard above). + if (dftu != nullptr) { + ModuleBase::WARNING_QUIT("DFPT_PW::init", + "DFT+U with DFPT is not supported yet: the " + "first-order U response is not implemented " + "(U0 reservation); rerun with dft_plus_u 0."); + } + + // q points: an explicit q list file overrides the Monkhorst-Pack mesh + if (!pimpl_->qfile_.empty()) { + pimpl_->qlist_.read_from_file(pimpl_->qfile_, ucell); + if (pimpl_->qlist_.get_nq() == 0) { + ModuleBase::WARNING_QUIT("DFPT_PW::init", + "failed to read the DFPT q-point file: " + pimpl_->qfile_); + } + } else { + std::vector mp_grid = {pimpl_->nqx_, pimpl_->nqy_, pimpl_->nqz_}; + pimpl_->qlist_.generate_mesh(ucell, ucell.symm, mp_grid, true); + } + int nq = pimpl_->qlist_.get_nq(); int nk = psi.get_nk(); int nbands = psi.get_nbands(); int npw_max = psi.get_current_ngk(); - int nrxx = 0; + int nrxx = (pw_rho != nullptr) ? pw_rho->nrxx : 0; int nspin = 1; int nat = ucell.nat; - - pimpl_->data_.init(&pimpl_->qlist_, nk, nbands, npw_max, nrxx, nspin, nat); + + if (pw_rho != nullptr && pw_wfc != nullptr && sf != nullptr) { + pimpl_->pert_.init(ucell, pw_rho, pw_wfc, *sf); + // plain-mixing coefficient: the response Jacobian has strongly + // negative eigenvalues concentrated on the smallest-G shells (the + // Coulomb stiffness 4pi/G^2; measured lambda ~ -2.2 on {111}/{200} + // for the diamond smoke case), so the coefficient must stay below + // 2 / (1 + |lambda_min|); the INPUT default 0.4 keeps margin up to + // |lambda| ~ 3; the alternative is mix_type = "kerker", the screen + // f_g = |G+q|^2 / (|G+q|^2 + a^2) in 1/lat0^2 units (a^2 via + // DFPT_KERKER_A2), which stabilizes those shells at beta up to 1; + // the env knobs are design-phase calibration aids + double mix_beta = pimpl_->mix_beta_; + if (const char* env_beta = getenv("DFPT_MIX_BETA")) { + const double parsed = atof(env_beta); + if (parsed > 0.0 && parsed <= 1.0) { + mix_beta = parsed; + } + } + std::string mix_type = "plain"; + if (const char* env_type = getenv("DFPT_MIX_TYPE")) { + const std::string parsed = env_type; + if (parsed == "plain" || parsed == "kerker") { + mix_type = parsed; + } + } + double kerker_a2 = 1.0; + if (const char* env_a2 = getenv("DFPT_KERKER_A2")) { + const double parsed = atof(env_a2); + if (parsed > 0.0) { + kerker_a2 = parsed; + } + } + pimpl_->rho_.init(nspin, nrxx, pw_rho, pw_wfc, ucell.G, mix_type, mix_beta, kerker_a2); + pimpl_->phon_.init(ucell, pw_rho, &pimpl_->pert_); + pimpl_->q0_.init(ucell, pw_rho, pw_wfc, &pimpl_->pert_); + delete pimpl_->hamilt_; + pimpl_->hamilt_ = new DFPT_HamiltShift(ucell, pw_rho, pw_wfc, veff_r, &pimpl_->pert_); + } else { + pimpl_->phon_.init(ucell, nullptr, nullptr); + } + pimpl_->data_.init(&pimpl_->qlist_, nk, nbands, npw_max, nrxx, nspin, nat, dftu); +} + +bool DFPT_PW::get_with_u() const { + return pimpl_->data_.with_u(); +} + +bool DFPT_PW::get_u_active() const { + return pimpl_->data_.u_active(); +} + +void DFPT_PW::Impl::build_occ_kq(int q_idx) { + const int nk = pw_wfc_->nks; + occ_kq_.assign(nk, std::vector>>()); + ikq_of_k_.assign(nk, -1); + const ModuleBase::Vector3 q_frac = data_.get_qvec(q_idx); + const ModuleBase::Vector3 q_cart = q_frac * ucell_->G; + for (int ik = 0; ik < nk; ++ik) { + // k+q folded into [0,1) direct coordinates must be a ground-state k + // point (DFPT q meshes are commensurate with the k mesh) + const ModuleBase::Vector3 target = pw_wfc_->kvec_d[ik] + q_frac; + int ikq = -1; + for (int j = 0; j < nk; ++j) { + const ModuleBase::Vector3& kj = pw_wfc_->kvec_d[j]; + const double rx = std::round(kj.x - target.x); + const double ry = std::round(kj.y - target.y); + const double rz = std::round(kj.z - target.z); + if (std::abs(kj.x - target.x - rx) < 1.0e-6 + && std::abs(kj.y - target.y - ry) < 1.0e-6 + && std::abs(kj.z - target.z - rz) < 1.0e-6) { + ikq = j; + break; + } + } + if (ikq < 0) { + std::ostringstream oss; + oss << "k+q is not a point of the ground-state k list: the DFPT " + "q mesh must be commensurate with the k mesh (and inside " + "the first Brillouin zone). ik=" << ik + << " k_d=(" << pw_wfc_->kvec_d[ik].x << "," << pw_wfc_->kvec_d[ik].y + << "," << pw_wfc_->kvec_d[ik].z << ") q_d=(" << q_frac.x << "," + << q_frac.y << "," << q_frac.z << ") k+q=(" << target.x << "," + << target.y << "," << target.z << ") nk=" << nk; + ModuleBase::WARNING_QUIT("DFPT_PW::build_occ_kq", oss.str()); + } + ikq_of_k_[ik] = ikq; + + DFPT_KQ_Basis kq; + kq.init(pw_wfc_, pw_rho_, q_cart, ik); + const int npw_kq = kq.get_npwk(); + + // The congruence match above may fold k+q onto a *different label* + // of the same physical point (e.g. k lists holding both (1/2,0,0) + // and (-1/2,0,0), which differ by a reciprocal lattice vector). + // The two balls then enumerate different G labels: a state of the + // ikq ball with vector G' coincides physically with the k+q-ball + // vector G when G' + k(ijq) == G + k(ik) + q, i.e. + // G' = G + dn with dn = k_d(ik) + q - k_d(ikq) integer in + // reciprocal-basis coordinates. Coincident FFT cells identify the + // same G only for dn = 0, so match through the G vectors instead. + const ModuleBase::Vector3 dn = pw_wfc_->kvec_d[ik] + q_frac + - pw_wfc_->kvec_d[ikq]; + const double dnr[3] = {std::round(dn.x), std::round(dn.y), std::round(dn.z)}; + if (std::abs(dn.x - dnr[0]) > 1.0e-6 || std::abs(dn.y - dnr[1]) > 1.0e-6 + || std::abs(dn.z - dnr[2]) > 1.0e-6) { + ModuleBase::WARNING_QUIT("DFPT_PW::build_occ_kq", + "k+q folds onto a k-list entry with a " + "non-integer reciprocal offset."); + } + const int dn_i[3] = {static_cast(dnr[0]), + static_cast(dnr[1]), + static_cast(dnr[2])}; + const ModuleBase::Matrix3 ginv = pw_wfc_->G.Inverse(); + // reciprocal-basis integer triple -> per-k index of the ikq ball + // (pw_wfc_ is a PW_Basis_K whose gcar holds a per-k ball layout, + // not the parent-class global-ig layout: read it through getgcar) + std::map, int> jgl_of_n; + for (int jgl = 0; jgl < pw_wfc_->npwk[ikq]; ++jgl) { + const ModuleBase::Vector3 gf + = pw_wfc_->getgcar(ikq, jgl) * ginv; + const std::vector key = {static_cast(std::round(gf.x)), + static_cast(std::round(gf.y)), + static_cast(std::round(gf.z))}; + jgl_of_n[key] = jgl; + } + + const int nbands = gs_psi_.get_nbands(); + for (int m = 0; m < nbands; ++m) { + if (!dfpt_band_occupied(wg_, ikq, m)) { + continue; // empty at k+q: outside the P_c projector + } + std::vector> state(npw_kq, std::complex(0.0, 0.0)); + for (int igl = 0; igl < npw_kq; ++igl) { + const ModuleBase::Vector3 gf = kq.get_gcar(igl) * ginv; + const std::vector key + = {static_cast(std::round(gf.x)) + dn_i[0], + static_cast(std::round(gf.y)) + dn_i[1], + static_cast(std::round(gf.z)) + dn_i[2]}; + const auto it = jgl_of_n.find(key); + if (it != jgl_of_n.end()) { + state[igl] = gs_psi_(ikq, m, it->second); + } + } + occ_kq_[ik].push_back(std::move(state)); + } + } + last_q_ = q_idx; + last_ik_ = -1; +} + +double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { + if (!wired() || hamilt_ == nullptr) { + return 0.0; + } + const ModuleBase::Vector3 q_frac = data_.get_qvec(q_idx); + const ModuleBase::Vector3 q_cart = q_frac * ucell_->G; + const int nrxx = pw_rho_->nrxx; + const int nk = gs_psi_.get_nk(); + const int nbands = gs_psi_.get_nbands(); + + pert_.build_dv(q_idx, iat, idir, data_); + rho_.reset_mixing(q_idx); + // the previous perturbation's stored response must not leak into the + // first iteration of this one + data_.set_drho_g(q_idx, 0, + std::vector>(pw_rho_->npw, + std::complex(0.0, 0.0))); + + const int lin_max = data_.get_max_iter(); + const double lin_thr = data_.get_conv_thr(); + + bool converged = false; + double residual = 0.0; + const bool dbg = (getenv("DFPT_DEBUG") != nullptr); + // last screened response potential (hoisted out of the loop: the 2n+1 + // accumulation below needs the converged v_sc of this displacement) + std::vector> v_sc_r_last; + for (int iter = 0; iter < max_iter_ && !converged; ++iter) { + // the per-displacement SCF state (iter / residual / converged) is + // local to this solve: the DFPT_PW_Data ledger is the per-(q,irrep) + // outer-pass record kept by run(), and the final residual is + // returned to the caller for that aggregation (B4) + + // ---- 1. screened response potential from the mixed input density: + // q-shifted complex periodic amplitude on the shared grid, i.e. the + // same convention as dv_rc (v_hartree_q acts on the q-shifted + // coefficients; the XC kernel responds to Re/Im of the amplitude) + std::vector> v_sc_r(nrxx, std::complex(0.0, 0.0)); + const std::vector> drho_in_g = data_.get_drho_g(q_idx, 0); + if (!drho_in_g.empty() && static_cast(drho_in_g.size()) == pw_rho_->npw) { + std::vector> dv_ha_g; + rho_.v_hartree_q(q_cart, drho_in_g, dv_ha_g); + std::vector> vh_r(nrxx); + pw_rho_->recip2real(dv_ha_g.data(), vh_r.data()); + for (int ir = 0; ir < nrxx; ++ir) { + v_sc_r[ir] = vh_r[ir]; + } + if (xc_ != nullptr) { + std::vector> a_r(nrxx); + pw_rho_->recip2real(drho_in_g.data(), a_r.data()); + std::vector> b_r; + xc_->apply(a_r, b_r); + if (static_cast(b_r.size()) == nrxx) { + for (int ir = 0; ir < nrxx; ++ir) { + v_sc_r[ir] += b_r[ir]; + } + } + } + if (dbg) { + double dh = 0.0; + double dv = 0.0; + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + dh += std::norm(drho_in_g[ig]); + } + for (int ir = 0; ir < nrxx; ++ir) { + dv += std::norm(v_sc_r[ir]); + } + std::cout << "DBG iter=" << iter << " |drho_in_g|=" << std::sqrt(dh) + << " |v_sc_r|=" << std::sqrt(dv) << std::endl; + } + } + v_sc_r_last = v_sc_r; + + // ---- 2. Sternheimer solve of every occupied (k, band) + for (int ik = 0; ik < nk; ++ik) { + if (static_cast(occ_kq_.size()) <= ik || occ_kq_[ik].empty()) { + if (dbg) { std::cout << "DBG skip ik=" << ik << " no occ_kq" << std::endl; } + continue; // no occupied states at k+q: nothing to solve + } + // dV_ext |psi_n> for all bands (dVloc convolution + dVnl_dtau) + pert_.apply_dv(q_idx, ik, gs_psi_, data_); + // screened response part |v_sc psi_n> + std::vector>> dv_sc; + pert_.apply_vr(q_idx, ik, v_sc_r, gs_psi_, q_cart, dv_sc); + if (ik != last_ik_ || last_q_ != q_idx) { + hamilt_->set_context(q_cart, ik); + last_ik_ = ik; + if (dbg) { + std::cout << "DBG occ_kq nstates=" << occ_kq_[ik].size() << std::endl; + for (size_t m = 0; m < occ_kq_[ik].size(); ++m) { + double nrm = 0.0; + for (size_t i = 0; i < occ_kq_[ik][m].size(); ++i) { + nrm += std::norm(occ_kq_[ik][m][i]); + } + std::cout << "DBG occ[" << m << "] |psi|^2=" << nrm << std::endl; + } + // kernel consistency: must equal + // eig(ikq, m); the eigenvalue used by set_shift below is + // the k-side one (equal only when H is assembled right) + for (size_t m = 0; m < occ_kq_[ik].size(); ++m) { + hamilt_->set_shift(0.0); + std::vector> hp(occ_kq_[ik][m].size()); + hamilt_->apply(occ_kq_[ik][m].data(), hp.data()); + std::complex dot(0.0, 0.0); + for (size_t i = 0; i < hp.size(); ++i) { + dot += std::conj(occ_kq_[ik][m][i]) * hp[i]; + } + std::cout << "DBG = " + << dot.real() << " + i " << dot.imag() + << " (GS eig " << eig_(ikq_of_k_[ik], static_cast(m)) << ")" << std::endl; + std::cout << "DBG = " + << hamilt_->debug_t_vnl(occ_kq_[ik][m]) << std::endl; + std::cout << "DBG = " + << hamilt_->debug_v_wfc(occ_kq_[ik][m]) << std::endl; + } + } + } + for (int ib = 0; ib < nbands; ++ib) { + if (!dfpt_band_occupied(wg_, ik, ib)) { + continue; // unoccupied: no Sternheimer equation + } + std::vector> rhs = data_.get_dpsi(q_idx, ik, ib); + if (rhs.empty() || static_cast(dv_sc.size()) != nbands + || rhs.size() != dv_sc[ib].size()) { + if (dbg) { + std::cout << "DBG skip solve ik=" << ik << " ib=" << ib + << " rhs.size=" << rhs.size() + << " dv_sc.size=" << dv_sc.size() + << " dv_sc[ib].size=" << (dv_sc.size() > static_cast(ib) ? dv_sc[ib].size() : 999999) + << std::endl; + } + continue; + } + // b = -(dV_ext + dV_sc)|psi_n> + for (size_t i = 0; i < rhs.size(); ++i) { + rhs[i] = -(rhs[i] + dv_sc[ib][i]); + } + hamilt_->set_shift(eig_(ik, ib)); + std::vector> dpsi_out; + double res = 0.0; + stern_.solve(*hamilt_, occ_kq_[ik], rhs, lin_max, lin_thr, dpsi_out, res); + if (dbg) { + double nr = 0.0, nb2 = 0.0; + for (size_t i = 0; i < dpsi_out.size(); ++i) { + nr += std::norm(dpsi_out[i]); + nb2 += std::norm(rhs[i]); + } + std::cout << "DBG solve ik=" << ik << " ib=" << ib + << " eps=" << eig_(ik, ib) + << " res=" << res << " |dpsi|=" << std::sqrt(nr) + << " |rhs|=" << std::sqrt(nb2) + << " finite=" << (std::isfinite(std::sqrt(nr)) ? 1 : 0) + << std::endl; + } + data_.set_dpsi(q_idx, ik, ib, dpsi_out); + } + } + + // ---- 3. first-order density and mixing + rho_.compute_drho(gs_psi_, wg_, q_idx, data_); + rho_.mix_drho(q_idx, data_); + residual = rho_.get_residual(q_idx, data_); + if (dbg) { + std::cout << "DBG iter=" << iter << " residual=" << residual + << " conv_thr=" << conv_thr_ << std::endl; + } + converged = (residual < conv_thr_); + } + // stash the converged screened potential and dpsi of this displacement + // for the two-pass 2n+1 accumulation (term2 cross section needs + // dV_ext^b + dV_sc^b and dpsi^b of every displacement) + data_.set_vsc_r(iat, idir, v_sc_r_last); + { + std::vector>>> disp( + nk, std::vector>>(nbands)); + for (int ik = 0; ik < nk; ++ik) { + for (int ib = 0; ib < nbands; ++ib) { + disp[ik][ib] = data_.get_dpsi(q_idx, ik, ib); + } + } + data_.set_dpsi_disp(iat, idir, disp); + } + + return residual; +} + +void DFPT_PW::Impl::solve_pos_resp(int q_idx) { + // Y^a_{k,v} = P_c x_a|psi_{k,v}> through the Sternheimer equation + // (H(k) - eps_v) Y^a_v = P_c [H, x_a]|psi_v>, + // [H, x_a]|psi> = -(i/tpiba) dH/dk_a|psi> (velocity form), + // exactly the linear solve of QE dvpsi_e (whose rhs negation restores + // P_c[H,x]psi from commutator_Hx_psi's [x,H] convention). dH/dk_a is the + // pos_matrix velocity operator: the diagonal kinetic 2 tpiba^2 (k+G)_a + // plus the separable projector derivative (build_vkb/build_vkb_dk). The + // solved vector carries the complete conduction-space position response + // and replaces the empty-eigenvector-truncated r-matrix contraction. + if (!wired() || hamilt_ == nullptr) { + return; + } + const ModuleBase::Vector3 q_cart = data_.get_qvec(q_idx) * ucell_->G; + const int nk = gs_psi_.get_nk(); + const int nbands = gs_psi_.get_nbands(); + const double tpiba = ucell_->tpiba; + const double tpiba2 = tpiba * tpiba; + const int lin_max = data_.get_max_iter(); + const double lin_thr = data_.get_conv_thr(); + const bool dbg = (getenv("DFPT_DEBUG") != nullptr); + + for (int a = 0; a < 3; ++a) { + std::vector>>> yvec( + nk, std::vector>>(nbands)); + for (int ik = 0; ik < nk; ++ik) { + if (occ_kq_[ik].empty()) { + continue; // matches the displacement solve guard + } + if (last_q_ != q_idx || last_ik_ != ik) { + hamilt_->set_context(q_cart, ik); + last_q_ = q_idx; + last_ik_ = ik; + } + const int npwk = pw_wfc_->npwk[ik]; + std::vector> gk(npwk); + for (int ig = 0; ig < npwk; ++ig) { + gk[ig] = pw_wfc_->getgpluskcar(ik, ig); + } + // dH/dk_a|psi_b> for every band: diagonal kinetic part + std::vector>> vel( + nbands, + std::vector>(npwk, std::complex(0.0, 0.0))); + for (int ib = 0; ib < nbands; ++ib) { + for (int ig = 0; ig < npwk; ++ig) { + vel[ib][ig] = 2.0 * tpiba2 * gk[ig][a] * gs_psi_(ik, ib, ig); + } + } + // nonlocal derivative part (pos_matrix velocity form; NCPP + // separable projectors only) + for (int it = 0; it < ucell_->ntype; ++it) { + const pseudo& ncpp = ucell_->atoms[it].ncpp; + const int nh = ncpp.nh; + if (nh == 0) { + continue; + } + // projector -> (radial beta index, m channel) table + std::vector mu_ib(nh, 0); + std::vector mu_m(nh, 0); + int mu_idx = 0; + for (int ib = 0; ib < ncpp.nbeta; ++ib) { + const int l = ncpp.lll[ib]; + for (int m = 0; m < 2 * l + 1; ++m) { + if (mu_idx < nh) { + mu_ib[mu_idx] = ib; + mu_m[mu_idx] = m; + } + ++mu_idx; + } + } + for (int ia = 0; ia < ucell_->atoms[it].na; ++ia) { + std::vector>> vkb; + pert_.build_vkb(it, ia, gk, vkb); + // becp_b[mu] = + std::vector>> becp(nbands); + for (int b = 0; b < nbands; ++b) { + becp[b].assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int ig = 0; ig < npwk; ++ig) { + becp[b][mu] += std::conj(vkb[mu][ig]) * gs_psi_(ik, b, ig); + } + } + } + std::vector>> dvkb; + pert_.build_vkb_dk(it, ia, a, gk, vkb, dvkb); + // dbecp_b[mu] = + std::vector>> dbecp(nbands); + for (int b = 0; b < nbands; ++b) { + dbecp[b].assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int ig = 0; ig < npwk; ++ig) { + dbecp[b][mu] += std::conj(dvkb[mu][ig]) * gs_psi_(ik, b, ig); + } + } + } + // dV_nl/dk_a|psi_b> = sum_mu |dvkb_mu> (D becp_b)_mu + // + |vkb_mu> (D dbecp_b)_mu + for (int b = 0; b < nbands; ++b) { + for (int mu = 0; mu < nh; ++mu) { + std::complex out_b(0.0, 0.0); + std::complex in_b(0.0, 0.0); + for (int nu = 0; nu < nh; ++nu) { + if (mu_m[mu] != mu_m[nu]) { + continue; + } + const double dij = ncpp.dion(mu_ib[mu], mu_ib[nu]); + out_b += dij * becp[b][nu]; + in_b += dij * dbecp[b][nu]; + } + for (int ig = 0; ig < npwk; ++ig) { + vel[b][ig] += dvkb[mu][ig] * out_b + vkb[mu][ig] * in_b; + } + } + } + } + } + // solve (H - eps_v) Y = -(i/tpiba) vel for every occupied band + for (int ib = 0; ib < nbands; ++ib) { + if (!dfpt_band_occupied(wg_, ik, ib)) { + continue; + } + std::vector> rhs( + npwk, std::complex(0.0, 0.0)); + const std::complex fac(0.0, -1.0 / tpiba); + for (int ig = 0; ig < npwk; ++ig) { + rhs[ig] = fac * vel[ib][ig]; + } + hamilt_->set_shift(eig_(ik, ib)); + double res = 0.0; + stern_.solve(*hamilt_, occ_kq_[ik], rhs, lin_max, lin_thr, + yvec[ik][ib], res); + if (dbg) { + std::cout << "DBG posresp a=" << a << " ik=" << ik + << " ib=" << ib << " eps=" << eig_(ik, ib) + << " res=" << res << std::endl; + } + } + } + data_.set_pos_resp(a, yvec); + } +} + +void DFPT_PW::Impl::solve_efield_resp(int q_idx) { + // E-field SCF response (QE solve_e + dfpt_kernel form): the bare legs + // Y^a stashed by solve_pos_resp are the field rhs base and the fixed + // point adds the screened response potential of the mixed drho^E + // exactly like solve_displacement. The converged dpsi^E,a feeds the + // SCF dielectric tensor (DFPT_Q0::compute_eps) and the zstar_eu + // cross-check probe (DFPT_ALEG). + if (!wired() || hamilt_ == nullptr) { + return; + } + const ModuleBase::Vector3 q_cart = data_.get_qvec(q_idx) * ucell_->G; + const int nrxx = pw_rho_->nrxx; + const int nk = gs_psi_.get_nk(); + const int nbands = gs_psi_.get_nbands(); + const int lin_max = data_.get_max_iter(); + const double lin_thr = data_.get_conv_thr(); + + for (int a = 0; a < 3; ++a) { + const std::vector>>> yr + = data_.get_pos_resp(a); + if (static_cast(yr.size()) != nk) { + continue; // bare legs not solved: no E response either + } + rho_.reset_mixing(q_idx); + data_.set_drho_g(q_idx, 0, + std::vector>(pw_rho_->npw, + std::complex(0.0, 0.0))); + bool converged = false; + for (int iter = 0; iter < max_iter_ && !converged; ++iter) { + // screened response potential of the mixed input density + // (identical assembly to solve_displacement) + std::vector> v_sc_r(nrxx, std::complex(0.0, 0.0)); + const std::vector> drho_in_g = data_.get_drho_g(q_idx, 0); + if (!drho_in_g.empty() && static_cast(drho_in_g.size()) == pw_rho_->npw) { + std::vector> dv_ha_g; + rho_.v_hartree_q(q_cart, drho_in_g, dv_ha_g); + pw_rho_->recip2real(dv_ha_g.data(), v_sc_r.data()); + if (xc_ != nullptr) { + std::vector> a_r(nrxx); + pw_rho_->recip2real(drho_in_g.data(), a_r.data()); + std::vector> b_r; + xc_->apply(a_r, b_r); + if (static_cast(b_r.size()) == nrxx) { + for (int ir = 0; ir < nrxx; ++ir) { + v_sc_r[ir] += b_r[ir]; + } + } + } + } + for (int ik = 0; ik < nk; ++ik) { + if (static_cast(occ_kq_.size()) <= ik || occ_kq_[ik].empty()) { + continue; + } + std::vector>> dv_sc; + pert_.apply_vr(q_idx, ik, v_sc_r, gs_psi_, q_cart, dv_sc); + if (last_q_ != q_idx || last_ik_ != ik) { + hamilt_->set_context(q_cart, ik); + last_q_ = q_idx; + last_ik_ = ik; + } + for (int ib = 0; ib < nbands; ++ib) { + if (!dfpt_band_occupied(wg_, ik, ib)) { + continue; + } + if (static_cast(yr[ik][ib].size()) == 0 + || static_cast(dv_sc.size()) != nbands + || yr[ik][ib].size() != dv_sc[ib].size()) { + continue; + } + std::vector> rhs(yr[ik][ib].size()); + for (size_t i = 0; i < rhs.size(); ++i) { + rhs[i] = -(yr[ik][ib][i] + dv_sc[ib][i]); + } + hamilt_->set_shift(eig_(ik, ib)); + std::vector> dpsi_out; + double res = 0.0; + stern_.solve(*hamilt_, occ_kq_[ik], rhs, lin_max, lin_thr, + dpsi_out, res); + data_.set_dpsi(q_idx, ik, ib, dpsi_out); + } + } + rho_.compute_drho(gs_psi_, wg_, q_idx, data_); + rho_.mix_drho(q_idx, data_); + const double residual = rho_.get_residual(q_idx, data_); + converged = (residual < conv_thr_); + if (converged) { + std::cout << "DFPT efield dir=" << a + << " converged, residual=" << residual + << " (iter=" << iter << ")" << std::endl; + } + } + // stash dpsi^E,a before any later solve reuses the slots + std::vector>>> de( + nk, std::vector>>(nbands)); + for (int ik = 0; ik < nk; ++ik) { + for (int ib = 0; ib < nbands; ++ib) { + de[ik][ib] = data_.get_dpsi(q_idx, ik, ib); + } + } + data_.set_dpsi_efield(a, de); + } } void DFPT_PW::run() { - int nq = pimpl_->qlist_.get_nq(); + const int nq = pimpl_->qlist_.get_nq(); for (int q_idx = 0; q_idx < nq; ++q_idx) { - // TODO: Implement self-consistent loop for each q-point - // According to the standard DFPT workflow, the SCF loop should include: - // 1. Compute the perturbation of the screening potential - // - pimpl_->pert_.compute_screening_potential(q_idx, pimpl_->data_) - // 2. Solve the Sternheimer equation - // - pimpl_->stern_.solve(q_idx, pimpl_->data_) - // 3. Calculate the first-order density - // - pimpl_->rho_.compute_first_order(q_idx, pimpl_->data_) - // 4. Check convergence and iterate until self-consistency is achieved - // Special handling for q=0 (uniform electric field responses): // The standard position operator r is ill-defined in periodic systems. // Developers should NOT pass a conventional position matrix. Instead, // matrix elements should be computed using the well-defined periodic // commutator [Ĥ_SCF, r̂]. This is implemented in DFPT_Q0 module. - if (q_idx == 0) { - pimpl_->q0_.compute_q0_response(pimpl_->data_); + if (q_idx == 0 && pimpl_->data_.get_compute_q0()) { + pimpl_->q0_.compute_q0_response(pimpl_->data_); + } + + // occupied states at k+q for every k of this q (projector of P_c); + // also invalidates the shifted-operator context cache + if (pimpl_->wired()) { + pimpl_->build_occ_kq(q_idx); + } + + // position legs of the screened Born charges: the q = 0 Y solves + // need the projector just built and must land before the two-pass + // displacement solves below reuse the shifted-operator context + if (q_idx == 0 && pimpl_->data_.get_compute_q0() && pimpl_->wired()) { + pimpl_->solve_pos_resp(q_idx); + // SCF E-field responses of the dielectric tensor: after the + // bare Y legs they consume, before the displacement solves + // reuse the slots; the epsilon contraction runs straight after + // (QE solve_e -> dielec.f90 order) + pimpl_->solve_efield_resp(q_idx); + pimpl_->q0_.compute_eps(pimpl_->wg_, pimpl_->data_); + } + + // Per-irrep self-consistent loop: the little-group irrep + // decomposition is a placeholder until stage A, so the single + // available irrep falls back to the full 3N displacement basis. + // Ledger semantics (B4): one outer pass solves every displacement + // to its own convergence (solve_displacement restarts each from a + // zero input density), and the pass residual is the worst final + // displacement residual; the pass converges when that worst is + // below conv_thr. An unconverged pass therefore re-runs the full + // solve, bounded by max_iter_ outer passes, and the residual + // history keeps an honest record instead of the former + // unconditional single-pass convergence. + const int nirr = pimpl_->data_.get_nirr(q_idx); + for (int irrep = 0; irrep < nirr; ++irrep) { + pimpl_->data_.set_converged(q_idx, irrep, false); + pimpl_->data_.set_current_iter(q_idx, irrep, 0); + while (!pimpl_->data_.get_converged(q_idx, irrep) + && pimpl_->data_.get_current_iter(q_idx, irrep) < pimpl_->max_iter_) { + if (pimpl_->wired()) { + const int nat = pimpl_->ucell_->nat; + // two passes over the 3N displacement basis: first solve + // every displacement to convergence (the 2n+1 accumulation + // of displacement b needs the converged dpsi AND screened + // potential of every column displacement a), then run the + // 2n+1 accumulation for each + double worst = 0.0; + for (int iat = 0; iat < nat; ++iat) { + for (int idir = 0; idir < 3; ++idir) { + const double residual = pimpl_->solve_displacement(q_idx, iat, idir); + worst = std::max(worst, residual); + } + } + for (int iat = 0; iat < nat; ++iat) { + for (int idir = 0; idir < 3; ++idir) { + // 2n+1 accumulation of this converged displacement + pimpl_->phon_.accumulate_electron(q_idx, iat, idir, + pimpl_->gs_psi_, + pimpl_->wg_, + pimpl_->data_); + } + } + pimpl_->data_.add_residual(q_idx, irrep, worst); + pimpl_->data_.set_converged(q_idx, irrep, + worst < pimpl_->data_.get_conv_thr()); + } else { + // design-phase skeleton: no bases wired, converge at once + pimpl_->data_.add_residual(q_idx, irrep, 0.0); + pimpl_->data_.set_converged(q_idx, irrep, true); + } + pimpl_->data_.set_current_iter( + q_idx, irrep, pimpl_->data_.get_current_iter(q_idx, irrep) + 1); + } } - + + // screened Born charges: the Gonze-Lee 2n+1 form consumes the + // converged (screened) dpsi of every q = 0 displacement stashed by + // solve_displacement, so it must run after the two-pass solves + // above and before the LO-TO term below consumes it + if (q_idx == 0 && pimpl_->data_.get_compute_q0() && pimpl_->wired()) { + pimpl_->q0_.compute_born(pimpl_->gs_psi_, pimpl_->wg_, + pimpl_->eig_, pimpl_->data_); + } + pimpl_->phon_.assemble(q_idx, pimpl_->data_); pimpl_->phon_.diagonalize(q_idx, pimpl_->data_); + if (q_idx == 0 && pimpl_->data_.get_loto()) { + // non-analytic LO-TO correction along the data-layer direction + // (default isotropic (1,1,1)/sqrt(3) for cubic crystals; + // set_loto_dir overrides, e.g. per irrep direction in stage A) + pimpl_->phon_.add_loto(pimpl_->data_.get_loto_dir(), pimpl_->data_); + pimpl_->phon_.diagonalize_loto(pimpl_->data_); + } } } +int DFPT_PW::get_nq() const { + return pimpl_->qlist_.get_nq(); +} + +ModuleBase::Vector3 DFPT_PW::get_qvec(int q_idx) const { + return pimpl_->data_.get_qvec(q_idx); +} + std::vector DFPT_PW::get_phonon_freq(int q_idx) const { return pimpl_->data_.get_phon_freq(q_idx); } +std::vector DFPT_PW::get_phon_freq_loto() const { + return pimpl_->data_.get_phon_freq_loto(); +} + +ModuleBase::Vector3 DFPT_PW::get_loto_dir() const { + return pimpl_->data_.get_loto_dir(); +} + +std::string DFPT_PW::format_q_report(int q_idx) const { + return pimpl_->phon_.format_q_report(q_idx, pimpl_->data_); +} + +std::string DFPT_PW::format_loto_report() const { + return pimpl_->phon_.format_loto_report(pimpl_->data_); +} + ModuleBase::matrix DFPT_PW::get_dielectric_tensor() const { return pimpl_->data_.get_dielectric(); } @@ -108,8 +905,8 @@ ModuleBase::matrix DFPT_PW::get_born_charges(int atom_idx) const { return pimpl_->data_.get_born(atom_idx); } -void DFPT_PW::set_parameters(const std::string& param_file) { - (void)param_file; +void DFPT_PW::set_qfile(const std::string& filename) { + pimpl_->qfile_ = filename; } void DFPT_PW::set_qmesh(int nqx, int nqy, int nqz) { @@ -128,4 +925,22 @@ void DFPT_PW::set_max_iter(int max_iter) { pimpl_->data_.set_max_iter(max_iter); } -} // namespace ModuleDFPT \ No newline at end of file +void DFPT_PW::set_mix_beta(double beta) { + if (beta > 0.0 && beta <= 1.0) { + pimpl_->mix_beta_ = beta; + } +} + +void DFPT_PW::set_compute_q0(bool flag) { + pimpl_->data_.set_compute_q0(flag); +} + +void DFPT_PW::set_loto(bool flag) { + pimpl_->data_.set_loto(flag); +} + +void DFPT_PW::set_loto_dir(const ModuleBase::Vector3& dir) { + pimpl_->data_.set_loto_dir(dir); +} + +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_pw.h b/source/source_pw/module_dfpt/dfpt_pw.h index b6ddee13eb4..59a2dbb1fdb 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.h +++ b/source/source_pw/module_dfpt/dfpt_pw.h @@ -1,6 +1,6 @@ // ============================================================ // This code is added by Mohan Chen on 2026-05-18. -// This code is currently in the design phase and has not been +// This code is currently in design phase and has not been // put into production yet. It may change in the future. // Please use this code with caution. Only developers who know // what they are doing should use this code. @@ -9,37 +9,112 @@ #ifndef DFPT_PW_H #define DFPT_PW_H -#include -#include +#include "source_base/matrix.h" +#include "source_base/vector3.h" #include "source_cell/unitcell.h" #include "source_psi/psi.h" +#include +#include + +class Plus_U_Base; +class Structure_Factor; + +namespace ModulePW { +class PW_Basis; +class PW_Basis_K; +} + namespace ModuleDFPT { +class XC_First_Order; + +/** + * @brief Density-functional perturbation theory driver (plane waves). + * + * C7 wiring: init receives the converged ground state (psi, wg, eig), the + * shared-grid plane-wave bases, the real-space effective potential and the + * first-order XC kernel contract; run() then drives, per irreducible q, the + * per-displacement self-consistent Sternheimer cycle + * build_dv (bare external) + * -> [ dv_sc = v_hartree_q(drho_in) + xc_->apply(drho_in) + * -> Sternheimer solve of (H(k+q) - eps_n) P_c dpsi = -P_c (dV_ext + * + dV_sc)|psi_n> with the k+q occupied states as the projector + * -> compute_drho -> mix_drho ]* + * -> accumulate_electron -> assemble -> diagonalize (+ LO-TO at q = 0). + * With null bases (design-phase skeleton) run() keeps the documented + * first-iteration-converged fallback of the irrep bookkeeping loop. + */ class DFPT_PW { public: DFPT_PW(); ~DFPT_PW(); - - void init(UnitCell& ucell, const psi::Psi>& psi, - double nelec, double ecutwfc); - + + void init(UnitCell& ucell, const psi::Psi>& psi, + ModulePW::PW_Basis* pw_rho, ModulePW::PW_Basis_K* pw_wfc, + Structure_Factor* sf, const std::vector& veff_r, + const ModuleBase::matrix& wg, const ModuleBase::matrix& eig, + const XC_First_Order* xc, + double nelec, double ecutwfc, const Plus_U_Base* dftu); + void run(); - + + /// DFT+U reservation accessors (U0): with_u() reports whether a DFT+U + /// provider is wired (dft_plus_u enabled upstream); u_active() further + /// requires the provider to be usable (occupation matrices initialized). + /// init() rejects a wired provider explicitly: every DFPT U hook is a + /// no-op reservation, so a DFT+U ground state must not run DFPT until + /// the first-order U response (U1) is implemented. + bool get_with_u() const; + bool get_u_active() const; + std::vector get_phonon_freq(int q_idx) const; - + ModuleBase::matrix get_dielectric_tensor() const; - + ModuleBase::matrix get_born_charges(int atom_idx) const; - - void set_parameters(const std::string& param_file); - + + /// q-point source: a q list file overrides the Monkhorst-Pack q mesh + void set_qfile(const std::string& filename); + void set_qmesh(int nqx, int nqy, int nqz); - + void set_conv_thr(double thr); - + void set_max_iter(int max_iter); - + + void set_mix_beta(double beta); + + /// q = 0 response switches (epsilon_inf / Born charges / LO-TO) + void set_compute_q0(bool flag); + + void set_loto(bool flag); + + /// q->0 direction of the non-analytic (LO-TO) term: any non-null vector + /// is normalized to a unit direction by the data layer; the default is + /// the isotropic (1,1,1)/sqrt(3). Consumed by run() so no direction is + /// hardcoded in the driver anymore. + void set_loto_dir(const ModuleBase::Vector3& dir); + + /// number of q points of the current list (q file or q mesh) + int get_nq() const; + + /// direct (reciprocal-lattice fractional) coordinates of q_idx + ModuleBase::Vector3 get_qvec(int q_idx) const; + + ModuleBase::Vector3 get_loto_dir() const; + + /// signed Gamma frequencies (cm^-1) after the LO-TO correction; empty + /// when loto is off or the correction has not run + std::vector get_phon_freq_loto() const; + + /// formatted per-q frequency report (deterministic layout, pinned by + /// the serial format regression test); the LO-TO variant is empty when + /// no corrected frequencies are available + std::string format_q_report(int q_idx) const; + + std::string format_loto_report() const; + private: class Impl; Impl* pimpl_; @@ -47,4 +122,4 @@ class DFPT_PW { } // namespace ModuleDFPT -#endif // DFPT_PW_H \ No newline at end of file +#endif // DFPT_PW_H diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index b0cb27c498f..fc8bea0d747 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -7,6 +7,9 @@ // ============================================================ #include "dfpt_pw_data.h" +#include "source_pw/module_pwdft/dftu_base.h" + +#include namespace ModuleDFPT { @@ -17,7 +20,7 @@ DFPT_PW_Data::~DFPT_PW_Data() { } void DFPT_PW_Data::init(ModuleCell::QList* qlist, int nk, int nbands, int npw_max, - int nrxx, int nspin, int nat) { + int nrxx, int nspin, int nat, const Plus_U_Base* dftu) { qlist_ = qlist; nk_ = nk; nbands_ = nbands; @@ -25,6 +28,7 @@ void DFPT_PW_Data::init(ModuleCell::QList* qlist, int nk, int nbands, int npw_ma nrxx_ = nrxx; nspin_ = nspin; nat_ = nat; + dftu_ = dftu; allocate_memory(); is_initialized_ = true; @@ -35,6 +39,116 @@ void DFPT_PW_Data::clean() { is_initialized_ = false; } +bool DFPT_PW_Data::u_active() const { + // a usable provider has its occupation matrices initialized (the ground + // state does this when DFT+U actually runs); a wired provider without + // them (e.g. a default-constructed reservation) stays inactive. + return with_u() && dftu_->is_occ_mat_initialized(); +} + +void DFPT_PW_Data::set_docc(int q_idx, const std::vector>& occ) { + if (q_idx < 0) { + return; + } + if (q_idx >= static_cast(docc_.size())) { + docc_.resize(q_idx + 1); + } + docc_[q_idx] = occ; +} + +void DFPT_PW_Data::set_vsc_r(int atom_idx, int dir, + const std::vector>& v) { + if (atom_idx < 0 || dir < 0 || dir >= 3) { + return; + } + const size_t slot = static_cast(3 * atom_idx + dir); + if (slot >= vsc_r_.size()) { + vsc_r_.resize(slot + 1); + } + vsc_r_[slot] = v; +} + +std::vector> DFPT_PW_Data::get_vsc_r(int atom_idx, int dir) const { + if (atom_idx < 0 || dir < 0 || dir >= 3) { + return std::vector>(); + } + const size_t slot = static_cast(3 * atom_idx + dir); + if (slot < vsc_r_.size()) { + return vsc_r_[slot]; + } + return std::vector>(); +} + +void DFPT_PW_Data::set_dpsi_disp( + int atom_idx, int dir, + const std::vector>>>& d) { + if (atom_idx < 0 || dir < 0 || dir >= 3) { + return; + } + const size_t slot = static_cast(3 * atom_idx + dir); + if (slot >= dpsi_disp_.size()) { + dpsi_disp_.resize(slot + 1); + } + dpsi_disp_[slot] = d; +} + +std::vector>>> +DFPT_PW_Data::get_dpsi_disp(int atom_idx, int dir) const { + if (atom_idx < 0 || dir < 0 || dir >= 3) { + return std::vector>>>(); + } + const size_t slot = static_cast(3 * atom_idx + dir); + if (slot < dpsi_disp_.size()) { + return dpsi_disp_[slot]; + } + return std::vector>>>(); +} + +void DFPT_PW_Data::set_pos_resp( + int dir, const std::vector>>>& y) { + if (dir < 0 || dir >= 3) { + return; + } + if (pos_resp_.size() < 3) { + pos_resp_.resize(3); + } + pos_resp_[dir] = y; +} + +std::vector>>> +DFPT_PW_Data::get_pos_resp(int dir) const { + if (dir < 0 || dir >= 3 || dir >= static_cast(pos_resp_.size())) { + return std::vector>>>(); + } + return pos_resp_[dir]; +} + +void DFPT_PW_Data::set_dpsi_efield( + int dir, const std::vector>>>& d) { + if (dir < 0 || dir >= 3) { + return; + } + if (dpsi_efield_.size() < 3) { + dpsi_efield_.resize(3); + } + dpsi_efield_[dir] = d; +} + +std::vector>>> +DFPT_PW_Data::get_dpsi_efield(int dir) const { + if (dir < 0 || dir >= 3 || dir >= static_cast(dpsi_efield_.size())) { + return std::vector>>>(); + } + return dpsi_efield_[dir]; +} + +std::vector> DFPT_PW_Data::get_docc(int q_idx) const { + if (q_idx >= 0 && q_idx < static_cast(docc_.size())) { + return docc_[q_idx]; + } + return std::vector>(); +} + int DFPT_PW_Data::get_nq() const { return qlist_->get_nq(); } @@ -53,73 +167,176 @@ std::vector DFPT_PW_Data::get_irrep_modes(int q_idx, int irrep) const { void DFPT_PW_Data::set_dpsi(int q_idx, int k_idx, int band_idx, const std::vector>& psi) { - (void)q_idx; - (void)k_idx; - (void)band_idx; - (void)psi; + if (q_idx < 0 || k_idx < 0 || band_idx < 0) { + return; + } + if (q_idx >= static_cast(dpsi_.size())) { + dpsi_.resize(q_idx + 1); + } + if (k_idx >= static_cast(dpsi_[q_idx].size())) { + dpsi_[q_idx].resize(k_idx + 1); + } + if (band_idx >= static_cast(dpsi_[q_idx][k_idx].size())) { + dpsi_[q_idx][k_idx].resize(band_idx + 1); + } + dpsi_[q_idx][k_idx][band_idx] = psi; } std::vector> DFPT_PW_Data::get_dpsi(int q_idx, int k_idx, int band_idx) const { - (void)q_idx; - (void)k_idx; - (void)band_idx; + if (q_idx >= 0 && k_idx >= 0 && band_idx >= 0 && + q_idx < static_cast(dpsi_.size()) && + k_idx < static_cast(dpsi_[q_idx].size()) && + band_idx < static_cast(dpsi_[q_idx][k_idx].size())) + { + return dpsi_[q_idx][k_idx][band_idx]; + } return std::vector>(); } -psi::Psi>& DFPT_PW_Data::get_dpsi_obj(int q_idx) { - static psi::Psi> dummy; - (void)q_idx; - return dummy; +void DFPT_PW_Data::set_converged(int q_idx, int irrep, bool flag) { + converged_[std::make_pair(q_idx, irrep)] = flag; +} + +bool DFPT_PW_Data::get_converged(int q_idx, int irrep) const { + const auto it = converged_.find(std::make_pair(q_idx, irrep)); + return it != converged_.end() ? it->second : false; +} + +void DFPT_PW_Data::add_residual(int q_idx, int irrep, double r) { + residuals_[std::make_pair(q_idx, irrep)].push_back(r); +} + +std::vector DFPT_PW_Data::get_residuals(int q_idx, int irrep) const { + const auto it = residuals_.find(std::make_pair(q_idx, irrep)); + return it != residuals_.end() ? it->second : std::vector(); +} + +void DFPT_PW_Data::set_current_iter(int q_idx, int irrep, int iter) { + current_iter_[std::make_pair(q_idx, irrep)] = iter; +} + +int DFPT_PW_Data::get_current_iter(int q_idx, int irrep) const { + const auto it = current_iter_.find(std::make_pair(q_idx, irrep)); + return it != current_iter_.end() ? it->second : 0; } void DFPT_PW_Data::set_drho_r(int q_idx, int spin, const std::vector& rho) { - (void)q_idx; - (void)spin; - (void)rho; + if (q_idx < 0 || spin < 0) { return; } + if (q_idx >= static_cast(drho_r_.size())) { + drho_r_.resize(q_idx + 1); + } + if (spin >= static_cast(drho_r_[q_idx].size())) { + drho_r_[q_idx].resize(spin + 1); + } + drho_r_[q_idx][spin] = rho; } std::vector DFPT_PW_Data::get_drho_r(int q_idx, int spin) const { - (void)q_idx; - (void)spin; + if (q_idx >= 0 && spin >= 0 && + q_idx < static_cast(drho_r_.size()) && + spin < static_cast(drho_r_[q_idx].size())) + { + return drho_r_[q_idx][spin]; + } return std::vector(); } void DFPT_PW_Data::set_drho_g(int q_idx, int spin, const std::vector>& rho) { - (void)q_idx; - (void)spin; - (void)rho; + if (q_idx < 0 || spin < 0) { return; } + if (q_idx >= static_cast(drho_g_.size())) { + drho_g_.resize(q_idx + 1); + } + if (spin >= static_cast(drho_g_[q_idx].size())) { + drho_g_[q_idx].resize(spin + 1); + } + drho_g_[q_idx][spin] = rho; } std::vector> DFPT_PW_Data::get_drho_g(int q_idx, int spin) const { - (void)q_idx; - (void)spin; + if (q_idx >= 0 && spin >= 0 && + q_idx < static_cast(drho_g_.size()) && + spin < static_cast(drho_g_[q_idx].size())) + { + return drho_g_[q_idx][spin]; + } return std::vector>(); } void DFPT_PW_Data::set_dv_r(int q_idx, int spin, const std::vector& v) { - (void)q_idx; - (void)spin; - (void)v; + if (q_idx < 0 || spin < 0) { return; } + if (q_idx >= static_cast(dv_r_.size())) { + dv_r_.resize(q_idx + 1); + } + if (spin >= static_cast(dv_r_[q_idx].size())) { + dv_r_[q_idx].resize(spin + 1); + } + dv_r_[q_idx][spin] = v; } std::vector DFPT_PW_Data::get_dv_r(int q_idx, int spin) const { - (void)q_idx; - (void)spin; + if (q_idx >= 0 && spin >= 0 && + q_idx < static_cast(dv_r_.size()) && + spin < static_cast(dv_r_[q_idx].size())) + { + return dv_r_[q_idx][spin]; + } return std::vector(); } -void DFPT_PW_Data::set_dynmat(int q_idx, const ModuleBase::matrix& dm) { +void DFPT_PW_Data::set_dv_recip_c(int q_idx, int spin, const std::vector>& v) { + if (q_idx < 0 || spin < 0) { return; } + if (q_idx >= static_cast(dv_recip_c_.size())) { + dv_recip_c_.resize(q_idx + 1); + } + if (spin >= static_cast(dv_recip_c_[q_idx].size())) { + dv_recip_c_[q_idx].resize(spin + 1); + } + dv_recip_c_[q_idx][spin] = v; +} + +std::vector> DFPT_PW_Data::get_dv_recip_c(int q_idx, int spin) const { + if (q_idx >= 0 && spin >= 0 && + q_idx < static_cast(dv_recip_c_.size()) && + spin < static_cast(dv_recip_c_[q_idx].size())) + { + return dv_recip_c_[q_idx][spin]; + } + return std::vector>(); +} + +void DFPT_PW_Data::set_dv_rc(int q_idx, int spin, const std::vector>& v) { + if (q_idx < 0 || spin < 0) { return; } + if (q_idx >= static_cast(dv_rc_.size())) { + dv_rc_.resize(q_idx + 1); + } + if (spin >= static_cast(dv_rc_[q_idx].size())) { + dv_rc_[q_idx].resize(spin + 1); + } + dv_rc_[q_idx][spin] = v; +} + +std::vector> DFPT_PW_Data::get_dv_rc(int q_idx, int spin) const { + if (q_idx >= 0 && spin >= 0 && + q_idx < static_cast(dv_rc_.size()) && + spin < static_cast(dv_rc_[q_idx].size())) + { + return dv_rc_[q_idx][spin]; + } + return std::vector>(); +} + +void DFPT_PW_Data::set_dynmat(int q_idx, const ModuleBase::ComplexMatrix& dm) { if (q_idx >= static_cast(dynmat_.size())) { dynmat_.resize(q_idx + 1); } dynmat_[q_idx] = dm; } -ModuleBase::matrix DFPT_PW_Data::get_dynmat(int q_idx) const { +ModuleBase::ComplexMatrix DFPT_PW_Data::get_dynmat(int q_idx) const { if (q_idx < static_cast(dynmat_.size())) { return dynmat_[q_idx]; } - return ModuleBase::matrix(); + return ModuleBase::ComplexMatrix(); } void DFPT_PW_Data::set_phon_freq(int q_idx, const std::vector& freq) { @@ -136,6 +353,14 @@ std::vector DFPT_PW_Data::get_phon_freq(int q_idx) const { return std::vector(); } +void DFPT_PW_Data::set_loto_dir(const ModuleBase::Vector3& dir) { + const double norm = std::sqrt(dir * dir); + if (norm < 1.0e-10) { + return; // keep the current direction on a null input + } + loto_dir_ = dir / norm; +} + void DFPT_PW_Data::set_dielectric(const ModuleBase::matrix& eps) { dielectric_ = eps; } @@ -168,7 +393,14 @@ void DFPT_PW_Data::deallocate_memory() { dynmat_.clear(); phon_freq_.clear(); born_.clear(); + docc_.clear(); + dv_r_.clear(); + dv_recip_c_.clear(); + dv_rc_.clear(); + dpsi_.clear(); + converged_.clear(); residuals_.clear(); + current_iter_.clear(); } } // namespace ModuleDFPT \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index 64aaa0bd0db..6ad7656ec18 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -10,21 +10,43 @@ #define DFPT_PW_DATA_H #include "source_base/matrix.h" +#include "source_base/complexmatrix.h" #include "source_base/vector3.h" #include "source_psi/psi.h" #include "source_cell/qlist.h" +#include +#include #include #include +class Plus_U_Base; + namespace ModuleDFPT { +/// Occupied-band classifier shared by the projector build, the Sternheimer +/// driver, the response-density accumulation and the 2n+1 assembly. A band +/// counts as occupied iff its weight exceeds half of the per-k full +/// reference (band 0 is always the deepest, fully occupied band). A fixed +/// absolute threshold makes the Sternheimer projector jump between k +/// samplings: a smeared Fermi-tail band with weight ~1e-6 lands on either +/// side of 1e-8 depending on where the sampling's Fermi level sits, which +/// opens or closes its empty-state channel in (H-eps)^-1 and changes the +/// converged response at the percent level. The majority criterion keeps +/// that channel open for tail bands (their occupied-type contribution is +/// f-weighted and negligible), reproducing the insulator limit that +/// finite-difference references follow. +inline bool dfpt_band_occupied(const ModuleBase::matrix& wg, int ik, int ib) +{ + return wg(ik, ib) > 0.5 * wg(ik, 0); +} + class DFPT_PW_Data { public: DFPT_PW_Data(); ~DFPT_PW_Data(); void init(ModuleCell::QList* qlist, int nk, int nbands, int npw_max, - int nrxx, int nspin, int nat); + int nrxx, int nspin, int nat, const Plus_U_Base* dftu); void clean(); @@ -33,10 +55,9 @@ class DFPT_PW_Data { int get_nirr(int q_idx) const; std::vector get_irrep_modes(int q_idx, int irrep) const; - void set_dpsi(int q_idx, int k_idx, int band_idx, + void set_dpsi(int q_idx, int k_idx, int band_idx, const std::vector>& psi); std::vector> get_dpsi(int q_idx, int k_idx, int band_idx) const; - psi::Psi>& get_dpsi_obj(int q_idx); void set_drho_r(int q_idx, int spin, const std::vector& rho); std::vector get_drho_r(int q_idx, int spin) const; @@ -46,8 +67,23 @@ class DFPT_PW_Data { void set_dv_r(int q_idx, int spin, const std::vector& v); std::vector get_dv_r(int q_idx, int spin) const; - void set_dynmat(int q_idx, const ModuleBase::matrix& dm); - ModuleBase::matrix get_dynmat(int q_idx) const; + /// First-order perturbation potential dV stored as complex plane-wave + /// coefficients (indexed by the rho-grid ig) and as the corresponding + /// complex real-space array on the shared FFT grid (C1). + /// The reciprocal coefficients already carry the -i(Delta+q) prefactor + /// and the atomic phase exp(i(Delta+q).tau); the real-space array is + /// their inverse Fourier transform, so that dV.psi is a plain cyclic + /// convolution on the shared grid (apply_dv needs no extra q-phase). + void set_dv_recip_c(int q_idx, int spin, const std::vector>& v); + std::vector> get_dv_recip_c(int q_idx, int spin) const; + void set_dv_rc(int q_idx, int spin, const std::vector>& v); + std::vector> get_dv_rc(int q_idx, int spin) const; + + /// The dynamical matrix at a generic q is complex Hermitian; stored as a + /// ModuleBase::ComplexMatrix (C5), consumed by DFPT_Phon::diagonalize + /// through the LapackConnector::zheev wrapper. + void set_dynmat(int q_idx, const ModuleBase::ComplexMatrix& dm); + ModuleBase::ComplexMatrix get_dynmat(int q_idx) const; void set_phon_freq(int q_idx, const std::vector& freq); std::vector get_phon_freq(int q_idx) const; @@ -60,6 +96,28 @@ class DFPT_PW_Data { bool get_compute_q0() const { return compute_q0_; } void set_loto(bool flag) { loto_ = flag; } bool get_loto() const { return loto_; } + + /// q->0 direction of the non-analytic (LO-TO) term, as a unit vector. + /// The setter normalizes; a null vector falls back to the isotropic + /// default (1,1,1)/sqrt(3) (documented cubic-crystal default; a general + /// direction control arrives with the irrep machinery of stage A). + void set_loto_dir(const ModuleBase::Vector3& dir); + ModuleBase::Vector3 get_loto_dir() const { return loto_dir_; } + + /// signed Gamma frequencies (cm^-1) after the non-analytic LO-TO term + /// along loto_dir_; empty until add_loto + diagonalize_loto have run + void set_phon_freq_loto(const std::vector& freq) { phon_freq_loto_ = freq; } + std::vector get_phon_freq_loto() const { return phon_freq_loto_; } + + /// The perturbation currently being solved: displacement of which linear + /// atom index (over all atoms) and along which cartesian direction. + /// Set by DFPT_Pert::build_dv and consumed by DFPT_Pert::apply_dv so the + /// Stern solver can keep applying the same perturbation per irrep without + /// re-passing (atom,dir) on every matrix-vector product. + void set_pert_atom(int atom_idx) { pert_atom_ = atom_idx; } + int get_pert_atom() const { return pert_atom_; } + void set_pert_dir(int dir) { pert_dir_ = dir; } + int get_pert_dir() const { return pert_dir_; } void set_is_metal(bool flag) { is_metal_ = flag; } bool get_is_metal() const { return is_metal_; } @@ -70,14 +128,74 @@ class DFPT_PW_Data { int get_max_iter() const { return max_iter_; } void set_conv_thr(double thr) { conv_thr_ = thr; } double get_conv_thr() const { return conv_thr_; } - void set_current_iter(int iter) { current_iter_ = iter; } - int get_current_iter() const { return current_iter_; } - void set_converged(bool flag) { converged_ = flag; } - bool get_converged() const { return converged_; } + + /// Per-(q, irrep) SCF convergence ledger (B4: sunk from the retired + /// DFPT_IrrepData adapter). The irrep dimension is a stage-A slot: + /// today the single fallback irrep 0 carries the full 3N displacement + /// basis, and DFPT_PW::run records one ledger entry per outer SCF pass + /// (worst displacement residual of the pass); missing keys read as + /// not-converged / empty history / iteration 0. + void set_converged(int q_idx, int irrep, bool flag); + bool get_converged(int q_idx, int irrep) const; + void add_residual(int q_idx, int irrep, double r); + std::vector get_residuals(int q_idx, int irrep) const; + void set_current_iter(int q_idx, int irrep, int iter); + int get_current_iter(int q_idx, int irrep) const; - void add_residual(double r) { residuals_.push_back(r); } - std::vector get_residuals() const { return residuals_; } + /// DFT+U interface reservation (U0): + /// the DFPT modules never read global input state directly; the esolver + /// layer decides whether DFT+U is active and passes a non-null + /// Plus_U_Base* only then. + /// with_u(): a DFT+U provider is wired (dft_plus_u enabled upstream). + /// u_active(): the provider is additionally usable (occupation matrices + /// initialized, which the ground state does when DFT+U + /// actually runs; a provider without them stays inactive). + bool with_u() const { return dftu_ != nullptr; } + bool u_active() const; + const Plus_U_Base* get_dftu() const { return dftu_; } + /// first-order occupation matrix (docc) storage, indexed by q. + /// lazy allocation: unset / out-of-range reads return an empty vector. + void set_docc(int q_idx, const std::vector>& occ); + std::vector> get_docc(int q_idx) const; + + /// converged screened response potential of displacement (atom, dir): + /// the real-space q-shifted complex amplitude v_sc used by the last + /// Sternheimer iteration of that displacement. The 2n+1 accumulation + /// needs it to complete the term2 cross section + /// 2 (screening channel). + void set_vsc_r(int atom_idx, int dir, + const std::vector>& v); + std::vector> get_vsc_r(int atom_idx, int dir) const; + + /// converged dpsi of displacement (atom, dir), indexed [k][band]; the + /// two-pass 2n+1 accumulation reads it back after all displacements of + /// the basis have been solved (the working dpsi slots get overwritten by + /// later solves). + void set_dpsi_disp(int atom_idx, int dir, + const std::vector>>>& d); + std::vector>>> + get_dpsi_disp(int atom_idx, int dir) const; + + /// conduction-projected position operator P_c r_dir |u_(k,band)> of the + /// q = 0 mesh, solved exactly as a linear response ((H - eps_band) Y = + /// -(i/tpiba) dH/dk_dir |u>), indexed [dir][k][band]; the screened Born + /// charge contraction avoids the empty-eigenvector + /// truncation of the explicit r-matrix sum + void set_pos_resp(int dir, + const std::vector>>>& y); + std::vector>>> + get_pos_resp(int dir) const; + + /// converged screened E-field response dpsi^E(dir) of the q = 0 mesh + /// (QE solve_e + dfpt_kernel fixed point on the rhs + /// -(Y^dir + dV_sc^E|psi>)), indexed [dir][k][band] + void set_dpsi_efield( + int dir, + const std::vector>>>& d); + std::vector>>> + get_dpsi_efield(int dir) const; + private: ModuleCell::QList* qlist_ = nullptr; @@ -88,29 +206,60 @@ class DFPT_PW_Data { int nspin_ = 1; int nat_ = 0; - std::vector>> dpsi_; + /// first-order wavefunction response, indexed [q][k][band]; each entry is + /// the dpsi on the k+q basis for that band (a vector of complex coefficients). + std::vector>>>> dpsi_; std::vector>> drho_r_; std::vector>>> drho_g_; std::vector>> dv_r_; - std::vector dynmat_; + std::vector>>> dv_recip_c_; + std::vector>>> dv_rc_; + + std::vector dynmat_; std::vector> phon_freq_; bool compute_q0_ = false; bool loto_ = false; + ModuleBase::Vector3 loto_dir_{1.0 / std::sqrt(3.0), + 1.0 / std::sqrt(3.0), + 1.0 / std::sqrt(3.0)}; + std::vector phon_freq_loto_; + int pert_atom_ = -1; + int pert_dir_ = -1; ModuleBase::matrix dielectric_; std::vector born_; bool is_metal_ = false; double dmu_ = 0.0; + /// DFT+U reservation state (U0) + const Plus_U_Base* dftu_ = nullptr; + std::vector>> docc_; + + /// converged v_sc per displacement (atom, dir): [3*nat] entries + std::vector>> vsc_r_; + + /// converged dpsi per displacement (atom, dir): [3*nat][k][band] entries + std::vector>>>> dpsi_disp_; + + /// conduction-projected position response P_c r_dir|u> per direction: + /// [3][k][band] entries + std::vector>>>> pos_resp_; + + /// converged E-field response dpsi^E per direction: [3][k][band] + std::vector>>>> + dpsi_efield_; + int max_iter_ = 100; double conv_thr_ = 1e-8; - int current_iter_ = 0; - bool converged_ = false; - std::vector residuals_; + + ///< per-(q, irrep) SCF ledger (B4: absorbed from DFPT_IrrepData) + std::map, bool> converged_; + std::map, std::vector> residuals_; + std::map, int> current_iter_; bool is_initialized_ = false; diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index 89e60e3edbc..25212f50f67 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -1,6 +1,6 @@ // ============================================================ // This code is added by Mohan Chen on 2026-05-18. -// This code is currently in the design phase and has not been +// This code is currently in design phase and has not been // put into production yet. It may change in the future. // Please use this code with caution. Only developers who know // what they are doing should use this code. @@ -8,39 +8,492 @@ #include "dfpt_q0.h" +#include "dfpt_pert.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" + +#include +#include +#include +#include +#include + namespace ModuleDFPT { DFPT_Q0::DFPT_Q0() {} DFPT_Q0::~DFPT_Q0() {} -void DFPT_Q0::init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, - ModulePW::PW_Basis_K* pw_wfc) { +void DFPT_Q0::init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, + ModulePW::PW_Basis_K* pw_wfc, DFPT_Pert* pert) { ucell_ = &ucell; pw_rho_ = pw_rho; pw_wfc_ = pw_wfc; + pert_ = pert; + stars_.clear(); } -void DFPT_Q0::compute_eps(const psi::Psi>& psi, - const ModuleBase::matrix& wg, DFPT_PW_Data& data) { - (void)psi; - (void)wg; - (void)data; +namespace { +// element accessor for ModuleBase::Matrix3 (row i, column j); the public +// interface only exposes the named e11..e33 members +inline double me(const ModuleBase::Matrix3& m, int i, int j) { + switch (3 * i + j) { + case 0: return m.e11; + case 1: return m.e12; + case 2: return m.e13; + case 3: return m.e21; + case 4: return m.e22; + case 5: return m.e23; + case 6: return m.e31; + case 7: return m.e32; + default: return m.e33; + } } -void DFPT_Q0::compute_born(const psi::Psi>& psi, DFPT_PW_Data& data) { - (void)psi; - (void)data; +// folded fractional equality with the lattice periodicity absorbed +inline bool folded_equal(double a, double b, double tol) { + const double d = std::abs(a - b); + return d < tol || std::abs(d - 1.0) < tol; } +} // namespace -void DFPT_Q0::compute_q0_response(DFPT_PW_Data& data) { - (void)data; +void DFPT_Q0::build_stars(int nk) { + // every k starts with the identity member (also the permanent fallback) + stars_.assign(nk, std::vector(1, StarMember())); + if (ucell_ == nullptr || pw_wfc_ == nullptr || pw_wfc_->kvec_d == nullptr) { + return; + } + const ModuleSymmetry::Symmetry& symm = ucell_->symm; + if (symm.nrotk <= 0) { + // no point-group analysis (symmetry off / unreduced mesh): the + // stored list is already the full mesh, identity members only + return; + } + const int nat = ucell_->nat; + std::vector> kfolds; + for (int ik = 0; ik < nk; ++ik) { + kfolds.clear(); + // the pre-filled identity member owns the folded k itself + ModuleBase::Vector3 k0 = pw_wfc_->kvec_d[ik]; + k0.x -= std::round(k0.x); + k0.y -= std::round(k0.y); + k0.z -= std::round(k0.z); + kfolds.push_back(k0); + for (int j = 0; j < symm.nrotk; ++j) { + ModuleBase::Vector3 kp = pw_wfc_->kvec_d[ik] * symm.kgmatrix[j]; + // fold to [-0.5, 0.5): star members are grid points, the + // folded coordinates identify the distinct mesh points + kp.x -= std::round(kp.x); + kp.y -= std::round(kp.y); + kp.z -= std::round(kp.z); + bool dup = false; + for (size_t im = 0; im < kfolds.size(); ++im) { + if (folded_equal(kp.x, kfolds[im].x, 1.0e-5) + && folded_equal(kp.y, kfolds[im].y, 1.0e-5) + && folded_equal(kp.z, kfolds[im].z, 1.0e-5)) { + dup = true; + break; + } + } + if (dup) { + continue; + } + kfolds.push_back(kp); + StarMember mem; + // cartesian form of the same operation: k_frac' = k_frac * K, + // k_cart = k_frac * G, hence k_cart' = k_cart * (G^-1 K G). That + // product is the row-convention operator; rotate_tensor applies + // the column form chi' = R chi R^T, so store the transpose + const ModuleBase::Matrix3 krow + = ucell_->G.Inverse() * symm.kgmatrix[j] * ucell_->G; + mem.cart = ModuleBase::Matrix3(krow.e11, krow.e21, krow.e31, + krow.e12, krow.e22, krow.e32, + krow.e13, krow.e23, krow.e33); + // atom image under the paired direct-space operation + mem.atom_map.assign(nat, -1); + bool ok = true; + for (int iat = 0; iat < nat && ok; ++iat) { + const int it = ucell_->iat2it[iat]; + const int ia = ucell_->iat2ia[iat]; + ModuleBase::Vector3 tp + = ucell_->atoms[it].taud[ia] * symm.gmatrix[j] + symm.gtrans[j]; + tp.x -= std::floor(tp.x); + tp.y -= std::floor(tp.y); + tp.z -= std::floor(tp.z); + for (int jat = 0; jat < nat; ++jat) { + if (ucell_->iat2it[jat] != it) { + continue; // a species maps onto itself + } + const int ja = ucell_->iat2ia[jat]; + const ModuleBase::Vector3& tq + = ucell_->atoms[it].taud[ja]; + if (folded_equal(tp.x, tq.x, 1.0e-4) + && folded_equal(tp.y, tq.y, 1.0e-4) + && folded_equal(tp.z, tq.z, 1.0e-4)) { + mem.atom_map[iat] = jat; + break; + } + } + if (mem.atom_map[iat] < 0) { + ok = false; + } + } + if (!ok) { + // inconsistent operation set: fall back to identity-only + // stars for every k (the unreduced-sum behavior) + stars_.assign(nk, std::vector(1, StarMember())); + return; + } + stars_[ik].push_back(mem); + } + } +} + +void DFPT_Q0::rotate_tensor(const ModuleBase::Matrix3& r, + const ModuleBase::matrix& chi, + double (&chi_rot)[9]) { + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + double s = 0.0; + for (int ap = 0; ap < 3; ++ap) { + for (int bp = 0; bp < 3; ++bp) { + s += me(r, a, ap) * me(r, b, bp) * chi(ap, bp); + } + } + chi_rot[3 * a + b] = s; + } + } } void DFPT_Q0::pos_matrix(const psi::Psi>& psi, - std::vector>>>& r_mat) { - (void)psi; - (void)r_mat; + const ModuleBase::matrix& eig, + std::vector>>>>& r_mat) { + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + r_mat.assign(nk, + std::vector>>>( + nbands, + std::vector>>( + nbands, ModuleBase::Vector3>(0.0, 0.0, 0.0)))); + if (pw_wfc_ == nullptr || ucell_ == nullptr || pert_ == nullptr) { + return; + } + const double tpiba = ucell_->tpiba; + const double tpiba2 = tpiba * tpiba; + for (int ik = 0; ik < nk; ++ik) { + const int npwk = pw_wfc_->npwk[ik]; + std::vector> gk(npwk); + for (int ig = 0; ig < npwk; ++ig) { + gk[ig] = pw_wfc_->getgpluskcar(ik, ig); + } + // velocity operator dH/dk matrix elements, with the k derivative in + // the same dimensionless 2*pi/lat0 units build_vkb_dk uses: + // p^d_{mn} = + // V_loc is k-independent; the DFT+U commutator is the U0 reservation. + std::vector>>> p_mat( + nbands, + std::vector>>( + nbands, ModuleBase::Vector3>(0.0, 0.0, 0.0))); + // diagonal kinetic part: T = tpiba^2 |k+G|^2 (Ry a.u.) + for (int m = 0; m < nbands; ++m) { + for (int n = 0; n < nbands; ++n) { + std::complex dot[3] = {std::complex(0.0, 0.0), + std::complex(0.0, 0.0), + std::complex(0.0, 0.0)}; + for (int ig = 0; ig < npwk; ++ig) { + const std::complex cc = + std::conj(psi(ik, m, ig)) * psi(ik, n, ig); + for (int d = 0; d < 3; ++d) { + dot[d] += 2.0 * tpiba2 * gk[ig][d] * cc; + } + } + for (int d = 0; d < 3; ++d) { + p_mat[m][n][d] = dot[d]; + } + } + } + // nonlocal derivative part: dV_nl/dk_d = sum_{mu,nu} (|dvkb_mu> D_{mu,nu} D_{mu,nu} ntype; ++it) { + const pseudo& ncpp = ucell_->atoms[it].ncpp; + const int nh = ncpp.nh; + if (nh == 0) { + continue; + } + if (ncpp.tvanp || ncpp.has_so) { + ModuleBase::WARNING_QUIT("DFPT_Q0::pos_matrix", + "DFPT velocity operator is implemented for " + "normal-conserving separable pseudopotentials only."); + } + // projector -> (radial beta index, m channel) table, matching build_vkb + std::vector mu_ib(nh, 0); + std::vector mu_m(nh, 0); + int mu_idx = 0; + for (int ib = 0; ib < ncpp.nbeta; ++ib) { + const int l = ncpp.lll[ib]; + for (int m = 0; m < 2 * l + 1; ++m) { + if (mu_idx < nh) { + mu_ib[mu_idx] = ib; + mu_m[mu_idx] = m; + } + ++mu_idx; + } + } + for (int ia = 0; ia < ucell_->atoms[it].na; ++ia) { + std::vector>> vkb; + pert_->build_vkb(it, ia, gk, vkb); + // becp_b[mu] = for all bands + std::vector>> becp(nbands); + for (int b = 0; b < nbands; ++b) { + becp[b].assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int ig = 0; ig < npwk; ++ig) { + becp[b][mu] += std::conj(vkb[mu][ig]) * psi(ik, b, ig); + } + } + } + for (int d = 0; d < 3; ++d) { + std::vector>> dvkb; + pert_->build_vkb_dk(it, ia, d, gk, vkb, dvkb); + // dbecp_b[mu] = + std::vector>> dbecp(nbands); + for (int b = 0; b < nbands; ++b) { + dbecp[b].assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) { + for (int ig = 0; ig < npwk; ++ig) { + dbecp[b][mu] += std::conj(dvkb[mu][ig]) * psi(ik, b, ig); + } + } + } + // accumulate the two Hermitian-conjugate projector terms + for (int m = 0; m < nbands; ++m) { + for (int n = 0; n < nbands; ++n) { + std::complex term(0.0, 0.0); + for (int mu = 0; mu < nh; ++mu) { + std::complex out_m(0.0, 0.0); + std::complex in_n(0.0, 0.0); + for (int nu = 0; nu < nh; ++nu) { + if (mu_m[mu] != mu_m[nu]) { + continue; + } + const double dij = ncpp.dion(mu_ib[mu], mu_ib[nu]); + out_m += dij * becp[n][nu]; + in_n += dij * dbecp[n][nu]; + } + // D + D + term += std::conj(dbecp[m][mu]) * out_m + + std::conj(becp[m][mu]) * in_n; + } + p_mat[m][n][d] += term; + } + } + } + } + } + // velocity -> position: r = -i v / (tpiba (eps_m - eps_n)), r in bohr + // (from [H, r] = -i dH/dk in Ry a.u.); degenerate pairs are skipped, + // their gauge-dependent matrix elements carry no unique value. + for (int m = 0; m < nbands; ++m) { + for (int n = 0; n < nbands; ++n) { + if (m == n) { + continue; + } + const double de = eig(ik, m) - eig(ik, n); + if (std::abs(de) < 1.0e-8) { + continue; + } + for (int d = 0; d < 3; ++d) { + r_mat[ik][m][n][d] = std::complex(0.0, -1.0) * p_mat[m][n][d] + / (tpiba * de); + } + } + } + } +} + +void DFPT_Q0::compute_eps(const ModuleBase::matrix& wg, DFPT_PW_Data& data) { + if (ucell_ == nullptr) { + return; + } + const int nk = wg.nr; + const int nbands = wg.nc; + + // bare position legs Y^a and converged E-field responses dpsi^E,b of + // the q = 0 mesh (DFPT_PW::solve_pos_resp / solve_efield_resp) + std::vector>>>> yr(3); + std::vector>>>> de(3); + for (int a = 0; a < 3; ++a) { + yr[a] = data.get_pos_resp(a); + de[a] = data.get_dpsi_efield(a); + if (static_cast(yr[a].size()) != nk + || static_cast(de[a].size()) != nk) { + return; // responses not solved: nothing to accumulate + } + } + + build_stars(nk); + // wg-weighted partial chi_k[ik](a, b) = sum_occ Re at + // every stored k (QE dielec.f90: eps -= 4*(4pi/Omega)*wk*Re) + std::vector chi_k(nk, ModuleBase::matrix(3, 3, true)); + for (int ik = 0; ik < nk; ++ik) { + for (int v = 0; v < nbands; ++v) { + if (!dfpt_band_occupied(wg, ik, v)) { + continue; // empty + } + for (int a = 0; a < 3; ++a) { + const int npw = static_cast(yr[a][ik][v].size()); + if (npw <= 0) { + continue; + } + for (int b = 0; b < 3; ++b) { + if (static_cast(de[b][ik][v].size()) != npw) { + continue; + } + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npw; ++ig) { + dot += std::conj(yr[a][ik][v][ig]) * de[b][ik][v][ig]; + } + chi_k[ik](a, b) += wg(ik, v) * dot.real(); + } + } + } + } + ModuleBase::matrix eps(3, 3, true); + // wg carries the full k weight (star size included) times the spin + // factor 2, so the star-averaged partials sum to the complete + // Brillouin-zone average: no extra 1/nk normalization + for (int ik = 0; ik < nk; ++ik) { + const double inv_nstar = 1.0 / static_cast(stars_[ik].size()); + for (size_t im = 0; im < stars_[ik].size(); ++im) { + double rot[9]; + rotate_tensor(stars_[ik][im].cart, chi_k[ik], rot); + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + eps(a, b) += inv_nstar * rot[3 * a + b]; + } + } + } + } + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + // 16 pi / Omega: QE dielec.f90 form eps = 1 - 4*(4pi/Omega)*wk* + // Re (validated against QE 7.2 Si to 0.06%: + // 23.6825 here vs 23.6685 QE) + eps(a, b) *= -16.0 * ModuleBase::PI / ucell_->omega; + if (a == b) { + eps(a, b) += 1.0; + } + } + } + data.set_dielectric(eps); +} + +void DFPT_Q0::compute_born(const psi::Psi>& psi, + const ModuleBase::matrix& wg, + const ModuleBase::matrix& eig, DFPT_PW_Data& data) { + if (ucell_ == nullptr) { + return; + } + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + const int nat = ucell_->nat; + const int nbasis = psi.get_nbasis(); + (void)eig; + + // solved position legs Y^a_{k,v} = P_c x_a|psi_v> of the q = 0 mesh + // (DFPT_PW::solve_pos_resp stashes them per direction) + std::vector>>>> yr(3); + for (int a = 0; a < 3; ++a) { + yr[a] = data.get_pos_resp(a); + if (static_cast(yr[a].size()) != nk) { + return; // position responses not solved: nothing to accumulate + } + } + + build_stars(nk); + // star-rotated electronic partials, credited to the image atom under + // each star member: zacc[kappa](a, idir) + std::vector zacc(nat, ModuleBase::matrix(3, 3, true)); + + for (int iat = 0; iat < nat; ++iat) { + // wg-weighted partial chi_k[ik](a, idir) of THIS atom at every k + std::vector chi_k(nk, ModuleBase::matrix(3, 3, true)); + for (int idir = 0; idir < 3; ++idir) { + // converged screened displacement response dpsi(scf)/du of this + // mode, stashed by solve_displacement before compute_born runs + const std::vector>>> disp + = data.get_dpsi_disp(iat, idir); + if (static_cast(disp.size()) != nk) { + continue; + } + for (int ik = 0; ik < nk; ++ik) { + for (int v = 0; v < nbands; ++v) { + if (!dfpt_band_occupied(wg, ik, v)) { + continue; // empty + } + const int npw = static_cast(disp[ik][v].size()); + if (npw <= 0 || npw > nbasis) { + continue; // unsolved slot or inconsistent basis + } + // per field direction + for (int a = 0; a < 3; ++a) { + if (static_cast(yr[a][ik][v].size()) != npw) { + continue; + } + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npw; ++ig) { + dot += std::conj(disp[ik][v][ig]) * yr[a][ik][v][ig]; + } + chi_k[ik](a, idir) += wg(ik, v) * dot.real(); + } + } + } + } + // star average: the partial at member Rk is R chi(k) R^T and is + // credited to the image atom R(iat); wg already carries the star + // size, so each member contributes with 1/n_star + for (int ik = 0; ik < nk; ++ik) { + const double inv_nstar + = 1.0 / static_cast(stars_[ik].size()); + for (size_t im = 0; im < stars_[ik].size(); ++im) { + const StarMember& mem = stars_[ik][im]; + const int jat = (mem.atom_map.empty()) ? iat : mem.atom_map[iat]; + double rot[9]; + rotate_tensor(mem.cart, chi_k[ik], rot); + for (int a = 0; a < 3; ++a) { + for (int d = 0; d < 3; ++d) { + zacc[jat](a, d) += inv_nstar * rot[3 * a + d]; + } + } + } + } + } + + for (int iat = 0; iat < nat; ++iat) { + ModuleBase::matrix zstar(3, 3, true); + for (int a = 0; a < 3; ++a) { + for (int d = 0; d < 3; ++d) { + zstar(a, d) = -2.0 * zacc[iat](a, d); + } + } + // ionic rigid-ion charge on the diagonal (a == b directions) + const int it = ucell_->iat2it[iat]; + const double zion = ucell_->atoms[it].ncpp.zv; + for (int d = 0; d < 3; ++d) { + zstar(d, d) += zion; + } + data.set_born(iat, zstar); + } +} + +void DFPT_Q0::compute_q0_response(DFPT_PW_Data& data) { + // DFT+U reservation (U0): V_U is nonlocal (onsite projector), so the + // position operator does NOT commute with the DFT+U potential. The + // [r, V_U] commutator term must be handled separately in addition to + // the occupation-matrix response (docc) when u_active() runs; this is + // the hardest DFT+U piece and is deferred with the Plus_U wiring. + (void)data; } -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_q0.h b/source/source_pw/module_dfpt/dfpt_q0.h index 178567018e4..3b78db34f25 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.h +++ b/source/source_pw/module_dfpt/dfpt_q0.h @@ -17,30 +17,117 @@ namespace ModuleDFPT { +class DFPT_Pert; + +/** + * @brief q -> 0 response: dielectric tensor, Born charges, LO-TO (C6). + * + * The position operator is ill-defined for periodic states, so the + * periodic-gauge matrix elements are obtained through the velocity + * (commutator) form (Gonze & Lee, PRB 55, 10355 (1997)), m != n: + * = -i / (tpiba (eps_m - eps_n)), + * dH/dk_dir = 2 tpiba^2 (k+G)_dir (diagonal kinetic part) + * + dV_nl/dk_dir (build_vkb_dk; V_loc is k-independent), + * with the k derivative in dimensionless 2*pi/lat0 units (matching + * build_vkb_dk) so r comes out in bohr; [r, V_U] of DFT+U is a + * documented U0 reservation (the onsite projector is nonlocal, so the + * commutator does not vanish with U on). + * + * Dielectric tensor (insulating, ABACUS Ry a.u. with wg carrying the spin + * degeneracy; the extra 1/(eps_c - eps_v) is the length-gauge denominator, + * consistent with the oscillator-strength sum rule): + * eps_ab = delta_ab + (8 pi / Omega) sum_{k,v occ,c emp} wg + * * Re[] / (eps_c - eps_v) + * Born charges from dP/dtau, the screened displacement leg paired with the + * SOLVED conduction-projected position response (QE zstar_eu/add_zstar_ue + * anchoring; Gonze-Lee screened form). The position leg + * Y^a_{k,v} = P_c x_a|psi_{k,v}>, (H(k)-eps_v) Y = P_c [H,x_a]|psi_v>, + * with the commutator rhs [H,x_a]|psi> = -(i/tpiba) dH/dk_a|psi> (the + * same velocity operator as above), is solved exactly by Sternheimer + * solves in DFPT_PW (solve_pos_resp, stashed per direction in the shared + * data) and therefore carries the complete conduction-space response; the + * eigenvector-truncated r-matrix contraction of the du form is only its + * nbands-cut approximation. With dpsi^kappa(scf) the converged q = 0 + * Sternheimer displacement responses: + * Z*_k,ab = Z_k delta_ab - 2 sum_{k,v occ} wg + * * Re + * (wg carries the spin degeneracy, so the prefactor is the -2*wk of + * add_zstar_ue). By the symmetry of the mixed second derivative of the + * total energy this equals the transposed leg + * -2*sum wg*Re that QE's zstar_eu + * computes with the electric-field responses; only one leg is needed. + * The dpsi^kappa Sternheimer gauge ( = 0) drops the + * occupied-occupied block of x exactly. The diamond C7 target + * (Z* -> 0 by inversion + ASR) requires the screened dpsi. + * With a symmetry-reduced k list both sums run over the irreducible k and + * each partial tensor chi(k) is star-averaged: the physical partial at a + * rotated star member Rk is R chi(k) R^T, and atom-resolved (Born) partials + * are credited to the image atom under R. With symmetry off the stored list + * is the full mesh and the star machinery degenerates to the identity. + * The absolute calibration of both expressions is pinned by the diamond + * end-to-end test in C7 (structure/symmetry by the C6 tests). + */ class DFPT_Q0 { public: DFPT_Q0(); ~DFPT_Q0(); - - void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, - ModulePW::PW_Basis_K* pw_wfc); - - void compute_eps(const psi::Psi>& psi, - const ModuleBase::matrix& wg, DFPT_PW_Data& data); - - void compute_born(const psi::Psi>& psi, DFPT_PW_Data& data); - + + void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, + ModulePW::PW_Basis_K* pw_wfc, DFPT_Pert* pert); + + /// SCF dielectric tensor (QE dielec.f90 form): + /// eps = 1 - (16 pi / Omega) sum_k wg sum_v Re + /// consuming the converged E-field responses dpsi_efield and the bare + /// position legs pos_resp stashed by DFPT_PW (solve_pos_resp / + /// solve_efield_resp). Must run after both stashes are complete. + void compute_eps(const ModuleBase::matrix& wg, DFPT_PW_Data& data); + + void compute_born(const psi::Psi>& psi, + const ModuleBase::matrix& wg, + const ModuleBase::matrix& eig, DFPT_PW_Data& data); + void compute_q0_response(DFPT_PW_Data& data); - + + /// position-operator matrix elements r_mat[ik][m][n].d = + /// (m != n), periodic gauge (velocity form); + /// eig is the ground-state eigenvalue matrix (nk x nbands, Ry). + /// Design-phase PT reference: no production caller since compute_eps + /// moved to the SCF contraction (kept for the analytic serial tests + /// and as the independent-particle cross-check; cleanup review P0-3). + void pos_matrix(const psi::Psi>& psi, + const ModuleBase::matrix& eig, + std::vector>>>>& r_mat); + + // ---- k-star rotation of the symmetry-reduced q=0 tensor sums ---- + // One entry per DISTINCT folded star member Rk of an irreducible k: + // the representative operation in cartesian COLUMN form (rotate_tensor + // applies chi' = R chi R^T directly) plus the atom map iat -> image + // atom under the same operation (built from the direct space + // gmatrix/gtrans pair; species map to themselves). + struct StarMember { + ModuleBase::Matrix3 cart; ///< defaults to the identity + std::vector atom_map; ///< empty means the identity map + }; + std::vector> stars_; ///< [ik] -> star members + + /// rebuild stars_ for the stored k list (nk points); falls back to a + /// single identity member per k when the point group is unavailable + /// (symmetry off / unreduced mesh) or an atom map fails + void build_stars(int nk); + + /// chi_rot(a,b) = sum_{a'b'} R(a,a') R(b,b') chi(a',b') of a 3x3 + /// partial tensor under a cartesian rotation + static void rotate_tensor(const ModuleBase::Matrix3& r, + const ModuleBase::matrix& chi, + double (&chi_rot)[9]); + private: UnitCell* ucell_ = nullptr; ModulePW::PW_Basis* pw_rho_ = nullptr; ModulePW::PW_Basis_K* pw_wfc_ = nullptr; - - void pos_matrix(const psi::Psi>& psi, - std::vector>>>& r_mat); + DFPT_Pert* pert_ = nullptr; }; } // namespace ModuleDFPT -#endif // DFPT_Q0_H \ No newline at end of file +#endif // DFPT_Q0_H diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index 4cf239adb5b..8cb12ae5359 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -8,6 +8,16 @@ #include "dfpt_rho.h" +#include "dfpt_kq_basis.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" +#include "source_base/module_mixing/plain_mixing.h" +#include +#include +#include +#include +#include + namespace ModuleDFPT { DFPT_Rho::DFPT_Rho() {} @@ -19,35 +29,341 @@ DFPT_Rho::~DFPT_Rho() { } } -void DFPT_Rho::init(int nspin, int nrxx, ModulePW::PW_Basis* pw_rho, - ModulePW::PW_Basis_K* pw_wfc, const std::string& mix_type, - double mix_beta) { +void DFPT_Rho::init(int nspin, int nrxx, ModulePW::PW_Basis* pw_rho, + ModulePW::PW_Basis_K* pw_wfc, + const ModuleBase::Matrix3& recip_matrix, + const std::string& mix_type, double mix_beta, + double kerker_a2) { nspin_ = nspin; nrxx_ = nrxx; pw_rho_ = pw_rho; pw_wfc_ = pw_wfc; - (void)mix_type; - (void)mix_beta; + recip_matrix_ = recip_matrix; + mix_beta_ = mix_beta; + mix_type_ = mix_type; + kerker_a2_ = kerker_a2; + if (mix_type != "plain" && mix_type != "kerker") + { + ModuleBase::WARNING_QUIT("DFPT_Rho", + "unsupported mix_type, expected plain or kerker"); + } + delete mixer_; + mixer_ = new Base_Mixing::Plain_Mixing(mix_beta_); } -void DFPT_Rho::compute_drho(const psi::Psi>& psi, - const ModuleBase::matrix& wg, int q_idx, +void DFPT_Rho::compute_drho(const psi::Psi>& psi, + const ModuleBase::matrix& wg, int q_idx, DFPT_PW_Data& data) { + if (pw_rho_ == nullptr || pw_wfc_ == nullptr) { + return; + } + if (nspin_ != 1) + { + ModuleBase::WARNING_QUIT("DFPT_Rho", + "only nspin = 1 is supported in the design phase"); + } + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + const ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); + const ModuleBase::Vector3 q_cart = q_frac * recip_matrix_; + + std::vector> a_r(pw_rho_->nrxx, std::complex(0.0, 0.0)); + std::vector> u_r(pw_rho_->nrxx); + std::vector> d_r(pw_rho_->nrxx); + std::vector> d_recip(pw_rho_->npw, std::complex(0.0, 0.0)); + DFPT_KQ_Basis kq; + for (int ik = 0; ik < nk; ++ik) { + kq.init(pw_wfc_, pw_rho_, q_cart, ik); + const int npw_kq = kq.get_npwk(); + // k+q G index -> rho-grid ig (both bases share the FFT cell) + std::vector kq2rho(npw_kq, -1); + for (int igl = 0; igl < npw_kq; ++igl) { + kq2rho[igl] = kq.get_ig_rho(igl); + } + for (int ib = 0; ib < nbands; ++ib) { + const double w = wg(ik, ib); + if (!dfpt_band_occupied(wg, ik, ib)) { + continue; // unoccupied band: no contribution to the density + } + // periodic part u_nk(r) on the shared grid (phase-free FFT) + pw_wfc_->recip2real(&psi(ik, ib, 0), u_r.data(), ik); + // periodic part du_nk(r): scatter the k+q coefficients onto the + // rho grid and transform (same convention, so the product is + // consistent with the u transform) + std::fill(d_recip.begin(), d_recip.end(), std::complex(0.0, 0.0)); + const std::vector> dpsi = data.get_dpsi(q_idx, ik, ib); + const int nd = std::min(npw_kq, static_cast(dpsi.size())); + for (int igl = 0; igl < nd; ++igl) { + if (kq2rho[igl] >= 0) { + d_recip[kq2rho[igl]] = dpsi[igl]; + } + } + pw_rho_->recip2real(d_recip.data(), d_r.data()); + // same normalization as the GS density accumulation + // (elecstate_pw.cpp rhoBandK: w1 = wg / omega), including the + // spin factor 2: QE incdrhoscf uses wgt = 2 * weight / omega + // at every q (the factor 2 is the spin degeneracy, not a + // Hermitian completion) + const double w1 = 2.0 * w / pw_rho_->omega; + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + a_r[ir] += w1 * std::conj(u_r[ir]) * d_r[ir]; + } + } + } + + // Hermitian completion at q = 0: the band loop above stores only the + // u_n^* du_n piece (spin factor 2 already included in w1); the physical + // (real) response density at q = 0 also contains the du_n u_n^* piece, + // which coincides with the conjugate of the stored one, so keep only + // the real part of the amplitude before the FFT. The resulting + // coefficients are exactly Hermitian on the sphere, including + // one-sided sticks whose -G falls outside it. Away from q = 0 the +q + // harmonic of the response is exactly the one-sided object and no + // completion applies. + const bool q_is_zero = (std::abs(q_frac.x) < 1.0e-10 + && std::abs(q_frac.y) < 1.0e-10 + && std::abs(q_frac.z) < 1.0e-10); + if (q_is_zero) { + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + a_r[ir] = std::complex(a_r[ir].real(), 0.0); + } + } + + // q-shifted coefficients A_Delta on the rho grid + std::vector> drho_g(pw_rho_->npw); + pw_rho_->real2recip(a_r.data(), drho_g.data()); + + // charge conservation: the Delta = -q harmonic (G+q = 0 component of the + // response density) must vanish whenever -q falls on a reciprocal + // lattice vector; for a generic q inside the cell this never triggers + { + const ModuleBase::Vector3 mq_cart(-q_cart.x, -q_cart.y, -q_cart.z); + const ModuleBase::Vector3 mfrac = mq_cart * recip_matrix_.Inverse(); + const double mr[3] = {std::round(mfrac.x), std::round(mfrac.y), std::round(mfrac.z)}; + if (std::abs(mfrac.x - mr[0]) < 1.0e-6 && + std::abs(mfrac.y - mr[1]) < 1.0e-6 && + std::abs(mfrac.z - mr[2]) < 1.0e-6) + { + // locate the rho-grid G equal to -q through its FFT cell + const int cix = (static_cast(mr[0]) % pw_rho_->nx + pw_rho_->nx) % pw_rho_->nx; + const int ciy = (static_cast(mr[1]) % pw_rho_->ny + pw_rho_->ny) % pw_rho_->ny; + const int ciz = (static_cast(mr[2]) % pw_rho_->nz + pw_rho_->nz) % pw_rho_->nz; + int ig0 = -1; + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + const int isz = pw_rho_->ig2isz[ig]; + const int iz = isz % pw_rho_->nz; + const int is = isz / pw_rho_->nz; + const int ixy = pw_rho_->is2fftixy[is]; + const int ix = ixy / pw_rho_->fftny; + const int iy = ixy % pw_rho_->fftny; + if (ix == cix && iy == ciy && iz == ciz) { + ig0 = ig; + break; + } + } + if (ig0 >= 0) { + drho_g[ig0] = std::complex(0.0, 0.0); + } + } + } + data.set_drho_g(q_idx, 0, drho_g); + + // real-space manifest density: at q = 0 the completed coefficients are + // already the full (real) response; away from q = 0 the manifest is the + // real combination 2 Re[e^{i q r} A(r)] of the one-sided amplitude + std::vector> a_clean(pw_rho_->nrxx); + pw_rho_->recip2real(drho_g.data(), a_clean.data()); + std::vector drho_r(pw_rho_->nrxx); + if (q_is_zero) { + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + drho_r[ir] = a_clean[ir].real(); + } + } else { + for (int ix = 0; ix < pw_rho_->nx; ++ix) { + for (int iy = 0; iy < pw_rho_->ny; ++iy) { + for (int iz = 0; iz < pw_rho_->nz; ++iz) { + const int ir = (ix * pw_rho_->ny + iy) * pw_rho_->nz + iz; + const double theta = ModuleBase::TWO_PI * + (q_frac.x * ix / pw_rho_->nx + + q_frac.y * iy / pw_rho_->ny + + q_frac.z * iz / pw_rho_->nz); + drho_r[ir] = 2.0 * (a_clean[ir].real() * std::cos(theta) - + a_clean[ir].imag() * std::sin(theta)); + } + } + } + } + data.set_drho_r(q_idx, 0, drho_r); + + // remember the freshly computed output for the mixing step + if (q_idx >= static_cast(drho_out_.size())) { + drho_out_.resize(q_idx + 1); + } + drho_out_[q_idx].assign(1, drho_g); +} + +void DFPT_Rho::cal_docc(const psi::Psi>& psi, + const ModuleBase::matrix& wg, int q_idx, + DFPT_PW_Data& data) { + // Reserved first-order occupation matrix (docc) for DFT+U (U0). + // The physical cross terms need the beta projectors at both k and k+q + // (a PW-side adapter of the build_vkb machinery); they land together + // with the Plus_U production wiring in the C7/U1 window, when dpsi and + // a usable Plus_U provider coexist. Pure-PW runs keep u_active() false + // and never reach this accumulation: + // cross term: Re(becp(k+q, dpsi) * becp(k, psi)) (response) + // frozen term: becp(k, psi) * dbecp_f(k, psi) (GS k) + if (!data.with_u()) { + return; + } (void)psi; (void)wg; (void)q_idx; (void)data; } +void DFPT_Rho::reset_mixing(int q_idx) { + if (q_idx < 0) { + return; + } + if (q_idx < static_cast(drho_in_.size())) { + drho_in_[q_idx].clear(); + } + if (q_idx < static_cast(residual_.size())) { + residual_[q_idx] = 0.0; + } +} + void DFPT_Rho::mix_drho(int q_idx, DFPT_PW_Data& data) { - (void)q_idx; - (void)data; + if (mixer_ == nullptr || pw_rho_ == nullptr) { + return; + } + const std::vector> out = data.get_drho_g(q_idx, 0); + if (out.empty() || static_cast(out.size()) != pw_rho_->npw) { + return; + } + const int npw = pw_rho_->npw; + if (q_idx >= static_cast(drho_in_.size())) { + drho_in_.resize(q_idx + 1); + residual_.resize(q_idx + 1, 0.0); + } + // first iteration starts from a zero input density + if (drho_in_[q_idx].empty()) { + drho_in_[q_idx].assign(1, std::vector>(npw, std::complex(0.0, 0.0))); + } + const std::vector>& rin = drho_in_[q_idx][0]; + std::vector> mixed(npw); + // the fractional q is needed both by the Kerker screen and by the + // real-space manifest below; the q-shifted |G+q| convention matches + // v_hartree_q (gcar + q_frac * recip, 1/lat0^2 units) + const ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); + if (mix_type_ == "kerker") { + const ModuleBase::Vector3 q_cart = q_frac * recip_matrix_; + std::vector> rin_s(npw); + std::vector> out_s(npw); + std::vector> mixed_s(npw); + for (int ig = 0; ig < npw; ++ig) { + const ModuleBase::Vector3 w = pw_rho_->gcar[ig] + q_cart; + const double w2 = w * w; + // |G+q| = 0 harmonic: f = 0, frozen at rin (that harmonic is + // dropped by compute_drho, so both inputs are zero there) + const double f = (w2 < 1.0e-12) ? 0.0 : w2 / (w2 + kerker_a2_); + rin_s[ig] = f * rin[ig]; + out_s[ig] = f * out[ig]; + } + mixer_->plain_mix(mixed_s.data(), + rin_s.data(), + out_s.data(), + npw, + std::function*)>()); + // add back the screened-out part: mixed = rin + beta f (out - rin), + // i.e. a plain mix with the per-shell coefficient beta f_g while + // the stored density stays physical (not screen-scaled) + for (int ig = 0; ig < npw; ++ig) { + mixed[ig] = rin[ig] + (mixed_s[ig] - rin_s[ig]); + } + } else { + mixer_->plain_mix(mixed.data(), + rin.data(), + out.data(), + npw, + std::function*)>()); + } + // relative residual ||out - in|| / ||out|| + double dn2 = 0.0; + double o2 = 0.0; + for (int ig = 0; ig < npw; ++ig) { + dn2 += std::norm(out[ig] - rin[ig]); + o2 += std::norm(out[ig]); + } + residual_[q_idx] = (o2 > 0.0) ? std::sqrt(dn2 / o2) : 0.0; + drho_in_[q_idx][0] = mixed; + data.set_drho_g(q_idx, 0, mixed); + + // rebuild the real-space manifest from the mixed coefficients (q = 0: + // completed coefficients are the full real response; otherwise the + // one-sided 2 Re[e^{i q r} A(r)] manifest) + const bool q_is_zero = (std::abs(q_frac.x) < 1.0e-10 + && std::abs(q_frac.y) < 1.0e-10 + && std::abs(q_frac.z) < 1.0e-10); + std::vector> a_clean(pw_rho_->nrxx); + pw_rho_->recip2real(mixed.data(), a_clean.data()); + std::vector drho_r(pw_rho_->nrxx); + if (q_is_zero) { + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + drho_r[ir] = a_clean[ir].real(); + } + } else { + for (int ix = 0; ix < pw_rho_->nx; ++ix) { + for (int iy = 0; iy < pw_rho_->ny; ++iy) { + for (int iz = 0; iz < pw_rho_->nz; ++iz) { + const int ir = (ix * pw_rho_->ny + iy) * pw_rho_->nz + iz; + const double theta = ModuleBase::TWO_PI * + (q_frac.x * ix / pw_rho_->nx + + q_frac.y * iy / pw_rho_->ny + + q_frac.z * iz / pw_rho_->nz); + drho_r[ir] = 2.0 * (a_clean[ir].real() * std::cos(theta) - + a_clean[ir].imag() * std::sin(theta)); + } + } + } + } + data.set_drho_r(q_idx, 0, drho_r); } double DFPT_Rho::get_residual(int q_idx, DFPT_PW_Data& data) const { - (void)q_idx; (void)data; - return 0.0; + if (q_idx < 0 || q_idx >= static_cast(residual_.size())) { + return 0.0; + } + return residual_[q_idx]; +} + +void DFPT_Rho::v_hartree_q(const ModuleBase::Vector3& q_cart, + const std::vector>& drho_g, + std::vector>& dv_ha_g) const { + if (pw_rho_ == nullptr) { + dv_ha_g.clear(); + return; + } + const int npw = pw_rho_->npw; + if (static_cast(drho_g.size()) != npw) { + dv_ha_g.clear(); + return; + } + dv_ha_g.assign(npw, std::complex(0.0, 0.0)); + for (int ig = 0; ig < npw; ++ig) { + const ModuleBase::Vector3 w = pw_rho_->gcar[ig] + q_cart; + const double w2_lat0 = w * w; // 1/lat0^2 units, like pw_rho_->gg + // skip |G+q| = 0 (ig = -q): the q-shifted G=0 harmonic of the + // Hartree kernel (v_hartree skips ig_gge0 the same way) + if (w2_lat0 < 1.0e-12) { + continue; + } + const double fac = ModuleBase::e2 * ModuleBase::FOUR_PI + / (pw_rho_->tpiba2 * w2_lat0); + dv_ha_g[ig] = fac * drho_g[ig]; + } } -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_rho.h b/source/source_pw/module_dfpt/dfpt_rho.h index 9790e583821..f1b696d88d8 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -10,27 +10,111 @@ #define DFPT_RHO_H #include "dfpt_pw_data.h" +#include "source_base/matrix3.h" #include "source_psi/psi.h" -#include "source_estate/module_charge/charge_mixing.h" #include "source_basis/module_pw/pw_basis.h" #include "source_basis/module_pw/pw_basis_k.h" +#include +#include + +namespace Base_Mixing +{ +class Plain_Mixing; +} namespace ModuleDFPT { +/** + * @brief First-order exchange-correlation kernel contract (C6). + * + * Production adapters live at the esolver wiring layer (C7): the complex + * q-shifted density amplitude drho_r is split into Re/Im parts, fed + * through the real-space finite-difference kernel (elecstate::PotXC_FDM, + * delta V_xc = V_xc[rho0 + drho] - V_xc[rho0]) and recombined - linear + * superposition is exact up to O(|drho|^2). module_dfpt itself never + * includes pot_xc_fdm.h (minimal header dependencies), mirroring the + * DFPT_Stern::LinearOperator injection convention. + */ +class XC_First_Order { +public: + virtual ~XC_First_Order() = default; + + /// dvxc_r(r) = delta V_xc[drho_r](r), complex q-shifted amplitude on + /// the shared real-space grid. Implementations must not resize or + /// alias drho_r; dvxc_r is resized to drho_r.size() and fully + /// overwritten. + virtual void apply(const std::vector>& drho_r, + std::vector>& dvxc_r) const = 0; +}; + +/** + * @brief First-order density response (C3). + * + * compute_drho builds the q-shifted response density + * drho(r) = 2 Re[ e^{i q r} A(r) ], + * A(r) = sum_{k,n occ} wg(k,n) u*_nk(r) du_nk(r), + * where u/du are the periodic parts of psi_nk (k basis) and dpsi_nk (k+q + * basis); PW_Basis_K / PW_Basis transforms are phase-free (they return the + * periodic part), so the Bloch phases combine into the single e^{i q r} + * factor. In reciprocal space drho_g holds the q-shifted coefficients + * drho_Delta (coefficient of e^{i (Delta+q) r}, indexed by the rho-grid ig), + * A_Delta = sum_{kn} wg sum_G c*_G(k,n) d_{G+Delta}(k,n); the Delta = -q + * harmonic is dropped when -q falls on a reciprocal-lattice vector (charge + * conservation, notably at q = Gamma). + * + * mix_drho mixes the q-shifted coefficients: + * drho_in <- drho_in + beta_g (drho_out - drho_in) + * through Base_Mixing::Plain_Mixing (no Charge_Mixing / Charge dependency). + * With mix_type = "plain" beta_g = beta on every shell; with mix_type = + * "kerker" beta_g = beta * f_g, the Kerker screen + * f_g = |G+q|^2 / (|G+q|^2 + a^2), + * evaluated with the same q_shifted |G+q| = |gcar + q_cart * recip| (in + * 1/lat0^2 units) convention as v_hartree_q, so the Coulomb-stiffness + * eigenvalues concentrated on the smallest shells (lambda ~ -2.2 on + * {111}/{200} for the diamond smoke case, where plain mixing needs + * beta < 2 / (1 + |lambda|)) become stabilizable at beta up to 1. The + * screen is applied to both drho_in and drho_out before the plain mix and + * the screened-out part is added back, i.e. the stored mixed density is + * rin + beta_g (out - in) (physical, not screen-scaled); the |G+q| = 0 + * harmonic (f = 0) is frozen, consistent with its drop in compute_drho. + */ class DFPT_Rho { public: DFPT_Rho(); ~DFPT_Rho(); - void init(int nspin, int nrxx, ModulePW::PW_Basis* pw_rho, - ModulePW::PW_Basis_K* pw_wfc, const std::string& mix_type, - double mix_beta); + void init(int nspin, int nrxx, ModulePW::PW_Basis* pw_rho, + ModulePW::PW_Basis_K* pw_wfc, + const ModuleBase::Matrix3& recip_matrix, + const std::string& mix_type, double mix_beta, + double kerker_a2); void compute_drho(const psi::Psi>& psi, const ModuleBase::matrix& wg, int q_idx, DFPT_PW_Data& data); + /// first-order occupation matrix (docc) for DFT+U (U0 reservation). + void cal_docc(const psi::Psi>& psi, + const ModuleBase::matrix& wg, int q_idx, + DFPT_PW_Data& data); + void mix_drho(int q_idx, DFPT_PW_Data& data); + + /// C7: drop the mixing state of q_idx so the next perturbation at the + /// same q restarts from a zero input density (the drho_in slot is + /// indexed by q only, while every (atom, direction) needs its own + /// self-consistent cycle). + void reset_mixing(int q_idx); + + /// C6: q-shifted first-order Hartree potential in reciprocal space, + /// dV_H(G) = 4 pi e^2 / |G+q|^2 * drho_g, + /// with the convention aligned with elecstate::H_Hartree_pw::v_hartree + /// (fac = e2 * FOUR_PI / (tpiba2 * |G+q|^2)); the |G+q| = 0 component + /// (ig = -q) is skipped. Serves both the C6 q->0 response and the C7 + /// screened potential at every q point. + void v_hartree_q(const ModuleBase::Vector3& q_cart, + const std::vector>& drho_g, + std::vector>& dv_ha_g) const; double get_residual(int q_idx, DFPT_PW_Data& data) const; @@ -39,11 +123,20 @@ class DFPT_Rho { int nrxx_ = 0; ModulePW::PW_Basis* pw_rho_ = nullptr; ModulePW::PW_Basis_K* pw_wfc_ = nullptr; + ///< reciprocal lattice matrix in 1/lat0 (UnitCell::G convention) + ModuleBase::Matrix3 recip_matrix_; + double mix_beta_ = 0.7; + ///< mixing algorithm: "plain" (beta only) or "kerker" (Kerker screen) + std::string mix_type_; + ///< Kerker screening parameter a^2 in 1/lat0^2 (same units as |G+q|^2) + double kerker_a2_ = 0.0; - Charge_Mixing* mixer_ = nullptr; + Base_Mixing::Plain_Mixing* mixer_ = nullptr; - std::vector>> drho_in_; - std::vector>> drho_out_; + /// mixing state, q-shifted coefficients on the rho grid, [q][spin] + std::vector>>> drho_in_; + std::vector>>> drho_out_; + std::vector residual_; }; } // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_stern.cpp b/source/source_pw/module_dfpt/dfpt_stern.cpp index 14fb3e47f65..fe379c2f39b 100644 --- a/source/source_pw/module_dfpt/dfpt_stern.cpp +++ b/source/source_pw/module_dfpt/dfpt_stern.cpp @@ -8,58 +8,145 @@ #include "dfpt_stern.h" +#include + namespace ModuleDFPT { DFPT_Stern::DFPT_Stern() {} DFPT_Stern::~DFPT_Stern() {} -void DFPT_Stern::init(int nk, int nbands, int npw_max, const ModuleBase::matrix& eig, - const ModuleBase::matrix& wg, double alpha) { - nk_ = nk; - nbands_ = nbands; - npw_max_ = npw_max; - eig_ = eig; - wg_ = wg; - alpha_ = alpha; -} +namespace { -void DFPT_Stern::solve(const psi::Psi>& psi, - const std::vector>& dv_psi, - int q_idx, int k_idx, int band_idx, double omega, - DFPT_PW_Data& data) { - (void)psi; - (void)dv_psi; - (void)q_idx; - (void)k_idx; - (void)band_idx; - (void)omega; - (void)data; +double real_vdot(const std::vector>& a, + const std::vector>& b) +{ + // Re = Re sum_i conj(a_i) b_i (the CG scalar products of a + // Hermitian operator are real up to roundoff) + double s = 0.0; + for (size_t i = 0; i < a.size(); ++i) + { + s += a[i].real() * b[i].real() + a[i].imag() * b[i].imag(); + } + return s; } -void DFPT_Stern::apply_pv(const std::vector>& x, - std::vector>& px) { - (void)x; - (void)px; -} +} // namespace -void DFPT_Stern::apply_op(const std::vector>& x, - std::vector>& y, - int k_idx, int band_idx) { - (void)x; - (void)y; - (void)k_idx; - (void)band_idx; +void DFPT_Stern::apply_pv(const std::vector>>& occ_kq, + const std::vector>& x, + std::vector>& px) const +{ + px = x; + // two modified Gram-Schmidt sweeps keep the complement exact enough for + // long CG chains even when the occupied set is only machine-orthonormal; + // each projection collects its coefficient before subtracting, so px may + // alias x + for (int sweep = 0; sweep < 2; ++sweep) + { + for (size_t m = 0; m < occ_kq.size(); ++m) + { + const std::vector>& u = occ_kq[m]; + std::complex c(0.0, 0.0); + for (size_t i = 0; i < u.size(); ++i) + { + c += std::conj(u[i]) * px[i]; + } + for (size_t i = 0; i < u.size(); ++i) + { + px[i] -= c * u[i]; + } + } + } } -void DFPT_Stern::cg_solve(const std::vector>& b, - std::vector>& x, - int k_idx, int band_idx, double& residual) { - (void)b; - (void)x; - (void)k_idx; - (void)band_idx; - (void)residual; +int DFPT_Stern::solve(const LinearOperator& aop, + const std::vector>>& occ_kq, + const std::vector>& b, + int max_iter, + double conv_thr, + std::vector>& dpsi, + double& residual) const +{ + const int n = aop.dimension(); + dpsi.assign(n, std::complex(0.0, 0.0)); + if (n == 0 || static_cast(b.size()) != n || max_iter <= 0) + { + residual = 0.0; + return 0; + } + for (size_t m = 0; m < occ_kq.size(); ++m) + { + if (static_cast(occ_kq[m].size()) != n) + { + residual = 0.0; + return 0; + } + } + + std::vector> pb(n); + apply_pv(occ_kq, b, pb); + const double bnorm = std::sqrt(real_vdot(pb, pb)); + if (bnorm < 1.0e-300) + { + // the right-hand side lies inside the occupied subspace: the + // projected system is homogeneous and dpsi = 0 solves it exactly + residual = 0.0; + return 0; + } + + std::vector> r = pb; + std::vector> p = pb; + std::vector> ap(n); + std::vector> pap(n); + std::vector> tmp(n); + double rnorm2 = real_vdot(r, r); + int used = 0; + for (int iter = 0; iter < max_iter; ++iter) + { + used = iter + 1; + aop.apply(p.data(), ap.data()); + apply_pv(occ_kq, ap, pap); + double pAp = real_vdot(p, pap); + if (pAp <= 0.0) + { + // loss of positive definiteness along p (roundoff drift out of + // the complement): restart the direction from the residual + p = r; + aop.apply(p.data(), ap.data()); + apply_pv(occ_kq, ap, pap); + pAp = real_vdot(p, pap); + if (pAp <= 0.0) + { + rnorm2 = real_vdot(r, r); + break; + } + } + const double alpha = rnorm2 / pAp; + for (int i = 0; i < n; ++i) + { + dpsi[i] += alpha * p[i]; + r[i] -= alpha * pap[i]; + } + const double rnew2 = real_vdot(r, r); + if (std::sqrt(rnew2) / bnorm < conv_thr) + { + rnorm2 = rnew2; + break; + } + const double beta = rnew2 / rnorm2; + for (int i = 0; i < n; ++i) + { + p[i] = r[i] + beta * p[i]; + } + apply_pv(occ_kq, p, p); // in-place re-projection of the search direction + rnorm2 = rnew2; + } + // final hygiene: remove any occupied-subspace leakage of the solution + apply_pv(occ_kq, dpsi, tmp); + dpsi.swap(tmp); + residual = std::sqrt(rnorm2) / bnorm; + return used; } -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_stern.h b/source/source_pw/module_dfpt/dfpt_stern.h index 38305e25268..762697ae22b 100644 --- a/source/source_pw/module_dfpt/dfpt_stern.h +++ b/source/source_pw/module_dfpt/dfpt_stern.h @@ -9,45 +9,67 @@ #ifndef DFPT_STERN_H #define DFPT_STERN_H -#include "dfpt_pw_data.h" -#include "source_psi/psi.h" +#include +#include namespace ModuleDFPT { +/** + * @brief Projected conjugate-gradient solver of the Sternheimer equation (C2). + * + * Solves the insulating valence response + * (H(k+q) - eps_n) P_c |dpsi_n> = -P_c |dV psi_n>, + * P_c = 1 - sum_{m occ} |u_m(k+q)>hPsi at the k+q point + * (wired in C7), while unit tests supply analytic operators. + */ class DFPT_Stern { public: DFPT_Stern(); ~DFPT_Stern(); - - void init(int nk, int nbands, int npw_max, const ModuleBase::matrix& eig, - const ModuleBase::matrix& wg, double alpha); - - void solve(const psi::Psi>& psi, - const std::vector>& dv_psi, - int q_idx, int k_idx, int band_idx, double omega, - DFPT_PW_Data& data); - + + /// Hermitian linear action y = (H(k+q) - eps) x on the k+q basis; the + /// eigenvalue shift is carried inside the implementation. + class LinearOperator { + public: + virtual ~LinearOperator() = default; + virtual int dimension() const = 0; + virtual void apply(const std::complex* x, std::complex* y) const = 0; + }; + + /** + * @brief Solve one projected Sternheimer system with conjugate gradients. + * + * @param aop shifted Hamiltonian (H(k+q) - eps_n), Hermitian + * @param occ_kq orthonormal occupied states at k+q (may be empty) + * @param b right-hand side -dV|psi_n>; projected internally + * @param max_iter linear-solver iteration cap (> 0) + * @param conv_thr relative residual threshold ||P_c r|| / ||P_c b|| + * @param dpsi output P_c|dpsi_n> (zero inside the occ subspace) + * @param residual achieved relative residual + * @return number of iterations used + */ + int solve(const LinearOperator& aop, + const std::vector>>& occ_kq, + const std::vector>& b, + int max_iter, + double conv_thr, + std::vector>& dpsi, + double& residual) const; + private: - int nk_ = 0; - int nbands_ = 0; - int npw_max_ = 0; - double alpha_ = 1.0; - - ModuleBase::matrix eig_; - ModuleBase::matrix wg_; - - void apply_pv(const std::vector>& x, - std::vector>& px); - - void apply_op(const std::vector>& x, - std::vector>& y, - int k_idx, int band_idx); - - void cg_solve(const std::vector>& b, - std::vector>& x, - int k_idx, int band_idx, double& residual); + /// P_c x by modified Gram-Schmidt against the occupied states; safe for + /// px to alias x (projection coefficients are collected before subtracting) + void apply_pv(const std::vector>>& occ_kq, + const std::vector>& x, + std::vector>& px) const; }; } // namespace ModuleDFPT -#endif // DFPT_STERN_H \ No newline at end of file +#endif // DFPT_STERN_H diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt new file mode 100644 index 00000000000..b14ec5ab375 --- /dev/null +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -0,0 +1,57 @@ +abacus_disable_feature_definitions(__MLALGO) +abacus_disable_feature_definitions(__CUDA) +abacus_disable_feature_definitions(__ROCM) +abacus_disable_feature_definitions(__EXX) + +AddTest( + TARGET MODULE_DFPT_pw_data_test + LIBS parameter base device symmetry + SOURCES dfpt_pw_data_test.cpp + ../dfpt_pw_data.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures. + dfpt_test_mocks.cpp + # Shared cubic-cell / stru_lib fixture. + dfpt_stru_fixture.cpp +) + +AddTest( + TARGET MODULE_DFPT_kq_basis_test + LIBS parameter base device symmetry planewave + SOURCES dfpt_kq_basis_test.cpp + ../dfpt_kq_basis.cpp + dfpt_test_mocks.cpp +) + +AddTest( + TARGET MODULE_DFPT_stern_test + LIBS parameter base device symmetry + SOURCES dfpt_stern_test.cpp + ../dfpt_stern.cpp + dfpt_test_mocks.cpp +) + +AddTest( + TARGET MODULE_DFPT_pw_run_test + LIBS parameter base device symmetry planewave + SOURCES dfpt_pw_run_test.cpp + ../dfpt_pw.cpp + ../dfpt_pw_data.cpp + ../dfpt_pert.cpp + ../dfpt_kq_basis.cpp + ../dfpt_stern.cpp + ../dfpt_rho.cpp + ../dfpt_phon.cpp + ../dfpt_q0.cpp + ../dfpt_metal.cpp + ../dfpt_hamilt_shift.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Shared ctor/dtor stubs for the cell/spepot/charge/Plus_U closures. + dfpt_test_mocks.cpp + # Shared cubic-cell / stru_lib fixture. + dfpt_stru_fixture.cpp +) diff --git a/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp b/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp new file mode 100644 index 00000000000..42a22003d12 --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp @@ -0,0 +1,413 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_basis/module_pw/pw_basis_k.h" +#include "source_pw/module_dfpt/dfpt_kq_basis.h" + +/************************************************ + * unit test of DFPT_KQ_Basis (C0) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_KQ_Basis::init() - enumeration of the local k+q plane-wave + * basis from the union of the ground-state wavefunction G grid and the + * denser charge G grid (the wavefunction reservoir alone does not + * cover q-shifted balls: its radius is (sqrt(gk_ecut)+max|k_mesh|)^2). + * - Accessors get_npwk / get_ig_rho / get_gcar / get_gpluskq / get_gk2 / + * get_kplusq. + * + * Both bases are hand-built with public members only (no FFT setup + * needed): complex (gamma_only=false) grids on a cubic lattice. Every + * selection is cross-checked against an independent brute-force count + * over the full FFT grid. The TruncatedWfcReservoirCompletedByRhoGrid + * case reproduces the q!=0 truncation defect the dual-reservoir + * enumeration fixes. + */ + +namespace { + +bool VecLess(const ModuleBase::Vector3& a, const ModuleBase::Vector3& b) +{ + if (a.x != b.x) + { + return a.x < b.x; + } + if (a.y != b.y) + { + return a.y < b.y; + } + return a.z < b.z; +} + +// mirror the wrap used by cal_GplusK_cartesian / collect_local_pw +int WrapIndex(int i, int n) +{ + if (i >= n / 2 + 1) + { + return i - n; + } + return i; +} + +class DFPTKQBasisTest : public testing::Test +{ + protected: + ModulePW::PW_Basis_K pw_; + ModulePW::PW_Basis prho_; + const double lat0_ = 1.8897261254578281; + const double ecutwfc_ = 520.0; // ball radius ~ 2 G shells + double tpiba2_ = 0.0; + double gk_ecut_ = 0.0; + double ggecut_ = 0.0; + ModuleBase::Matrix3 G_; + const int nx_ = 7, ny_ = 7, nz_ = 7; + + void ResetBasis() + { + delete[] pw_.kvec_c; + delete[] pw_.ig2isz; + delete[] pw_.is2fftixy; + pw_ = ModulePW::PW_Basis_K(); + pw_.nx = 0; + pw_.ny = 0; + pw_.nz = 0; + pw_.fftny = 0; + pw_.npw = 0; + pw_.nst = 0; + delete[] prho_.ig2isz; + delete[] prho_.is2fftixy; + prho_ = ModulePW::PW_Basis(); + prho_.nx = 0; + prho_.ny = 0; + prho_.nz = 0; + prho_.fftny = 0; + prho_.npw = 0; + prho_.nst = 0; + } + + // fill the (stick, z) layout the real distribution code produces for a + // given ball; fftny = ny (complex basis). nxyz must be preset. + void FillSticks(ModulePW::PW_Basis& pw, double ggecut) + { + std::vector is2fftixy; + std::vector ig2isz; + for (int ix0 = 0; ix0 < pw.nx; ++ix0) + { + const int wix = WrapIndex(ix0, pw.nx); + for (int iy0 = 0; iy0 < pw.ny; ++iy0) + { + const int wiy = WrapIndex(iy0, pw.ny); + std::vector stick; + for (int iz0 = 0; iz0 < pw.nz; ++iz0) + { + const int wiz = WrapIndex(iz0, pw.nz); + ModuleBase::Vector3 f(wix, wiy, wiz); + const ModuleBase::Vector3 g = f * G_; + if (g * g <= ggecut) + { + stick.push_back(iz0); + } + } + if (!stick.empty()) + { + const int is = static_cast(is2fftixy.size()); + is2fftixy.push_back(iy0 + ix0 * pw.fftny); + for (size_t s = 0; s < stick.size(); ++s) + { + ig2isz.push_back(is * pw.nz + stick[s]); + } + } + } + } + pw.nst = static_cast(is2fftixy.size()); + pw.npw = static_cast(ig2isz.size()); + pw.is2fftixy = new int[pw.nst]; + pw.ig2isz = new int[pw.npw]; + for (int i = 0; i < pw.nst; ++i) + { + pw.is2fftixy[i] = is2fftixy[i]; + } + for (int i = 0; i < pw.npw; ++i) + { + pw.ig2isz[i] = ig2isz[i]; + } + } + + // shared field setup for a cubic complex basis. The wavefunction grid + // uses the production radius (sqrt(gk_ecut) + max|k_mesh|)^2; the + // charge grid uses the production 4*ecutwfc (= 4*gk_ecut ball, no + // k list) that covers every q-shifted ball for q in the first BZ. + void BuildBase(const std::vector>& kvec_c) + { + tpiba2_ = ModuleBase::TWO_PI * ModuleBase::TWO_PI / (lat0_ * lat0_); + gk_ecut_ = ecutwfc_ / tpiba2_; + const double b = ModuleBase::TWO_PI / lat0_; + G_.e11 = b; + G_.e12 = 0; + G_.e13 = 0; + G_.e21 = 0; + G_.e22 = b; + G_.e23 = 0; + G_.e31 = 0; + G_.e32 = 0; + G_.e33 = b; + + double kmaxmod = 0.0; + for (size_t i = 0; i < kvec_c.size(); ++i) + { + kmaxmod = std::max(kmaxmod, std::sqrt(kvec_c[i] * kvec_c[i])); + } + ggecut_ = std::pow(std::sqrt(gk_ecut_) + kmaxmod, 2); + + pw_.nx = nx_; + pw_.ny = ny_; + pw_.nz = nz_; + pw_.nxyz = nx_ * ny_ * nz_; + pw_.fftny = ny_; // gamma_only = false + pw_.gamma_only = false; + pw_.G = G_; + pw_.ggecut = ggecut_; + pw_.gk_ecut = gk_ecut_; + pw_.nks = static_cast(kvec_c.size()); + pw_.kvec_c = new ModuleBase::Vector3[pw_.nks]; + for (int i = 0; i < pw_.nks; ++i) + { + pw_.kvec_c[i] = kvec_c[i]; + } + FillSticks(pw_, ggecut_); + + prho_.nx = nx_; + prho_.ny = ny_; + prho_.nz = nz_; + prho_.nxyz = nx_ * ny_ * nz_; + prho_.fftny = ny_; + prho_.gamma_only = false; + prho_.G = G_; + prho_.ggecut = 4.0 * gk_ecut_; + FillSticks(prho_, prho_.ggecut); + } + + // independent reference: brute-force count/collect over the whole FFT + // grid for a given shifted center, sorted for set comparison. + std::vector> ReferenceSelection(const ModuleBase::Vector3& center) + { + std::vector> out; + for (int ix0 = 0; ix0 < nx_; ++ix0) + { + const int wix = WrapIndex(ix0, nx_); + for (int iy0 = 0; iy0 < ny_; ++iy0) + { + const int wiy = WrapIndex(iy0, ny_); + for (int iz0 = 0; iz0 < nz_; ++iz0) + { + const int wiz = WrapIndex(iz0, nz_); + ModuleBase::Vector3 f(wix, wiy, wiz); + ModuleBase::Vector3 g = f * G_; + ModuleBase::Vector3 gp = g + center; + if (gp * gp <= gk_ecut_) + { + out.push_back(gp); + } + } + } + } + std::sort(out.begin(), out.end(), VecLess); + return out; + } + + std::vector> KqSet(const ModuleDFPT::DFPT_KQ_Basis& kq) + { + std::vector> out = kq.get_gcar_all(); + for (size_t i = 0; i < out.size(); ++i) + { + out[i] = kq.get_gpluskq(i); + } + std::sort(out.begin(), out.end(), VecLess); + return out; + } +}; + +TEST_F(DFPTKQBasisTest, GammaQ0ReproducesWfcGrid) +{ + // single Gamma k: kmaxmod = 0, so the wavefunction grid ball equals the + // Gamma ball and the q=0 selection must reproduce it verbatim. + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0)}); + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_, &prho_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + ASSERT_TRUE(kq.is_valid()); + EXPECT_EQ(kq.get_npwk(), pw_.npw); + + // every selected vector lies inside the cutoff and on the brute-force set + const std::vector> ref = ReferenceSelection( + ModuleBase::Vector3(0.0, 0.0, 0.0)); + EXPECT_EQ(static_cast(ref.size()), pw_.npw); + const std::vector> sel = KqSet(kq); + EXPECT_EQ(sel.size(), ref.size()); + for (size_t i = 0; i < ref.size(); ++i) + { + EXPECT_DOUBLE_EQ(sel[i].x, ref[i].x); + EXPECT_DOUBLE_EQ(sel[i].y, ref[i].y); + EXPECT_DOUBLE_EQ(sel[i].z, ref[i].z); + } + + // rho-grid indices are unique and in range + std::vector igs; + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + const int ig = kq.get_ig_rho(igl); + EXPECT_GE(ig, 0); + EXPECT_LT(ig, prho_.npw); + igs.push_back(ig); + } + std::sort(igs.begin(), igs.end()); + for (size_t i = 1; i < igs.size(); ++i) + { + EXPECT_NE(igs[i], igs[i - 1]); + } +} + +TEST_F(DFPTKQBasisTest, ShiftedCenterSelectsAsymmetricSphere) +{ + // k = (0,0,0.5b): the |G+k|^2 cut keeps an asymmetric shell + const double b = ModuleBase::TWO_PI / lat0_; + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.5 * b)}); + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_, &prho_, ModuleBase::Vector3(0.0, 0.0, 0.0), 1); + const ModuleBase::Vector3 center = kq.get_kplusq(); + EXPECT_NEAR(center.x, 0.0, 1e-12); + EXPECT_NEAR(center.y, 0.0, 1e-12); + EXPECT_NEAR(center.z, 0.5 * b, 1e-12); + + // independent brute force on the full FFT grid + const std::vector> ref = ReferenceSelection(center); + const std::vector> sel = KqSet(kq); + EXPECT_EQ(sel.size(), ref.size()); + for (size_t i = 0; i < ref.size(); ++i) + { + EXPECT_DOUBLE_EQ(sel[i].x, ref[i].x); + EXPECT_DOUBLE_EQ(sel[i].y, ref[i].y); + EXPECT_DOUBLE_EQ(sel[i].z, ref[i].z); + } +} + +TEST_F(DFPTKQBasisTest, TruncatedWfcReservoirCompletedByRhoGrid) +{ + // regression of the q!=0 truncation defect: the wavefunction grid is + // sized for the k-mesh only, so a q-shifted ball can poke outside it + // while staying inside the 4*ecutwfc charge ball. The dual-reservoir + // enumeration must still return the full brute-force set. + const double b = ModuleBase::TWO_PI / lat0_; + const ModuleBase::Vector3 k1(0.0, 0.0, 0.5 * b); + const ModuleBase::Vector3 q(0.5 * b, 0.5 * b, 0.5 * b); + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), k1}); + + // the k+q ball must reach beyond the wavefunction grid ball, otherwise + // this test would not exercise the completing reservoir + const double need = std::sqrt((k1 + q) * (k1 + q)) + std::sqrt(gk_ecut_); + EXPECT_GT(need, std::sqrt(ggecut_)); + EXPECT_LE(need, std::sqrt(prho_.ggecut)); + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_, &prho_, q, 1); + const std::vector> ref = ReferenceSelection(k1 + q); + const std::vector> sel = KqSet(kq); + ASSERT_EQ(sel.size(), ref.size()); + for (size_t i = 0; i < ref.size(); ++i) + { + EXPECT_DOUBLE_EQ(sel[i].x, ref[i].x); + EXPECT_DOUBLE_EQ(sel[i].y, ref[i].y); + EXPECT_DOUBLE_EQ(sel[i].z, ref[i].z); + } + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + EXPECT_LE(kq.get_gk2(igl), gk_ecut_ + 1e-12); + EXPECT_GE(kq.get_ig_rho(igl), 0); + } + + // cross-check every returned rho-grid index: the rho G it points to + // must carry the same integer triplet as the k+q entry + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + const int ig = kq.get_ig_rho(igl); + ASSERT_GE(ig, 0); + const int isz = prho_.ig2isz[ig]; + const int iz = WrapIndex(isz % prho_.nz, prho_.nz); + const int ixy = prho_.is2fftixy[isz / prho_.nz]; + const int ix = WrapIndex(ixy / prho_.fftny, prho_.nx); + const int iy = WrapIndex(ixy % prho_.fftny, prho_.ny); + const ModuleBase::Vector3 want = kq.get_gcar(igl); + EXPECT_NEAR(ix * b, want.x, 1e-10); + EXPECT_NEAR(iy * b, want.y, 1e-10); + EXPECT_NEAR(iz * b, want.z, 1e-10); + } +} + +TEST_F(DFPTKQBasisTest, TranslationInvarianceOfKQ) +{ + // the k+q basis depends only on the sum k+q: (ik=0, q=k1) must agree + // with (ik=1, q=0), both centered at k1 + const ModuleBase::Vector3 k1(0.0, 0.0, 0.5 * ModuleBase::TWO_PI / lat0_); + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), k1}); + + ModuleDFPT::DFPT_KQ_Basis a, b; + a.init(&pw_, &prho_, k1, 0); + b.init(&pw_, &prho_, ModuleBase::Vector3(0.0, 0.0, 0.0), 1); + ASSERT_EQ(a.get_npwk(), b.get_npwk()); + for (int igl = 0; igl < a.get_npwk(); ++igl) + { + EXPECT_EQ(a.get_ig_rho(igl), b.get_ig_rho(igl)); + EXPECT_DOUBLE_EQ(a.get_gk2(igl), b.get_gk2(igl)); + EXPECT_DOUBLE_EQ(a.get_gpluskq(igl).x, b.get_gpluskq(igl).x); + EXPECT_DOUBLE_EQ(a.get_gpluskq(igl).y, b.get_gpluskq(igl).y); + EXPECT_DOUBLE_EQ(a.get_gpluskq(igl).z, b.get_gpluskq(igl).z); + } + + // shifting back by -k1 recovers the Gamma basis + ModuleDFPT::DFPT_KQ_Basis c; + c.init(&pw_, &prho_, ModuleBase::Vector3(0.0, 0.0, 0.0) - k1, 1); + for (int igl = 0; igl < c.get_npwk(); ++igl) + { + EXPECT_DOUBLE_EQ(c.get_gk2(igl), c.get_gcar(igl) * c.get_gcar(igl)); + } +} + +TEST_F(DFPTKQBasisTest, InvalidOrMismatchedBaseIsRejected) +{ + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + // null providers: valid-but-empty basis + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(nullptr, nullptr, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + EXPECT_FALSE(kq.is_valid()); + EXPECT_EQ(kq.get_npwk(), 0); + + // gamma_only base is rejected (DFPT needs the full complex G ball) + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0)}); + pw_.gamma_only = true; + pw_.fftny = ny_ / 2 + 1; + ModuleDFPT::DFPT_KQ_Basis kq2; + EXPECT_EXIT(kq2.init(&pw_, &prho_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0), + ::testing::ExitedWithCode(1), + ""); + + // mismatched FFT grid dimensions between the two bases are rejected + pw_.gamma_only = false; + pw_.fftny = ny_; + prho_.nx = 9; + prho_.nxyz = 9 * prho_.ny * prho_.nz; + ModuleDFPT::DFPT_KQ_Basis kq3; + EXPECT_EXIT(kq3.init(&pw_, &prho_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0), + ::testing::ExitedWithCode(1), + ""); +} + +} // namespace diff --git a/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp new file mode 100644 index 00000000000..e597882828b --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp @@ -0,0 +1,213 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#define private public +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/pseudo.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_cell/magnetism.h" +#undef private +#include "source_base/parallel_global.h" +#include "source_base/global_variable.h" +#include "source_pw/module_dfpt/dfpt_pw_data.h" +#include "source_pw/module_pwdft/dftu_base.h" +#include "dfpt_stru_fixture.h" + +// ctor/dtor stubs for the cell/spepot link closures live in the shared +// dfpt_test_mocks.cpp compiled into every DFPT test binary. + +/************************************************ + * unit test of DFPT_PW_Data (Phase 4 wiring; B4 + * absorbed the retired DFPT_IrrepData adapter) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_PW_Data::get_nq() / get_nirr() / get_irrep_modes() + * - delegates to the QList irrep data + * - DFPT_PW_Data::set/get_dpsi, drho_r, drho_g, dv_r + * - per-q storage round trip and out-of-range safety + * - DFPT_PW_Data::set/get_converged, add/get_residuals, + * set/get_current_iter + * - per-(q, irrep) SCF ledger (B4: sunk from DFPT_IrrepData) + */ + +class DFPT_PW_DataTest : public DFPTStruTestFixture +{ + protected: + ModuleCell::QList qlist; + ModuleDFPT::DFPT_PW_Data data; + std::ifstream ifs; + std::ofstream ofs; + std::ofstream ofs_running; + std::string output; + + // build a reduced 2x2x2 q-mesh (4 irreducible q-points for O_h) + void init_qlist() + { + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_dfpt_qlist"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + qlist.generate_mesh(ucell, symm, {2, 2, 2}, true); + data.init(&qlist, 1, 2, 3, 0, 1, 1, nullptr); + } + + void clear_qlist() + { + data.clean(); + ofs_running.close(); + ClearUcell(); + remove("tmp_dfpt_qlist"); + } +}; + +TEST_F(DFPT_PW_DataTest, DelegatesToQList) +{ + init_qlist(); + + EXPECT_EQ(data.get_nq(), qlist.get_nq()); + EXPECT_EQ(data.get_nq(), 4); + for (int q_idx = 0; q_idx < data.get_nq(); ++q_idx) + { + EXPECT_EQ(data.get_nirr(q_idx), 1); + EXPECT_TRUE(data.get_irrep_modes(q_idx, 0).empty()); + } + + // the first irreducible q-point must be Gamma + EXPECT_DOUBLE_EQ(qlist.get_q(0).x, 0.0); + EXPECT_DOUBLE_EQ(qlist.get_q(0).y, 0.0); + EXPECT_DOUBLE_EQ(qlist.get_q(0).z, 0.0); + + clear_qlist(); +} + +TEST_F(DFPT_PW_DataTest, AccessorsAreBoundSafe) +{ + init_qlist(); + + const int nq = data.get_nq(); + + // out-of-range access must be safe and return empty containers + EXPECT_TRUE(data.get_irrep_modes(-1, 0).empty()); + EXPECT_TRUE(data.get_irrep_modes(nq, 0).empty()); + EXPECT_TRUE(data.get_dpsi(-1, 0, 0).empty()); + EXPECT_TRUE(data.get_drho_r(0, 5).empty()); + EXPECT_TRUE(data.get_drho_g(0, 5).empty()); + EXPECT_TRUE(data.get_dv_r(0, 5).empty()); + + clear_qlist(); +} + +TEST_F(DFPT_PW_DataTest, SetterRoundTrip) +{ + init_qlist(); + + // dpsi / drho / dv storage went live with C1 (dv) and C3 (drho): + // reads must return what was written through the per-q slots + std::vector> psi(3, std::complex(1.0, 2.0)); + data.set_dpsi(0, 0, 0, psi); + std::vector rho(2, 3.0); + data.set_drho_r(0, 0, rho); + data.set_drho_g(0, 0, std::vector>(2, std::complex(1.0, 0.0))); + data.set_dv_r(0, 0, rho); + + EXPECT_FALSE(data.get_dpsi(0, 0, 0).empty()); + EXPECT_FALSE(data.get_drho_r(0, 0).empty()); + EXPECT_FALSE(data.get_drho_g(0, 0).empty()); + EXPECT_FALSE(data.get_dv_r(0, 0).empty()); + + clear_qlist(); +} + +TEST_F(DFPT_PW_DataTest, PerIrrepScfLedger) +{ + init_qlist(); + + // the ledger must be independent per (q_idx, irrep) — the shape DFPT_PW + // ::run drives and the stage-A irrep implementation will fill (B4: + // absorbed from the retired DFPT_IrrepData adapter) + data.set_converged(0, 0, false); + data.set_converged(1, 0, true); + EXPECT_FALSE(data.get_converged(0, 0)); + EXPECT_TRUE(data.get_converged(1, 0)); + + data.add_residual(0, 0, 1e-3); + data.add_residual(0, 0, 2e-4); + data.add_residual(1, 0, 9e-5); + EXPECT_EQ(data.get_residuals(0, 0).size(), 2); + EXPECT_EQ(data.get_residuals(1, 0).size(), 1); + EXPECT_NEAR(data.get_residuals(0, 0)[1], 2e-4, 1e-12); + + data.set_current_iter(0, 0, 3); + EXPECT_EQ(data.get_current_iter(0, 0), 3); + EXPECT_EQ(data.get_current_iter(1, 0), 0); // untouched key defaults to 0 + EXPECT_FALSE(data.get_converged(2, 0)); // untouched key defaults to false + EXPECT_TRUE(data.get_residuals(2, 0).empty()); + + // clean() drops the whole ledger + data.clean(); + data.init(&qlist, 1, 2, 3, 0, 1, 1, nullptr); + EXPECT_TRUE(data.get_residuals(0, 0).empty()); + EXPECT_EQ(data.get_current_iter(0, 0), 0); + EXPECT_FALSE(data.get_converged(1, 0)); + + clear_qlist(); +} + +TEST_F(DFPT_PW_DataTest, DftuReservationWithNullProvider) +{ + init_qlist(); + + // no Plus_U wired -> with_u()/u_active() must both be false and docc + // reads must return empty, so the no-DFT+U path is untouched (U0) + EXPECT_FALSE(data.with_u()); + EXPECT_FALSE(data.u_active()); + EXPECT_EQ(data.get_dftu(), nullptr); + EXPECT_TRUE(data.get_docc(0).empty()); + + // docc storage is independent of the provider: roundtrip works even + // with a null provider, out-of-range reads stay safe (U0) + std::vector> occ(4, std::complex(0.5, 0.0)); + data.set_docc(0, occ); + data.set_docc(2, occ); + ASSERT_EQ(data.get_docc(0).size(), 4); + EXPECT_DOUBLE_EQ(data.get_docc(0)[1].real(), 0.5); + EXPECT_DOUBLE_EQ(data.get_docc(0)[1].imag(), 0.0); + EXPECT_TRUE(data.get_docc(1).empty()); + EXPECT_TRUE(data.get_docc(-1).empty()); + EXPECT_TRUE(data.get_docc(7).empty()); + + clear_qlist(); +} + +TEST_F(DFPT_PW_DataTest, DftuReservationProviderUsability) +{ + init_qlist(); + + // U0 reservation semantics after the Plus_U_Base migration: with_u() + // reports the wiring, u_active() follows the occupation-matrix state of + // the provider. A default-constructed provider (occupation matrices not + // initialized, e.g. no DFT+U ran in the ground state) stays inactive; + // once the provider marks its occupation matrices initialized (a real + // ground-state DFT+U run), the reservation turns active. DFPT_PW::init + // rejects a wired provider outright; this pins the data-level contract. + Plus_U_Base dftu; + data.init(&qlist, 1, 2, 3, 0, 1, 1, &dftu); + EXPECT_TRUE(data.with_u()); + EXPECT_FALSE(data.u_active()); + ASSERT_NE(data.get_dftu(), nullptr); + + dftu.set_occ_mat_initialized(true); + EXPECT_TRUE(data.u_active()); + + data.clean(); + // clean() resets the response storage but keeps the provider wiring + EXPECT_TRUE(data.with_u()); + + clear_qlist(); +} diff --git a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp new file mode 100644 index 00000000000..444e70aba04 --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -0,0 +1,124 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#define private public +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/pseudo.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_cell/magnetism.h" +#undef private +#include "source_base/parallel_global.h" +#include "source_base/global_variable.h" +#include "source_estate/module_charge/charge_mixing.h" +#include "source_pw/module_pwdft/dftu_base.h" +#include "source_pw/module_dfpt/dfpt_pw.h" +#include "dfpt_stru_fixture.h" + +// ctor/dtor stubs for the cell/spepot/charge link closures live in the +// shared dfpt_test_mocks.cpp compiled into every DFPT test binary. + +/************************************************ + * unit test of DFPT_PW::run() (Phase 4 wiring) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_PW::init() with a reduced q-mesh + * - DFPT_PW::run() - the per-irrep SCF loop skeleton. All heavy + * solvers (pert / stern / rho) are still design-phase stubs, so + * the loop must converge on the first iteration and still invoke + * the phonon assembly / diagonalization for every irreducible q. + * - DFPT_PW::get_phonon_freq() - one frequency per phonon mode, + * i.e. 3*nat entries for each q. + */ + +class DFPT_PWRunTest : public DFPTStruTestFixture +{ + protected: + ModuleDFPT::DFPT_PW dfpt; + std::ofstream ofs_running; + std::string output; + + void SetUp() override + { + construct_ucell(stru_lib[0]); + ofs_running.open("tmp_dfpt_run"); + ModuleSymmetry::Symmetry symm; + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); + ucell.symm = symm; + } + + void TearDown() override + { + ofs_running.close(); + ClearUcell(); + remove("tmp_dfpt_run"); + } +}; + +TEST_F(DFPT_PWRunTest, RunsPerIrrepLoopForAllQ) +{ + dfpt.set_qmesh(2, 2, 2); // reduced to 4 irreducible q in O_h + dfpt.set_max_iter(10); + psi::Psi> psi; + // skeleton mode: no bases wired (design-phase fallback of the irrep loop) + dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), + ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, nullptr); + dfpt.run(); + + // each of the 4 irreducible q points must expose 3*nat phonon modes + const int expected_modes = 3 * ucell.nat; + for (int q_idx = 0; q_idx < 4; ++q_idx) + { + EXPECT_EQ(dfpt.get_phonon_freq(q_idx).size(), expected_modes); + } +} + +TEST_F(DFPT_PWRunTest, DielectricAndBornAreExposed) +{ + dfpt.set_qmesh(1, 1, 1); // Gamma-only q mesh + psi::Psi> psi; + dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), + ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, nullptr); + dfpt.run(); + + // design-phase stubs return default-constructed matrices + ModuleBase::matrix eps = dfpt.get_dielectric_tensor(); + ModuleBase::matrix born = dfpt.get_born_charges(0); + EXPECT_EQ(eps.nr, 0); + EXPECT_EQ(eps.nc, 0); + EXPECT_EQ(born.nr, 0); + EXPECT_EQ(born.nc, 0); +} + +TEST_F(DFPT_PWRunTest, DftuReservationWithProviderRejectsInit) +{ + // the preceding irrep-loop tests run OpenMP regions, so the default + // "fast" fork-based death test can deadlock in the multithreaded child + // (CI: OMP_NUM_THREADS=2); use the fork+exec style instead + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + // DFT+U reservation (U0): the ground state wires a provider when + // dft_plus_u is enabled upstream, and PW-basis DFT+U actually runs in + // the ground state now. Since every DFPT U hook (cal_docc, build_dv_u, + // dftu_onsite, born/docc contractions) is a no-op reservation, running + // anyway would converge cleanly while silently dropping the whole + // first-order U response; init() must therefore reject the run + // explicitly (same fail-loud pattern as the metallic-sampling guard). + // The accessor semantics (with_u true, u_active following the + // occupation-matrix state) are pinned separately in + // DFPT_PW_DataTest.DftuReservationProviderUsability. + Plus_U_Base dftu; + dfpt.set_qmesh(1, 1, 1); + psi::Psi> psi; + // death tests match the child's stderr, while WARNING_QUIT writes the + // NOTICE block to std::cout; bridge the two inside the statement + EXPECT_EXIT({ + std::cout.rdbuf(std::cerr.rdbuf()); + dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), + ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, &dftu); + }, ::testing::ExitedWithCode(1), "DFT\\+U with DFPT is not supported"); +} diff --git a/source/source_pw/module_dfpt/test/dfpt_stern_test.cpp b/source/source_pw/module_dfpt/test/dfpt_stern_test.cpp new file mode 100644 index 00000000000..8742ec5d093 --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_stern_test.cpp @@ -0,0 +1,437 @@ +#include "gtest/gtest.h" +#include +#include +#include +#include "source_pw/module_dfpt/dfpt_stern.h" + +/************************************************ + * unit test of DFPT_Stern (C2) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_Stern::solve - projected conjugate-gradient solution of the + * Sternheimer equation (H(k+q)-eps) P_c |dpsi> = -P_c |dV psi>. + * - apply_pv (implicitly) - the occupied-subspace projection. + * + * References are fully analytic: + * 1. a diagonal (plane-wave-kinetic-like) operator with an exact + * closed-form complement solution; + * 2. a dense Hermitian operator built from its known eigenbasis + * H = U D U^dagger (rotations x phases), the matrix analogue of a + * harmonic-oscillator Sternheimer problem in its eigenbasis, solved + * against the analytic spectral expansion; + * 3. projection properties and degenerate right-hand sides. + */ + +namespace { + +unsigned g_seed = 20260814u; +double test_rand() +{ + g_seed = g_seed * 1664525u + 1013904223u; + return ((g_seed >> 8) & 0xffffff) / 16777216.0 * 2.0 - 1.0; +} + +std::complex crand() +{ + return std::complex(test_rand(), test_rand()); +} + +// Re +double vdot(const std::vector>& a, const std::vector>& b) +{ + double s = 0.0; + for (size_t i = 0; i < a.size(); ++i) + { + s += a[i].real() * b[i].real() + a[i].imag() * b[i].imag(); + } + return s; +} + +// diagonal shifted operator: y_i = (d_i - eps) x_i +class DiagonalOperator : public ModuleDFPT::DFPT_Stern::LinearOperator +{ + public: + DiagonalOperator(std::vector d, double eps) : d_(std::move(d)), eps_(eps) + { + } + int dimension() const override + { + return static_cast(d_.size()); + } + void apply(const std::complex* x, std::complex* y) const override + { + for (size_t i = 0; i < d_.size(); ++i) + { + y[i] = (d_[i] - eps_) * x[i]; + } + } + + private: + std::vector d_; + double eps_; +}; + +// dense Hermitian operator with a known eigenbasis: apply (H - eps I), +// H = U D U^dagger +class EigenbasisOperator : public ModuleDFPT::DFPT_Stern::LinearOperator +{ + public: + EigenbasisOperator(const std::vector>>& u, + const std::vector& lambda, + double eps) + : lambda_(lambda), eps_(eps) + { + const int n = static_cast(lambda.size()); + h_.assign(n, std::vector>(n, std::complex(0.0, 0.0))); + for (int j = 0; j < n; ++j) + { + for (int i = 0; i < n; ++i) + { + for (int k = 0; k < n; ++k) + { + // H(i,k) += lambda_j u(i,j) conj(u(k,j)) + h_[i][k] += lambda_[j] * u[i][j] * std::conj(u[k][j]); + } + } + } + } + int dimension() const override + { + return static_cast(lambda_.size()); + } + void apply(const std::complex* x, std::complex* y) const override + { + const int n = static_cast(lambda_.size()); + for (int i = 0; i < n; ++i) + { + std::complex s(0.0, 0.0); + for (int k = 0; k < n; ++k) + { + s += h_[i][k] * x[k]; + } + y[i] = s - eps_ * x[i]; + } + } + + private: + std::vector>> h_; + std::vector lambda_; + double eps_; +}; + +} // namespace + +TEST(DFPTSternTest, SolvesDiagonalSystemExactlyOnTheComplement) +{ + const int n = 40; + const int nocc = 5; + std::vector d(n); + for (int i = 0; i < n; ++i) + { + d[i] = 1.0 + 0.37 * i; + } + const double eps = 2.0; // below every complement eigenvalue d_i - eps > 0 + DiagonalOperator aop(d, eps); + + std::vector>> occ(nocc, std::vector>(n)); + for (int m = 0; m < nocc; ++m) + { + for (int i = 0; i < n; ++i) + { + occ[m][i] = (i == m) ? std::complex(1.0, 0.0) : std::complex(0.0, 0.0); + } + } + std::vector> b(n); + for (int i = 0; i < n; ++i) + { + b[i] = crand(); + } + // analytic reference: x_i = (P_c b)_i / (d_i - eps), zero inside the occ space + std::vector> ref = b; + for (int m = 0; m < nocc; ++m) + { + for (int i = 0; i < n; ++i) + { + ref[i] -= occ[m][i] * std::conj(occ[m][i]) * b[i]; + } + } + for (int i = 0; i < nocc; ++i) + { + ref[i] = std::complex(0.0, 0.0); + } + for (int i = nocc; i < n; ++i) + { + ref[i] /= (d[i] - eps); + } + + ModuleDFPT::DFPT_Stern stern; + std::vector> dpsi; + double residual = 1.0; + const int used = stern.solve(aop, occ, b, 200, 1.0e-10, dpsi, residual); + EXPECT_GT(used, 0); + EXPECT_LT(residual, 1.0e-10); + ASSERT_EQ(dpsi.size(), static_cast(n)); + double err2 = 0.0; + double ref2 = 0.0; + for (int i = 0; i < n; ++i) + { + err2 += std::norm(dpsi[i] - ref[i]); + ref2 += std::norm(ref[i]); + } + EXPECT_LT(std::sqrt(err2 / ref2), 1.0e-8); +} + +TEST(DFPTSternTest, SolvesDenseHermitianSystemAgainstSpectralReference) +{ + const int n = 24; + const int nocc = 4; + // unitary U = (Givens rotations) * diag(phases) + std::vector>> u(n, std::vector>(n)); + for (int i = 0; i < n; ++i) + { + for (int j = 0; j < n; ++j) + { + u[i][j] = (i == j) ? std::complex(1.0, 0.0) : std::complex(0.0, 0.0); + } + } + for (int j = 0; j < n; ++j) + { + const std::complex ph = std::polar(1.0, 0.31 * j + 0.11); + for (int i = 0; i < n; ++i) + { + u[i][j] *= ph; + } + } + for (int j = 0; j + 1 < n; ++j) + { + // rotate the (j, j+1) plane of every column + const double theta = 0.23 + 0.05 * j; + const double c = std::cos(theta); + const double s = std::sin(theta); + for (int i = 0; i < n; ++i) + { + const std::complex a = u[i][j]; + const std::complex b = u[i][j + 1]; + u[i][j] = c * a - s * b; + u[i][j + 1] = s * a + c * b; + } + } + std::vector lambda(n); + for (int j = 0; j < n; ++j) + { + lambda[j] = 1.5 + 0.4 * j; + } + const double eps = 1.7; // inside the occ spectrum: indefinite/null directions + EigenbasisOperator aop(u, lambda, eps); + + // occupied set = first nocc eigen-columns (machine-orthonormal) + std::vector>> occ(nocc, std::vector>(n)); + for (int m = 0; m < nocc; ++m) + { + for (int i = 0; i < n; ++i) + { + occ[m][i] = u[i][m]; + } + } + std::vector> b(n); + for (int i = 0; i < n; ++i) + { + b[i] = crand(); + } + // spectral reference: x = sum_{j >= nocc} u_j (u_j^dag b) / (lambda_j - eps) + std::vector> ref(n, std::complex(0.0, 0.0)); + for (int j = nocc; j < n; ++j) + { + std::complex c(0.0, 0.0); + for (int i = 0; i < n; ++i) + { + c += std::conj(u[i][j]) * b[i]; + } + for (int i = 0; i < n; ++i) + { + ref[i] += u[i][j] * c / (lambda[j] - eps); + } + } + + ModuleDFPT::DFPT_Stern stern; + std::vector> dpsi; + double residual = 1.0; + const int used = stern.solve(aop, occ, b, 500, 1.0e-11, dpsi, residual); + EXPECT_GT(used, 0); + EXPECT_LT(residual, 1.0e-10); + double err2 = 0.0; + double ref2 = 0.0; + for (int i = 0; i < n; ++i) + { + err2 += std::norm(dpsi[i] - ref[i]); + ref2 += std::norm(ref[i]); + } + EXPECT_LT(std::sqrt(err2 / ref2), 1.0e-7); +} + +TEST(DFPTSternTest, SolutionStaysOrthogonalToOccupiedStates) +{ + const int n = 32; + const int nocc = 6; + std::vector>> occ; + for (int m = 0; m < nocc; ++m) + { + std::vector> v(n); + for (int i = 0; i < n; ++i) + { + v[i] = crand(); + } + // orthonormalize against the previous ones + for (size_t k = 0; k < occ.size(); ++k) + { + std::complex c(0.0, 0.0); + for (int i = 0; i < n; ++i) + { + c += std::conj(occ[k][i]) * v[i]; + } + for (int i = 0; i < n; ++i) + { + v[i] -= c * occ[k][i]; + } + } + const double nrm = std::sqrt(vdot(v, v)); + for (int i = 0; i < n; ++i) + { + v[i] /= nrm; + } + occ.push_back(v); + } + // random Hermitian positive definite operator H = A A^dag / n + I + std::vector>> a(n, std::vector>(n)); + for (int i = 0; i < n; ++i) + { + for (int j = 0; j < n; ++j) + { + a[i][j] = crand() / std::sqrt(static_cast(n)); + } + } + class PDOperator : public ModuleDFPT::DFPT_Stern::LinearOperator + { + public: + explicit PDOperator(std::vector>> a) : a_(std::move(a)) + { + const int n = static_cast(a_.size()); + h_.assign(n, std::vector>(n)); + for (int i = 0; i < n; ++i) + { + for (int k = 0; k < n; ++k) + { + std::complex s(0.0, 0.0); + for (int j = 0; j < n; ++j) + { + s += a_[i][j] * std::conj(a_[k][j]); + } + h_[i][k] = s + ((i == k) ? std::complex(1.0, 0.0) : std::complex(0.0, 0.0)); + } + } + } + int dimension() const override + { + return static_cast(a_.size()); + } + void apply(const std::complex* x, std::complex* y) const override + { + const int n = static_cast(a_.size()); + for (int i = 0; i < n; ++i) + { + std::complex s(0.0, 0.0); + for (int k = 0; k < n; ++k) + { + s += h_[i][k] * x[k]; + } + y[i] = s; + } + } + + private: + std::vector>> a_; + std::vector>> h_; + }; + PDOperator aop(a); + + std::vector> b(n); + for (int i = 0; i < n; ++i) + { + b[i] = crand(); + } + ModuleDFPT::DFPT_Stern stern; + std::vector> dpsi; + double residual = 1.0; + const int used = stern.solve(aop, occ, b, 500, 1.0e-10, dpsi, residual); + EXPECT_GT(used, 0); + EXPECT_LT(residual, 1.0e-9); + ASSERT_EQ(dpsi.size(), static_cast(n)); + for (int m = 0; m < nocc; ++m) + { + std::complex c(0.0, 0.0); + for (int i = 0; i < n; ++i) + { + c += std::conj(occ[m][i]) * dpsi[i]; + } + EXPECT_LT(std::abs(c), 1.0e-9); + } +} + +TEST(DFPTSternTest, DegenerateRightHandSideInOccupiedSubspace) +{ + // b fully inside the occ subspace: dpsi must be exactly zero + const int n = 16; + const int nocc = 3; + std::vector>> occ(nocc, std::vector>(n)); + for (int m = 0; m < nocc; ++m) + { + for (int i = 0; i < n; ++i) + { + occ[m][i] = (i == m) ? std::complex(1.0, 0.0) : std::complex(0.0, 0.0); + } + } + std::vector d(n); + for (int i = 0; i < n; ++i) + { + d[i] = 1.0 + i; + } + DiagonalOperator aop(d, 2.5); + std::vector> b(n, std::complex(0.0, 0.0)); + for (int m = 0; m < nocc; ++m) + { + for (int i = 0; i < n; ++i) + { + b[i] += (0.3 * m + 0.1) * occ[m][i]; + } + } + ModuleDFPT::DFPT_Stern stern; + std::vector> dpsi; + double residual = 1.0; + const int used = stern.solve(aop, occ, b, 100, 1.0e-10, dpsi, residual); + EXPECT_EQ(used, 0); + EXPECT_EQ(residual, 0.0); + ASSERT_EQ(dpsi.size(), static_cast(n)); + for (int i = 0; i < n; ++i) + { + EXPECT_EQ(dpsi[i], std::complex(0.0, 0.0)); + } +} + +TEST(DFPTSternTest, ZeroRightHandSideReturnsImmediately) +{ + DiagonalOperator aop(std::vector(8, 1.0), 0.5); + std::vector> b(8, std::complex(0.0, 0.0)); + ModuleDFPT::DFPT_Stern stern; + std::vector> dpsi; + double residual = 1.0; + const int used = stern.solve(aop, std::vector>>(), b, 50, 1.0e-8, dpsi, residual); + EXPECT_EQ(used, 0); + EXPECT_EQ(residual, 0.0); + ASSERT_EQ(dpsi.size(), 8u); + for (int i = 0; i < 8; ++i) + { + EXPECT_EQ(dpsi[i], std::complex(0.0, 0.0)); + } +} diff --git a/source/source_pw/module_dfpt/test/dfpt_stru_fixture.cpp b/source/source_pw/module_dfpt/test/dfpt_stru_fixture.cpp new file mode 100644 index 00000000000..4df392da1cf --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_stru_fixture.cpp @@ -0,0 +1,72 @@ +#include "dfpt_stru_fixture.h" + +#include "source_base/mathzone.h" + +DFPTStruTestFixture::DFPTStruTestFixture() +{ + stru_lib.push_back(stru_{1, + "O_h", + "m-3m", + "Pm-3m", + std::vector{1., 0., 0., 0., 1., 0., 0., 0., 1.}, + std::vector{atomtype_{"C", + std::vector>{ + {0., 0., 0.}, + }}}}); +} + +void DFPTStruTestFixture::construct_ucell(stru_& stru) +{ + std::vector coord = stru.all_type; + ucell.a1 = ModuleBase::Vector3(stru.cell[0], stru.cell[1], stru.cell[2]); + ucell.a2 = ModuleBase::Vector3(stru.cell[3], stru.cell[4], stru.cell[5]); + ucell.a3 = ModuleBase::Vector3(stru.cell[6], stru.cell[7], stru.cell[8]); + ucell.ntype = stru.all_type.size(); + ucell.atoms = new Atom[ucell.ntype]; + ucell.nat = 0; + ucell.latvec.e11 = ucell.a1.x; + ucell.latvec.e12 = ucell.a1.y; + ucell.latvec.e13 = ucell.a1.z; + ucell.latvec.e21 = ucell.a2.x; + ucell.latvec.e22 = ucell.a2.y; + ucell.latvec.e23 = ucell.a2.z; + ucell.latvec.e31 = ucell.a3.x; + ucell.latvec.e32 = ucell.a3.y; + ucell.latvec.e33 = ucell.a3.z; + ucell.GT = ucell.latvec.Inverse(); + ucell.G = ucell.GT.Transpose(); + ucell.lat0 = 1.8897261254578281; + for (size_t i = 0; i < coord.size(); i++) + { + ucell.atoms[i].label = coord[i].atomname; + ucell.atoms[i].na = coord[i].coordinate.size(); + ucell.atoms[i].tau.resize(ucell.atoms[i].na); + ucell.atoms[i].taud.resize(ucell.atoms[i].na); + for (int j = 0; j < ucell.atoms[i].na; ++j) + { + std::vector this_atom = coord[i].coordinate[j]; + ucell.atoms[i].tau[j] = ModuleBase::Vector3(this_atom[0], this_atom[1], this_atom[2]); + ModuleBase::Mathzone::Cartesian_to_Direct(ucell.atoms[i].tau[j].x, + ucell.atoms[i].tau[j].y, + ucell.atoms[i].tau[j].z, + ucell.a1.x, + ucell.a1.y, + ucell.a1.z, + ucell.a2.x, + ucell.a2.y, + ucell.a2.z, + ucell.a3.x, + ucell.a3.y, + ucell.a3.z, + ucell.atoms[i].taud[j].x, + ucell.atoms[i].taud[j].y, + ucell.atoms[i].taud[j].z); + } + ucell.nat += ucell.atoms[i].na; + } +} + +void DFPTStruTestFixture::ClearUcell() +{ + delete[] ucell.atoms; +} diff --git a/source/source_pw/module_dfpt/test/dfpt_stru_fixture.h b/source/source_pw/module_dfpt/test/dfpt_stru_fixture.h new file mode 100644 index 00000000000..ab31094e92e --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_stru_fixture.h @@ -0,0 +1,49 @@ +#ifndef DFPT_STRU_FIXTURE_H +#define DFPT_STRU_FIXTURE_H + +#include +#include +#include "source_cell/unitcell.h" +#include "gtest/gtest.h" + +// Shared gtest fixture for building a minimal cubic UnitCell from a +// hand-written structure table (abbreviated from +// module_symmetry/test/symm_test.cpp and klist_test.cpp). Used by the +// MPI-side DFPT tests that drive the QList / DFPT_PW wiring +// (dfpt_pw_data_test.cpp, dfpt_pw_run_test.cpp). +// +// NOTE ON INCLUDE ORDER: every test that needs UnitCell private members +// includes the cell headers with `#define private public` BEFORE this +// header; the include guards then keep the fixture header's own includes +// inert. The fixture implementation (dfpt_stru_fixture.cpp) only touches +// public members, so it compiles without the define. + +struct atomtype_ +{ + std::string atomname; + std::vector> coordinate; +}; + +struct stru_ +{ + int ibrav; + std::string point_group; // Schoenflies symbol + std::string point_group_hm; // Hermann-Mauguin notation. + std::string space_group; + std::vector cell; + std::vector all_type; +}; + +class DFPTStruTestFixture : public testing::Test +{ + protected: + UnitCell ucell; + std::vector stru_lib; + + DFPTStruTestFixture(); + + void construct_ucell(stru_& stru); + void ClearUcell(); +}; + +#endif // DFPT_STRU_FIXTURE_H diff --git a/source/source_pw/module_dfpt/test/dfpt_test_mocks.cpp b/source/source_pw/module_dfpt/test/dfpt_test_mocks.cpp new file mode 100644 index 00000000000..48ff22ad59e --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_test_mocks.cpp @@ -0,0 +1,85 @@ +// ============================================================ +// Minimal test-support ctor/dtor stubs shared by all DFPT unit +// test binaries (test/ and test_serial/), mirroring the +// tmp_mocks.cpp convention of the other module test suites. +// +// In production these symbols live in full link closures the DFPT +// tests do not want to pull in: +// - cell/spepot/stru_fac/charge_mixing closures (via UnitCell), +// - the pwdft DFT+U closure (Plus_U_Base, see the former +// dftu_test_support.cpp this file absorbed). +// The DFPT tests only need default-constructible objects, so the +// empty definitions are replicated here once instead of in every +// test translation unit. Keep the signatures in sync with the +// production sources. +// ============================================================ + +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/magnetism.h" +#include "source_cell/pseudo.h" +#include "source_cell/unitcell.h" +#include "source_estate/module_charge/charge_mixing.h" +#include "source_pw/module_pwdft/dftu_base.h" +#include "source_pw/module_pwdft/stru_fac.h" + +pseudo::pseudo() +{ +} +pseudo::~pseudo() +{ +} +Atom::Atom() +{ +} +Atom::~Atom() +{ +} +Atom_pseudo::Atom_pseudo() +{ +} +Atom_pseudo::~Atom_pseudo() +{ +} +SepPot::SepPot() +{ +} +SepPot::~SepPot() +{ +} +Sep_Cell::Sep_Cell() noexcept +{ +} +Sep_Cell::~Sep_Cell() noexcept +{ +} +UnitCell::UnitCell() +{ +} +UnitCell::~UnitCell() +{ +} +Magnetism::Magnetism() +{ +} +Magnetism::~Magnetism() +{ +} + +Structure_Factor::Structure_Factor() +{ +} +Structure_Factor::~Structure_Factor() +{ +} + +Charge_Mixing::~Charge_Mixing() +{ +} + +Plus_U_Base::Plus_U_Base() +{ +} +Plus_U_Base::~Plus_U_Base() +{ +} diff --git a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt new file mode 100644 index 00000000000..c82aebdf193 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -0,0 +1,99 @@ +abacus_disable_feature_definitions(__MPI) +abacus_disable_feature_definitions(__EXX) +abacus_disable_feature_definitions(__CUDA) +abacus_disable_feature_definitions(__UT_USE_CUDA) +abacus_disable_feature_definitions(__ROCM) +abacus_disable_feature_definitions(__UT_USE_ROCM) +abacus_disable_feature_definitions(__MLALGO) + +# The DFPT first-order potential kernels (dfpt_pert.cpp) drive the plane-wave +# FFT machinery directly (recip2real / real2recip on the shared rho/wfc grid). +# PW_Basis changes its member layout under __MPI, so the pw sources linked +# here must be compiled without __MPI in the same translation-unit ABI as the +# dfpt sources (mirrors module_pw/test_serial). +add_library( + dfpt_planewave_serial + OBJECT + ../../../source_base/module_fft/fft_bundle.cpp + ../../../source_base/module_fft/fft_cpu.cpp + ../../../source_basis/module_pw/pw_basis.cpp + ../../../source_basis/module_pw/pw_basis_k.cpp + ../../../source_basis/module_pw/pw_basis_sup.cpp + ../../../source_basis/module_pw/pw_distributeg.cpp + ../../../source_basis/module_pw/pw_distg_method1.cpp + ../../../source_basis/module_pw/pw_distg_method2.cpp + ../../../source_basis/module_pw/pw_distributer.cpp + ../../../source_basis/module_pw/pw_init.cpp + ../../../source_basis/module_pw/pw_transform.cpp + ../../../source_basis/module_pw/pw_transform_k.cpp +) + +AddTest( + TARGET MODULE_DFPT_pert_serial + LIBS parameter dfpt_planewave_serial device base symmetry + SOURCES dfpt_pert_serial_test.cpp + ../dfpt_pert.cpp + ../dfpt_pw_data.cpp + ../dfpt_kq_basis.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp + # Shared serial-side cell/basis/data fixture. + dfpt_serial_fixture.cpp +) + +AddTest( + TARGET MODULE_DFPT_rho_serial + LIBS parameter dfpt_planewave_serial device base symmetry + SOURCES dfpt_rho_serial_test.cpp + ../dfpt_rho.cpp + ../dfpt_pw_data.cpp + ../dfpt_kq_basis.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp + # Shared serial-side cell/basis/data fixture. + dfpt_serial_fixture.cpp +) + +AddTest( + TARGET MODULE_DFPT_phon_serial + LIBS parameter dfpt_planewave_serial device base symmetry + SOURCES dfpt_phon_serial_test.cpp + ../dfpt_phon.cpp + ../dfpt_pert.cpp + ../dfpt_pw_data.cpp + ../dfpt_kq_basis.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp + # Shared serial-side cell/basis/data fixture. + dfpt_serial_fixture.cpp +) + +AddTest( + TARGET MODULE_DFPT_q0_serial + LIBS parameter dfpt_planewave_serial device base symmetry + SOURCES dfpt_q0_serial_test.cpp + ../dfpt_q0.cpp + ../dfpt_pert.cpp + ../dfpt_pw_data.cpp + ../dfpt_kq_basis.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp + # Shared serial-side cell/basis/data fixture. + dfpt_serial_fixture.cpp +) diff --git a/source/source_pw/module_dfpt/test_serial/dfpt_pert_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_pert_serial_test.cpp new file mode 100644 index 00000000000..b080c97cde9 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_pert_serial_test.cpp @@ -0,0 +1,470 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include + +// serial unit test of the first-order perturbation potential (C1). +// Everything here runs without __MPI: the plane-wave bases are built through +// the real serial initgrids/initparameters/setuptransform path on a shared +// FFT grid, exactly like the production setup_pwrho/setup_pwwfc sequence. + +#define private public +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/pseudo.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_cell/magnetism.h" +#include "source_pw/module_pwdft/stru_fac.h" +#include "source_pw/module_dfpt/dfpt_pert.h" +#undef private + +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_pw/module_pwdft/dftu_base.h" +#include "source_psi/psi.h" +#include "dfpt_serial_fixture.h" + +// ctor/dtor stubs for the cell/spepot/stru_fac link closures live in the +// shared test/dfpt_test_mocks.cpp compiled into every DFPT test binary. + +/************************************************ + * serial unit test of DFPT_Pert (C1) + ***********************************************/ + +/** + * - Tested Functions: + * - rho_gvec: reconstruction of rho-grid G vectors must coincide with the + * distributed gcar array filled by the real serial collect_local_pw(). + * - dVloc_dtau: the analytic first-order local potential is validated + * against a finite difference of the full displaced potential + * vloc(|Delta+q|) * exp(i 2pi (Delta+q).tau) for q != 0. + * - build_dv + apply_dv: the stored reciprocal coefficients and the FFT + * convolution dpsi(G'') = sum_G' psi(G') c(G''-G') are checked against + * the analytic matrix elements on the k+q basis (no extra q-phase). + * - build_efield: -E.r ramp layout and its closed-form discrete FT. + * - build_vkb: l=0 projector against an independent hand-rolled Simpson + * transform; pure phase response under a tau shift. + * - dVnl_dtau: the two-term NC identity is validated against a finite + * difference of the displaced separable operator; USPP is rejected. + * - build_dv under with_u()/u_active()==false (pure-PW DFT+U safety). + */ + +class DFPTPertSerialTest : public DFPTSerialBase +{ + protected: + Structure_Factor sf_; + ModuleDFPT::DFPT_Pert pert_; + + void SetUp() override + { + DFPTSerialBase::SetUp(); + pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); + } +}; + +TEST_F(DFPTPertSerialTest, RhoGvecMatchesDistributedGcar) +{ + ASSERT_GT(pw_rho_.npw, 0); + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + ModuleBase::Vector3 g; + pert_.rho_gvec(ig, g); + EXPECT_DOUBLE_EQ(g.x, pw_rho_.gcar[ig].x); + EXPECT_DOUBLE_EQ(g.y, pw_rho_.gcar[ig].y); + EXPECT_DOUBLE_EQ(g.z, pw_rho_.gcar[ig].z); + } +} + +TEST_F(DFPTPertSerialTest, DVlocDtauMatchesFiniteDifference) +{ + const ModuleBase::Vector3 qs[2] = {q_cart_, ModuleBase::Vector3(0.0, 0.0, 0.0)}; + const double eps = 1.0e-6; // lat0 units + for (int iq = 0; iq < 2; ++iq) + { + for (int dir = 0; dir < 3; dir += 2) + { + std::vector> dv; + pert_.dVloc_dtau(0, dir, qs[iq], dv); + ASSERT_EQ(dv.size(), static_cast(pw_rho_.npw)); + ModuleBase::Vector3 d(0.0, 0.0, 0.0); + d[dir] = 1.0; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + const ModuleBase::Vector3 w = pw_rho_.gcar[ig] + qs[iq]; + if (w * w < 1.0e-12) + { + EXPECT_EQ(dv[ig], std::complex(0.0, 0.0)); + continue; + } + // finite difference of vloc(|Delta+q|) e^{-i 2pi (Delta+q).tau} + // per bohr of displacement (GS stru_fac phase convention) + const double ap = -ModuleBase::TWO_PI * (w * (tau_ + eps * d)); + const double am = -ModuleBase::TWO_PI * (w * (tau_ - eps * d)); + const std::complex fd = VlocCoulomb((w * w) * ucell_.tpiba2) + * (std::polar(1.0, ap) - std::polar(1.0, am)) + / (2.0 * eps * lat0_); + EXPECT_NEAR(dv[ig].real(), fd.real(), 1.0e-9); + EXPECT_NEAR(dv[ig].imag(), fd.imag(), 1.0e-9); + } + } + } +} + +TEST_F(DFPTPertSerialTest, ApplyDvConvolutionMatchesAnalyticMatrixElement) +{ + pert_.build_dv(0, 0, 0, data_); // atom 0 displaced along x, q = q_d_ + EXPECT_EQ(data_.get_pert_atom(), 0); + EXPECT_EQ(data_.get_pert_dir(), 0); + + // stored reciprocal coefficients equal the analytic ones + const std::vector> dv_recip = data_.get_dv_recip_c(0, 0); + ASSERT_EQ(dv_recip.size(), static_cast(pw_rho_.npw)); + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + const std::complex expect = AnalyticDVloc(0, pw_rho_.gcar[ig] + q_cart_); + EXPECT_NEAR(dv_recip[ig].real(), expect.real(), 1.0e-9); + EXPECT_NEAR(dv_recip[ig].imag(), expect.imag(), 1.0e-9); + } + + // wavefunctions: band 0 = single plane wave at k, band 1 = two components + const int npwk = pw_wfc_.npwk[0]; + const ModuleBase::Vector3 kc = pw_wfc_.kvec_c[0]; + int ig_zero = -1, ig_x = -1, ig_y = -1; + for (int ig = 0; ig < npwk; ++ig) + { + const long long key = GKey(pw_wfc_.getgpluskcar(0, ig) - kc); + if (key == FKey(0, 0, 0)) + { + ig_zero = ig; + } + else if (key == FKey(1, 0, 0)) + { + ig_x = ig; + } + else if (key == FKey(0, 1, 0)) + { + ig_y = ig; + } + } + ASSERT_GE(ig_zero, 0); + ASSERT_GE(ig_x, 0); + ASSERT_GE(ig_y, 0); + + psi::Psi> psi(1, 2, npwk, npwk, true); + psi.zero_out(); + psi(0, 0, ig_zero) = std::complex(1.0, 0.0); + psi(0, 1, ig_x) = std::complex(0.7, 0.0); + psi(0, 1, ig_y) = std::complex(0.3, 0.2); + + pert_.apply_dv(0, 0, psi, data_); + + // expected: dpsi(G'') = sum_G' psi(G') c(G''-G'), c = analytic dVloc + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + const ModuleBase::Vector3 g1(0.1, 0.0, 0.0), g2(0.0, 0.1, 0.0); + const std::vector> d0 = data_.get_dpsi(0, 0, 0); + const std::vector> d1 = data_.get_dpsi(0, 0, 1); + ASSERT_EQ(d0.size(), static_cast(kq.get_npwk())); + ASSERT_EQ(d1.size(), static_cast(kq.get_npwk())); + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + const ModuleBase::Vector3 gpp = kq.get_gcar(igl); + const std::complex e0 = AnalyticDVloc(0, gpp + q_cart_); + const std::complex e1 = 0.7 * AnalyticDVloc(0, gpp - g1 + q_cart_) + + std::complex(0.3, 0.2) * AnalyticDVloc(0, gpp - g2 + q_cart_); + EXPECT_NEAR(d0[igl].real(), e0.real(), 1.0e-8); + EXPECT_NEAR(d0[igl].imag(), e0.imag(), 1.0e-8); + EXPECT_NEAR(d1[igl].real(), e1.real(), 1.0e-8); + EXPECT_NEAR(d1[igl].imag(), e1.imag(), 1.0e-8); + } +} + +TEST_F(DFPTPertSerialTest, BuildEfieldRampMatchesClosedForm) +{ + const double E0 = 0.5; + const double Lx = 10.0 * lat0_; + pert_.build_efield(ModuleBase::Vector3(E0, 0.0, 0.0), data_); + const std::vector> dv = data_.get_dv_rc(0, 0); + ASSERT_EQ(dv.size(), static_cast(pw_rho_.nrxx)); + const int nx = pw_rho_.nx; + + // real-space values form a pure x ramp -E0 (ix/nx) Lx + for (int ir = 0; ir < pw_rho_.nrxx; ++ir) + { + const double j = std::llround(-dv[ir].real() / (E0 * Lx) * nx); + EXPECT_GE(j, 0.0); + EXPECT_LE(j, nx - 1.0); + EXPECT_NEAR(dv[ir].real(), -E0 * (j / nx) * Lx, 1.0e-10); + EXPECT_EQ(dv[ir].imag(), 0.0); + } + + // closed-form discrete FT of the sawtooth discriminates the (ix,iy,iz) + // layout: c(k) = E0 Lx / (nx (1 - w)), w = exp(-2 pi i k / nx), k != 0 + std::vector> recip(pw_rho_.npw); + pw_rho_.real2recip(dv.data(), recip.data()); + int ig_p100 = -1, ig_m100 = -1, ig_p110 = -1, ig_p010 = -1; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + const long long key = GKey(pw_rho_.gcar[ig]); + if (key == FKey(1, 0, 0)) + { + ig_p100 = ig; + } + else if (key == FKey(-1, 0, 0)) + { + ig_m100 = ig; + } + else if (key == FKey(1, 1, 0)) + { + ig_p110 = ig; + } + else if (key == FKey(0, 1, 0)) + { + ig_p010 = ig; + } + } + ASSERT_GE(ig_p100, 0); + ASSERT_GE(ig_m100, 0); + ASSERT_GE(ig_p110, 0); + ASSERT_GE(ig_p010, 0); + const std::complex w1 = std::polar(1.0, -ModuleBase::TWO_PI / nx); + const std::complex nx_c(nx); + const std::complex c1 = E0 * Lx / (nx_c * (1.0 - w1)); + const std::complex cm1 = E0 * Lx / (nx_c * (1.0 - std::conj(w1))); + EXPECT_NEAR(recip[ig_p100].real(), c1.real(), 1.0e-8); + EXPECT_NEAR(recip[ig_p100].imag(), c1.imag(), 1.0e-8); + EXPECT_NEAR(recip[ig_m100].real(), cm1.real(), 1.0e-8); + EXPECT_NEAR(recip[ig_m100].imag(), cm1.imag(), 1.0e-8); + EXPECT_NEAR(std::abs(recip[ig_p110]), 0.0, 1.0e-10); + EXPECT_NEAR(std::abs(recip[ig_p010]), 0.0, 1.0e-10); +} + +TEST_F(DFPTPertSerialTest, BuildVkbL0MatchesIndependentSimpson) +{ + MakeNCAtom(); + const int npwk = pw_wfc_.npwk[0]; + std::vector> gk(npwk); + for (int ig = 0; ig < npwk; ++ig) + { + gk[ig] = pw_wfc_.getgpluskcar(0, ig); + } + std::vector>> vkb; + pert_.build_vkb(0, 0, gk, vkb); + ASSERT_EQ(vkb.size(), 4u); // l=0 gives one row, l=1 gives three rows + + const pseudo& p = ucell_.atoms[0].ncpp; + const double dx = p.rab[0]; + const double pref = ModuleBase::FOUR_PI / std::sqrt(ucell_.omega); + auto simpson = [&](const std::function& f, int n) + { + double s = f(0) + f(n - 1); + for (int i = 1; i < n - 1; ++i) + { + s += f(i) * ((i % 2 == 1) ? 4.0 : 2.0); + } + return s * dx / 3.0; + }; + + for (int ig = 0; ig < npwk; ++ig) + { + const double g = std::sqrt(gk[ig] * gk[ig]) * ucell_.tpiba; // bohr^-1 + // independent j0 and Simpson transform (no ModuleBase Sphbes/Integral) + auto f0 = [&](int i) + { + const double gr = g * p.r[i]; + const double j0 = (gr < 1.0e-12) ? 1.0 : std::sin(gr) / gr; + return p.betar(0, i) * j0 * p.r[i]; + }; + const double vq = pref * simpson(f0, p.msh); + const double arg = -ModuleBase::TWO_PI * (gk[ig] * tau_); + const std::complex expect = 0.5 * std::sqrt(1.0 / ModuleBase::PI) * vq + * std::complex(std::cos(arg), std::sin(arg)); + EXPECT_NEAR(vkb[0][ig].real(), expect.real(), 1.0e-9 * std::max(1.0, std::abs(expect))); + EXPECT_NEAR(vkb[0][ig].imag(), expect.imag(), 1.0e-9 * std::max(1.0, std::abs(expect))); + } + + // a tau shift changes every projector by the pure phase e^{-i 2pi gk.dtau} + const ModuleBase::Vector3 dtau(0.07, -0.11, 0.05); + ucell_.atoms[0].tau[0] = tau_ + dtau; + std::vector>> vkb2; + pert_.build_vkb(0, 0, gk, vkb2); + ucell_.atoms[0].tau[0] = tau_; + for (int mu = 0; mu < 4; ++mu) + { + for (int ig = 0; ig < npwk; ++ig) + { + if (std::abs(vkb[mu][ig]) < 1.0e-14) + { + continue; + } + const double arg = -ModuleBase::TWO_PI * (gk[ig] * dtau); + const std::complex expect(std::cos(arg), std::sin(arg)); + const std::complex ratio = vkb2[mu][ig] / vkb[mu][ig]; + EXPECT_NEAR(ratio.real(), expect.real(), 1.0e-9); + EXPECT_NEAR(ratio.imag(), expect.imag(), 1.0e-9); + } + } +} + +TEST_F(DFPTPertSerialTest, DVnlDtauMatchesOperatorFiniteDifference) +{ + MakeNCAtom(); + const int npwk = pw_wfc_.npwk[0]; + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + const int npwkq = kq.get_npwk(); + std::vector> gk_in(npwk), gk_out(npwkq); + for (int ig = 0; ig < npwk; ++ig) + { + gk_in[ig] = pw_wfc_.getgpluskcar(0, ig); + } + for (int igl = 0; igl < npwkq; ++igl) + { + gk_out[igl] = kq.get_gpluskq(igl); + } + + // deterministic pseudo-random wavefunctions, normalized per band + psi::Psi> psi(1, 2, npwk, npwk, true); + unsigned seed = 20260814u; + auto rnd = [&]() + { + seed = seed * 1664525u + 1013904223u; + return ((seed >> 8) & 0xffffff) / 16777216.0 * 2.0 - 1.0; + }; + for (int b = 0; b < 2; ++b) + { + double nrm = 0.0; + for (int ig = 0; ig < npwk; ++ig) + { + psi(0, b, ig) = std::complex(rnd(), rnd()); + nrm += std::norm(psi(0, b, ig)); + } + nrm = std::sqrt(nrm); + for (int ig = 0; ig < npwk; ++ig) + { + psi(0, b, ig) /= nrm; + } + } + + const int dir = 1; // y displacement + std::vector>> dv_psi; + pert_.dVnl_dtau(0, dir, q_cart_, psi, 0, dv_psi); + ASSERT_EQ(dv_psi.size(), 2u); + ASSERT_EQ(dv_psi[0].size(), static_cast(npwkq)); + + // reference: finite difference of the displaced separable operator + // = sum_mu vkb_out(mu,G'') (D becp)_mu + const pseudo& p = ucell_.atoms[0].ncpp; + const int nh = p.nh; + std::vector row_ib, row_m; + for (int ib = 0; ib < p.nbeta; ++ib) + { + for (int m = 0; m < 2 * p.lll[ib] + 1; ++m) + { + row_ib.push_back(ib); + row_m.push_back(m); + } + } + ASSERT_EQ(static_cast(row_ib.size()), nh); + + const double eps = 1.0e-5; // lat0 units + ModuleBase::Vector3 d(0.0, 0.0, 0.0); + d[dir] = 1.0; + std::vector>> vkb_in_p, vkb_in_m, vkb_out_p, vkb_out_m; + ucell_.atoms[0].tau[0] = tau_ + eps * d; + pert_.build_vkb(0, 0, gk_in, vkb_in_p); + pert_.build_vkb(0, 0, gk_out, vkb_out_p); + ucell_.atoms[0].tau[0] = tau_ - eps * d; + pert_.build_vkb(0, 0, gk_in, vkb_in_m); + pert_.build_vkb(0, 0, gk_out, vkb_out_m); + ucell_.atoms[0].tau[0] = tau_; + + for (int b = 0; b < 2; ++b) + { + std::vector> fd(npwkq, std::complex(0.0, 0.0)); + for (int side = 0; side < 2; ++side) + { + const auto& vin = side == 0 ? vkb_in_p : vkb_in_m; + const auto& vout = side == 0 ? vkb_out_p : vkb_out_m; + std::vector> becp(nh, std::complex(0.0, 0.0)); + std::vector> dc(nh, std::complex(0.0, 0.0)); + for (int nu = 0; nu < nh; ++nu) + { + for (int ig = 0; ig < npwk; ++ig) + { + becp[nu] += std::conj(vin[nu][ig]) * psi(0, b, ig); + } + } + for (int mu = 0; mu < nh; ++mu) + { + for (int nu = 0; nu < nh; ++nu) + { + if (row_m[mu] == row_m[nu]) + { + dc[mu] += p.dion(row_ib[mu], row_ib[nu]) * becp[nu]; + } + } + } + const double s = (side == 0) ? 1.0 : -1.0; + for (int igl = 0; igl < npwkq; ++igl) + { + std::complex m(0.0, 0.0); + for (int mu = 0; mu < nh; ++mu) + { + m += vout[mu][igl] * dc[mu]; + } + fd[igl] += s * m; + } + } + for (int igl = 0; igl < npwkq; ++igl) + { + fd[igl] /= (2.0 * eps * lat0_); + EXPECT_NEAR(dv_psi[b][igl].real(), fd[igl].real(), 1.0e-7); + EXPECT_NEAR(dv_psi[b][igl].imag(), fd[igl].imag(), 1.0e-7); + } + } +} + +TEST_F(DFPTPertSerialTest, NonlocalPathRejectsUltrasoft) +{ + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + MakeNCAtom(); + ucell_.atoms[0].ncpp.tvanp = true; + const int npwk = pw_wfc_.npwk[0]; + psi::Psi> psi(1, 1, npwk, npwk, true); + psi.zero_out(); + std::vector>> dv; + EXPECT_EXIT(pert_.dVnl_dtau(0, 0, q_cart_, psi, 0, dv), ::testing::ExitedWithCode(1), ".*"); +} + +TEST_F(DFPTPertSerialTest, BuildDvWithInactiveDftuIsPurePW) +{ + // a wired but unusable provider (occupation matrices not initialized) + // must not change the assembled first-order potential (U0 reservation); + // DFPT_PW::init additionally rejects a wired provider outright + ModuleDFPT::DFPT_PW_Data data_plain; + data_plain.init(&qlist_, 1, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + pert_.build_dv(0, 0, 1, data_plain); + + Plus_U_Base dftu; + ModuleDFPT::DFPT_PW_Data data_u; + data_u.init(&qlist_, 1, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, &dftu); + EXPECT_TRUE(data_u.with_u()); + EXPECT_FALSE(data_u.u_active()); + pert_.build_dv(0, 0, 1, data_u); + + const std::vector> dv_a = data_plain.get_dv_rc(0, 0); + const std::vector> dv_b = data_u.get_dv_rc(0, 0); + ASSERT_EQ(dv_a.size(), static_cast(pw_rho_.nrxx)); + ASSERT_EQ(dv_a.size(), dv_b.size()); + for (size_t i = 0; i < dv_a.size(); ++i) + { + EXPECT_DOUBLE_EQ(dv_a[i].real(), dv_b[i].real()); + EXPECT_DOUBLE_EQ(dv_a[i].imag(), dv_b[i].imag()); + } +} diff --git a/source/source_pw/module_dfpt/test_serial/dfpt_phon_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_phon_serial_test.cpp new file mode 100644 index 00000000000..78838183099 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_phon_serial_test.cpp @@ -0,0 +1,734 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include +#include + +// serial unit test of the DFPT dynamical matrix (C5): the Ewald ion-ion +// force constants, the electronic 2n+1 accumulation, the Hermitian +// eigensolver and the LO-TO term. Runs without __MPI on the shared FFT grid +// like the other DFPT serial tests. + +#define private public +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/pseudo.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_cell/magnetism.h" +#include "source_pw/module_pwdft/stru_fac.h" +#include "source_pw/module_dfpt/dfpt_pert.h" +#include "source_pw/module_dfpt/dfpt_phon.h" +#undef private + +#include "source_base/complexmatrix.h" +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_psi/psi.h" +#include "dfpt_serial_fixture.h" + +// ctor/dtor stubs for the cell/spepot/stru_fac link closures live in the +// shared test/dfpt_test_mocks.cpp compiled into every DFPT test binary. + +/************************************************ + * serial unit test of DFPT_Phon (C5) + ***********************************************/ + +/** + * - Tested Functions: + * - ion_ion: the Ewald force constants satisfy the acoustic sum rule at + * Gamma to grid accuracy (this pins the sign and magnitude of the + * Gaussian self term), give three zero acoustic modes at Gamma, and are + * cross-checked against a direct (unscreened) lattice Hessian sum at a + * generic incommensurate q where the dipole sum is oscillation-screened. + * - accumulate_electron: with an injected dpsi, the cross term + * 2 sum wg Re is validated against an analytic + * convolution (psi a single plane wave, Coulomb local potential), and + * the same-atom anharmonic term against the closed-form + * coefficient at Delta = 0 (|u|^2 = 1 keeps only the G=0 harmonic); + * the dpsi slot is restored after the accumulation. + * - diagonalize: signed frequencies of a known complex Hermitian matrix + * against an independently computed Ry/bohr^2/amu -> cm^-1 factor. + * - add_loto: isotropic dielectric/Born-charge LO-TO term against the + * closed-form matrix element. + * - check_sum_rule at Gamma. + */ + +class DFPTPhonSerialTest : public DFPTSerialBase +{ + protected: + Structure_Factor sf_; + ModuleDFPT::DFPT_Pert pert_; + ModuleDFPT::DFPT_Phon phon_; + + void SetUp() override + { + DFPTSerialBase::SetUp(); + SetupPhon(k_d_, q_d_); + } + + // (re)initialize the bases and the pert/phon wiring for a given (k, q) + // pair; SetUp uses the default fixture values + void SetupPhon(const ModuleBase::Vector3& k_d, + const ModuleBase::Vector3& q_d) + { + SetupBases(k_d, q_d, 2); + pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); + phon_.init(ucell_, &pw_rho_, &pert_); + } + + // independent Ry/bohr^2/amu -> cm^-1 conversion used by diagonalize + double RyBohr2AmuToCm1() const + { + const double amu_kg = 1.66053906660e-27; // CODATA amu in kg + return std::sqrt(ModuleBase::RYDBERG_SI / amu_kg) + / (0.529177210903e-10 * 2.0 * ModuleBase::PI * 2.99792458e10); + } + + // common setup of the isotropic loto closed-form tests: zero 6x6 + // dynamical matrix, eps_inf = 3I, Z*_1 = 1, Z*_2 = 2, and the + // two-atom 12/4 mass table for the mass lookup + void SetupIsotropicLoto() + { + data_.set_dynmat(0, ModuleBase::ComplexMatrix(6, 6, true)); + ModuleBase::matrix eps(3, 3, true); + for (int d = 0; d < 3; ++d) + { + eps(d, d) = 3.0; + } + data_.set_dielectric(eps); + ModuleBase::matrix z1(3, 3, true); + ModuleBase::matrix z2(3, 3, true); + z1(0, 0) = z1(1, 1) = z1(2, 2) = 1.0; + z2(0, 0) = z2(1, 1) = z2(2, 2) = 2.0; + data_.set_born(0, z1); + data_.set_born(1, z2); + MakeTwoAtomCell(); + } + + // band 0 occupied with wg = 2, band 1 unoccupied + static ModuleBase::matrix MakeOccWeights() + { + ModuleBase::matrix wg(1, 2, true); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + return wg; + } + + // psi: band 0 = single plane wave at G' = 0 (c = 1, occupied), + // band 1 unoccupied. The buffer is allocated uninitialized; zero it so + // only the component set below is nonzero regardless of heap history + // from earlier tests. getgpluskcar returns the cartesian k+G, so the + // G = 0 entry is the one with k+G = k_cart. + psi::Psi> MakeSinglePlaneWavePsi(const ModuleBase::Vector3& k_d) + { + const int npwk = pw_wfc_.npwk[0]; + psi::Psi> psi(1, 2, npwk, npwk, true); + psi.zero_out(); + const ModuleBase::Vector3 k_cart = k_d * ucell_.G; + for (int ig = 0; ig < npwk; ++ig) + { + const ModuleBase::Vector3 gk = pw_wfc_.getgpluskcar(0, ig); + if (std::abs(gk.x - k_cart.x) < 1e-10 && std::abs(gk.y - k_cart.y) < 1e-10 + && std::abs(gk.z - k_cart.z) < 1e-10) + { + psi(0, 0, ig) = std::complex(1.0, 0.0); + break; + } + } + return psi; + } + + // analytic cross term sum_G'' conj(dpsi_G'') sum_i c_i + // RHS^a(G''-G'_i): the first-order local Coulomb potential on the k+q + // basis, RHS^a(w) = -i tpiba w_a Vloc(|w|^2) e^{-i 2pi w.tau} with + // w = G'' - G' + q (GS structure-factor phase convention); the + // Delta + q = 0 component is dropped by dVloc + std::complex AnalyticCrossTerm(const ModuleDFPT::DFPT_KQ_Basis& kq, + const std::vector>& psi_coef, + const std::vector>& psi_gcart, + const std::vector>& dpsi_inj, + int adir) const + { + std::complex cross(0.0, 0.0); + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + const ModuleBase::Vector3 gpp = kq.get_gpluskq(igl); + for (size_t ic = 0; ic < psi_coef.size(); ++ic) + { + // AnalyticDVloc returns 0 at w = 0 (dVloc drop) + cross += psi_coef[ic] * std::conj(dpsi_inj[igl]) + * AnalyticDVloc(adir, gpp - psi_gcart[ic] + q_cart_); + } + } + return cross; + } +}; + +// --------------------------------------------------------------------------- +// ion_ion +// --------------------------------------------------------------------------- + +TEST_F(DFPTPhonSerialTest, IonIonAcousticSumRuleGamma) +{ + // a two-atom cell with different charges/masses breaks every symmetry: + // the Gamma acoustic sum rule is then a razor for the Ewald balance + // (G part + real part + Gaussian self term) + MakeTwoAtomCell(); + ModuleBase::ComplexMatrix dyn(6, 6, true); + phon_.ion_ion(ModuleBase::Vector3(0.0, 0.0, 0.0), dyn); + + double max_elem = 0.0; + for (int i = 0; i < 6; ++i) + { + for (int j = 0; j < 6; ++j) + { + max_elem = std::max(max_elem, std::abs(dyn(i, j))); + } + } + ASSERT_GT(max_elem, 1.0e-6); + // acoustic sum rule for the mass-scaled matrix D = Phi/sqrt(M_i M_j): + // sum_j Phi(i,j) = 0 => sum_j sqrt(M_j) D(i,j) = 0 for every row i + double sqrtm[2] = {std::sqrt(12.0), std::sqrt(4.0)}; + for (int i = 0; i < 6; ++i) + { + std::complex rowsum(0.0, 0.0); + for (int j = 0; j < 6; ++j) + { + rowsum += sqrtm[j / 3] * dyn(i, j); + } + EXPECT_LT(std::abs(rowsum), 1.0e-6 * max_elem) + << "row " << i << " sum " << std::abs(rowsum); + } + // Hermitian + for (int i = 0; i < 6; ++i) + { + for (int j = i + 1; j < 6; ++j) + { + EXPECT_NEAR(std::abs(dyn(i, j) - std::conj(dyn(j, i))), + 0.0, + 1.0e-10 * max_elem); + } + } +} + +TEST_F(DFPTPhonSerialTest, IonIonGammaAcousticZeroModes) +{ + // same two-atom cell: three acoustic eigenvalues vanish at Gamma + MakeTwoAtomCell(); + data_.set_dynmat(0, ModuleBase::ComplexMatrix(6, 6, true)); + ModuleBase::ComplexMatrix& dyn = data_.dynmat_[0]; + phon_.ion_ion(ModuleBase::Vector3(0.0, 0.0, 0.0), dyn); + phon_.diagonalize(0, data_); + const std::vector freq = data_.get_phon_freq(0); + ASSERT_EQ(freq.size(), 6u); + // three acoustic modes vanish; the frequencies come back in signed + // ascending order, and a net-charged cell can push optical modes + // negative (they then sort before the acoustic triple), so identify the + // acoustic modes by magnitude + std::vector mag(freq); + for (int i = 0; i < 6; ++i) + { + mag[i] = std::abs(freq[i]); + } + std::sort(mag.begin(), mag.end()); + for (int i = 0; i < 3; ++i) + { + EXPECT_LT(mag[i], 5.0) << "acoustic mode " << i; // cm^-1 + } +} + +TEST_F(DFPTPhonSerialTest, IonIonGenericQVsDirectSum) +{ + // two-atom cell at an incommensurate q: the Ewald result must agree + // with a direct (unscreened) dipole-Hessian lattice sum, whose shell + // oscillation e^{i q l} makes it convergent + MakeTwoAtomCell(); + const ModuleBase::Vector3 tau1(0.0, 0.0, 0.0); + const ModuleBase::Vector3 tau2(0.25, 0.31, 0.17); + const double z[2] = {4.0, 2.0}; + const double m[2] = {12.0, 4.0}; + const int nshell = 8; // lattice-vector cutoff in cells + + ModuleBase::ComplexMatrix dyn(6, 6, true); + phon_.ion_ion(q_d_, dyn); + + // direct reference (structure validated against standalone Ewald sums): + // off-diagonal (ia != ib): + // D_ab = -ZaZb e2/sqrt(MaMb) sum_l h0(R) e^{i2pi q.l} + // diagonal: the on-site cross pairs are phase-free (both derivatives act + // on tau_a in cell 0) while the same-atom images carry the phase + // difference: + // D_aa = sum_{b != a} ZaZb e2/Ma sum_l h0(R) + // + Za^2 e2/Ma sum_{l != 0} h0(L) (1 - e^{i2pi q.l}) + // (h0(L) is the bare 1/R Hessian of the pure lattice; its l = 0 term is + // killed by 1 - e^{i2pi q.0} = 0). The production Ewald drops the + // tau-independent G = 0 constant (4pi/Omega)/3 on the on-site diagonal, + // an ASR-preserving convention difference of ~2.5e-3 here, well inside + // the tolerance below. + ModuleBase::ComplexMatrix ref(6, 6, true); + for (int ia = 0; ia < 2; ++ia) + { + for (int ib = 0; ib < 2; ++ib) + { + const bool self = (ib == ia); + const ModuleBase::Vector3 dt = + (ib == 0 ? tau1 : tau2) - (ia == 0 ? tau1 : tau2); + for (int n1 = -nshell; n1 <= nshell; ++n1) + { + for (int n2 = -nshell; n2 <= nshell; ++n2) + { + for (int n3 = -nshell; n3 <= nshell; ++n3) + { + if (self && n1 == 0 && n2 == 0 && n3 == 0) + { + continue; + } + const ModuleBase::Vector3 r( + (n1 * a_ + dt.x) * lat0_, + (n2 * a_ + dt.y) * lat0_, + (n3 * a_ + dt.z) * lat0_); + const double r2 = r * r; + const double r5 = r2 * r2 * std::sqrt(r2); + const double ph = ModuleBase::TWO_PI + * (q_d_.x * n1 + q_d_.y * n2 + q_d_.z * n3); + const std::complex phase(std::cos(ph), std::sin(ph)); + const double pref = -z[ia] * z[ib] * ModuleBase::e2 / std::sqrt(m[ia] * m[ib]); + for (int da = 0; da < 3; ++da) + { + for (int db = 0; db < 3; ++db) + { + const double delta = (da == db) ? 1.0 : 0.0; + const double h0 = (3.0 * r[da] * r[db] - delta * r2) / r5; + if (self) + { + ref(3 * ia + da, 3 * ia + db) + += z[ia] * z[ia] * ModuleBase::e2 / m[ia] * h0 + * (1.0 - phase); + } + else + { + ref(3 * ia + da, 3 * ib + db) += pref * h0 * phase; + ref(3 * ia + da, 3 * ia + db) + -= pref * std::sqrt(m[ib] / m[ia]) * h0; + } + } + } + } + } + } + } + } + double max_ref = 0.0; + for (int i = 0; i < 6; ++i) + { + for (int j = 0; j < 6; ++j) + { + max_ref = std::max(max_ref, std::abs(ref(i, j))); + } + } + ASSERT_GT(max_ref, 1.0e-3); + for (int i = 0; i < 6; ++i) + { + for (int j = 0; j < 6; ++j) + { + EXPECT_LT(std::abs(dyn(i, j) - ref(i, j)), 2.0e-3 * max_ref) + << "(" << i << "," << j << ") ewald " << dyn(i, j) << " ref " << ref(i, j); + } + } +} + +// --------------------------------------------------------------------------- +// accumulate_electron +// --------------------------------------------------------------------------- + +TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) +{ + // psi: band 0 = single plane wave at G'=0 (c=1, occupied, wg=2), + // band 1 unoccupied. k = -q so the k+q basis vectors are plain G''. + psi::Psi> psi = MakeSinglePlaneWavePsi(k_d_); + ModuleBase::matrix wg = MakeOccWeights(); + + // inject a known dpsi for displacement (atom 0, dir=1) on the k+q basis + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + std::vector> dpsi_inj(kq.get_npwk(), std::complex(0.0, 0.0)); + dpsi_inj[0] = std::complex(0.3, 0.1); + if (kq.get_npwk() > 1) + { + dpsi_inj[1] = std::complex(-0.2, 0.05); + } + data_.set_dpsi(0, 0, 0, dpsi_inj); + + phon_.accumulate_electron(0, 0, 1, psi, wg, data_); + + // expected: row 1 (atom 0, dir 1) of the Hermitian 2n+1 accumulation: + // the row element receives wg* once per off-diagonal + // column; the diagonal column additionally gets its own conjugate + // (2 Re). The same-atom anharmonic d2 term is gate-skipped here: at + // this q = (0.13, 0, 0.07) the second-order potential carries + // 2q = (0.26, 0, 0.14), which is NOT a reciprocal vector, so the + // same-k expectation is momentum-forbidden (see the commensurate + // test below for the gate-on branch) + const std::vector> g0(1, ModuleBase::Vector3(0.0, 0.0, 0.0)); + for (int adir = 0; adir < 3; ++adir) + { + std::complex expect = wg(0, 0) * AnalyticCrossTerm(kq, + {std::complex(1.0, 0.0)}, + g0, + dpsi_inj, + adir); + if (adir == 1) + { + expect = 2.0 * expect.real(); + } + expect /= ucell_.atoms[0].mass; + EXPECT_NEAR(std::abs(phon_.dynmat_accum_(1, adir) - expect), + 0.0, + 1.0e-7 * (1.0 + std::abs(expect))) + << "adir " << adir << " got " << phon_.dynmat_accum_(1, adir) + << " expect " << expect; + } + + // the dpsi slot must be restored to the injected solution + const std::vector> restored = data_.get_dpsi(0, 0, 0); + ASSERT_EQ(restored.size(), dpsi_inj.size()); + for (size_t i = 0; i < dpsi_inj.size(); ++i) + { + EXPECT_DOUBLE_EQ(restored[i].real(), dpsi_inj[i].real()); + EXPECT_DOUBLE_EQ(restored[i].imag(), dpsi_inj[i].imag()); + } +} + +TEST_F(DFPTPhonSerialTest, AccumulateElectronD2GateOffGenericQ) +{ + // row 0 of the same generic-q fixture: the same-atom d2 kernel would be + // nonzero here under the ungated convention (the (0,0) element involves + // q_x^2 != 0), so this row is a sharp probe that the 2q-reciprocal gate + // really suppresses the momentum-forbidden term at a generic q + psi::Psi> psi = MakeSinglePlaneWavePsi(k_d_); + ModuleBase::matrix wg = MakeOccWeights(); + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + std::vector> dpsi_inj(kq.get_npwk(), + std::complex(0.0, 0.0)); + dpsi_inj[0] = std::complex(0.25, -0.15); + if (kq.get_npwk() > 2) + { + dpsi_inj[2] = std::complex(0.4, 0.2); + } + data_.set_dpsi(0, 0, 0, dpsi_inj); + + phon_.accumulate_electron(0, 0, 0, psi, wg, data_); + + const std::vector> g0(1, ModuleBase::Vector3(0.0, 0.0, 0.0)); + for (int adir = 0; adir < 3; ++adir) + { + std::complex expect = wg(0, 0) * AnalyticCrossTerm(kq, + {std::complex(1.0, 0.0)}, + g0, + dpsi_inj, + adir); + if (adir == 0) + { + expect = 2.0 * expect.real(); + } + expect /= ucell_.atoms[0].mass; + EXPECT_NEAR(std::abs(phon_.dynmat_accum_(0, adir) - expect), + 0.0, + 1.0e-7 * (1.0 + std::abs(expect))) + << "adir " << adir << " got " << phon_.dynmat_accum_(0, adir) + << " expect " << expect; + } +} + +TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) +{ + // k = (-0.5, 0, 0) and q = (0.5, 0, 0): the k+q ball is centered at 0 + // (pure G'' harmonics for the cross term) and 2q = (1, 0, 0) IS a + // reciprocal vector, so the same-atom d2 gate passes: the second-order + // local operator is lattice-periodic on the integer G set (q_eff = 0) + // with kernel K_{da,db}(G) = -tpiba^2 G_da G_db Vloc(|G|^2) e^{-i2pi G.tau} + const ModuleBase::Vector3 k_d(-0.5, 0.0, 0.0); + const ModuleBase::Vector3 q_d(0.5, 0.0, 0.0); + SetupPhon(k_d, q_d); + + const int npwk = pw_wfc_.npwk[0]; + psi::Psi> psi(1, 2, npwk, npwk, true); + psi.zero_out(); + const ModuleBase::Vector3 k_cart = k_d * ucell_.G; + // three plane-wave components of psi at G' = 0, (0,1,0), (0,0,1) (all + // inside the ecutwfc ball at this k): the d2 expectation lives on the + // pairwise differences of |psi|^2 (the G=0 diagonal difference hits the + // w=0 skip of the kernel); the (0,-1,1) difference makes the mixed + // component K_{2,1} nonzero as well + const std::vector> gfrac + = {ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 1.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 1.0)}; + const std::vector> ccoef + = {std::complex(1.0, 0.0), + std::complex(0.6, -0.3), + std::complex(-0.4, 0.25)}; + const size_t ncomp = gfrac.size(); + std::vector> gcart(ncomp); + std::vector ig_of(ncomp, -1); + for (size_t ic = 0; ic < ncomp; ++ic) + { + gcart[ic] = gfrac[ic] * ucell_.G; + } + for (int ig = 0; ig < npwk; ++ig) + { + const ModuleBase::Vector3 gprim + = pw_wfc_.getgpluskcar(0, ig) - k_cart; + for (size_t ic = 0; ic < ncomp; ++ic) + { + if (std::abs(gprim.x - gcart[ic].x) < 1e-10 + && std::abs(gprim.y - gcart[ic].y) < 1e-10 + && std::abs(gprim.z - gcart[ic].z) < 1e-10) + { + ig_of[ic] = ig; + } + } + } + for (size_t ic = 0; ic < ncomp; ++ic) + { + ASSERT_GE(ig_of[ic], 0); + psi(0, 0, ig_of[ic]) = ccoef[ic]; + } + const ModuleBase::matrix wg = MakeOccWeights(); + + // injected dpsi on the k+q = 0 ball (arbitrary coefficients) + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + std::vector> dpsi_inj(kq.get_npwk(), std::complex(0.0, 0.0)); + dpsi_inj[0] = std::complex(0.3, 0.1); + if (kq.get_npwk() > 1) + { + dpsi_inj[1] = std::complex(-0.2, 0.05); + } + data_.set_dpsi(0, 0, 0, dpsi_inj); + + phon_.accumulate_electron(0, 0, 1, psi, wg, data_); + + for (int adir = 0; adir < 3; ++adir) + { + std::complex expect = wg(0, 0) * AnalyticCrossTerm(kq, ccoef, gcart, dpsi_inj, adir); + if (adir == 1) + { + expect = 2.0 * expect.real(); + } + // d2 term: gate passes at this q; the columns cola >= rowb = 1 + // (adir 1 and 2) receive wg * . The |u|^2 + // harmonic at G'_j - G'_i probes the kernel at the NEGATIVE + // harmonic, so the closed form runs over K(G'_i - G'_j) with + // coefficient c_i* c_j (K(-g) = conj K(g), K(0) = 0); the diagonal + // column adds it once (real), the off-diagonal once + if (adir >= 1) + { + std::complex d2elem(0.0, 0.0); + for (size_t i = 0; i < ncomp; ++i) + { + for (size_t j = 0; j < ncomp; ++j) + { + const ModuleBase::Vector3 g = gcart[i] - gcart[j]; + const double g2 = g * g; + if (g2 < 1.0e-12) + { + continue; + } + const double arg = -ModuleBase::TWO_PI * (g * tau_); + const std::complex kterm + = -(ucell_.tpiba * g[adir]) * (ucell_.tpiba * g[1]) + * VlocCoulomb(g2 * ucell_.tpiba2) + * std::complex(std::cos(arg), std::sin(arg)); + d2elem += std::conj(ccoef[i]) * ccoef[j] * kterm; + } + } + expect += wg(0, 0) * d2elem; + } + expect /= ucell_.atoms[0].mass; + EXPECT_NEAR(std::abs(phon_.dynmat_accum_(1, adir) - expect), + 0.0, + 1.0e-7 * (1.0 + std::abs(expect))) + << "adir " << adir << " got " << phon_.dynmat_accum_(1, adir) + << " expect " << expect; + } +} + +// --------------------------------------------------------------------------- +// diagonalize +// --------------------------------------------------------------------------- + +TEST_F(DFPTPhonSerialTest, DiagonalizeKnownMatrix) +{ + // 2-atom layout so nat3 = 6. Hermitian blocks with known closed-form + // spectra: [[a, c], [conj(c), b]] has eigenvalues (a+b)/2 + // +- sqrt(((a-b)/2)^2 + |c|^2) + MakeTwoAtomCell(); + const double lam[6] = {0.04, 0.09, 0.16, -0.02, 0.01, 0.1225}; // Ry/bohr^2/amu + ModuleBase::ComplexMatrix dyn(6, 6, true); + for (int i = 0; i < 6; ++i) + { + dyn(i, i) = std::complex(lam[i], 0.0); + } + dyn(0, 1) = std::complex(0.01, 0.02); + dyn(1, 0) = std::conj(dyn(0, 1)); + dyn(2, 3) = std::complex(-0.03, 0.005); + dyn(3, 2) = std::conj(dyn(2, 3)); + data_.set_dynmat(0, dyn); + phon_.diagonalize(0, data_); + const std::vector freq = data_.get_phon_freq(0); + ASSERT_EQ(freq.size(), 6u); + std::vector expect; + auto block = [&expect](double a, double b, std::complex c) + { + const double mid = 0.5 * (a + b); + const double rad = std::sqrt(std::pow(0.5 * (a - b), 2) + std::norm(c)); + expect.push_back(mid + rad); + expect.push_back(mid - rad); + }; + block(lam[0], lam[1], dyn(0, 1)); // coupled pair + block(lam[2], lam[3], dyn(2, 3)); // coupled pair + expect.push_back(lam[4]); // untouched diagonal + expect.push_back(lam[5]); + for (double& e : expect) + { + const double s = (e >= 0.0) ? 1.0 : -1.0; + e = s * std::sqrt(std::abs(e)) * RyBohr2AmuToCm1(); + } + std::sort(expect.begin(), expect.end()); + std::vector got = freq; + std::sort(got.begin(), got.end()); + for (int i = 0; i < 6; ++i) + { + EXPECT_NEAR(got[i], expect[i], 1.0e-6 * std::abs(expect[i])); + } +} + +// --------------------------------------------------------------------------- +// add_loto / check_sum_rule +// --------------------------------------------------------------------------- + +TEST_F(DFPTPhonSerialTest, AddLotoIsotropicClosedForm) +{ + // isotropic eps_inf = 3, Born charges Z*_1 = 1, Z*_2 = 2, masses 12/4 + SetupIsotropicLoto(); + + const ModuleBase::Vector3 qhat(1.0, 0.0, 0.0); + phon_.add_loto(qhat, data_); + + // closed form: D_NAC(0x,1x) = 4pi e2/Omega * 1*2/(3) / sqrt(12*4) + const double expect = ModuleBase::FOUR_PI * ModuleBase::e2 / ucell_.omega / 3.0 + * 2.0 / std::sqrt(48.0); + const ModuleBase::ComplexMatrix dyn = data_.get_dynmat(0); + EXPECT_NEAR(std::abs(dyn(0, 3) - std::complex(expect, 0.0)), 0.0, 1.0e-12); + EXPECT_NEAR(std::abs(dyn(3, 0) - std::complex(expect, 0.0)), 0.0, 1.0e-12); + // off-qhat elements untouched + EXPECT_DOUBLE_EQ(std::abs(dyn(1, 4)), 0.0); +} + +TEST_F(DFPTPhonSerialTest, CheckSumRuleAtGamma) +{ + // the sum rule is a Gamma-only statement: use a Gamma q point + qlist_.kvec_d[0] = ModuleBase::Vector3(0.0, 0.0, 0.0); + // zero dynamical matrix trivially satisfies the rule + data_.set_dynmat(0, ModuleBase::ComplexMatrix(3, 3, true)); + EXPECT_TRUE(phon_.check_sum_rule(0, data_)); + // a uniform constant shift violates it + ModuleBase::ComplexMatrix dyn(3, 3, true); + for (int i = 0; i < 3; ++i) + { + for (int j = 0; j < 3; ++j) + { + dyn(i, j) = std::complex(0.1, 0.0); + } + } + data_.set_dynmat(0, dyn); + EXPECT_FALSE(phon_.check_sum_rule(0, data_)); +} + +// --------------------------------------------------------------------------- +// B2: LO-TO direction via the data layer, corrected-frequency +// diagonalization, and the output format regression +// --------------------------------------------------------------------------- + +TEST_F(DFPTPhonSerialTest, LotoDirNormalization) +{ + // default is the isotropic (1,1,1)/sqrt(3) + const ModuleBase::Vector3 def = data_.get_loto_dir(); + const double inv = 1.0 / std::sqrt(3.0); + EXPECT_NEAR(def.x, inv, 1.0e-12); + EXPECT_NEAR(def.y, inv, 1.0e-12); + EXPECT_NEAR(def.z, inv, 1.0e-12); + // any non-null vector is normalized to a unit direction + data_.set_loto_dir(ModuleBase::Vector3(2.0, 0.0, 0.0)); + const ModuleBase::Vector3 x = data_.get_loto_dir(); + EXPECT_NEAR(x.x, 1.0, 1.0e-12); + EXPECT_NEAR(x.y, 0.0, 1.0e-12); + EXPECT_NEAR(x.z, 0.0, 1.0e-12); + EXPECT_NEAR(std::sqrt(x * x), 1.0, 1.0e-12); + // a null vector keeps the previous direction + data_.set_loto_dir(ModuleBase::Vector3(0.0, 0.0, 0.0)); + EXPECT_NEAR(data_.get_loto_dir().x, 1.0, 1.0e-12); +} + +TEST_F(DFPTPhonSerialTest, DiagonalizeLotoClosedForm) +{ + // same isotropic fixture as AddLotoIsotropicClosedForm: zero dynamical + // matrix + eps = 3I, Z*_1 = 1, Z*_2 = 2, masses 12/4, qhat = x. + // add_loto fills BOTH the diagonal and cross xx elements with + // pref = 4pi e2/Omega/3: (0x,0x) = pref/12, (3x,3x) = pref, + // (0x,3x) = pref*2/sqrt(48); the 2x2 block + // [[1/12, 2/sqrt48], [2/sqrt48, 1]]*pref has eigenvalues + // {0, 13/12 * pref} (determinant 1/12 - 4/48 = 0), the yy/zz blocks + // stay zero, so the spectrum is {13/12*pref, 0 x 5} in Ry/bohr^2/amu + SetupIsotropicLoto(); + + phon_.add_loto(ModuleBase::Vector3(1.0, 0.0, 0.0), data_); + phon_.diagonalize_loto(data_); + + const double pref = ModuleBase::FOUR_PI * ModuleBase::e2 / ucell_.omega / 3.0; + const double expect = std::sqrt(13.0 / 12.0 * pref) * RyBohr2AmuToCm1(); + const std::vector freq = data_.get_phon_freq_loto(); + ASSERT_EQ(freq.size(), static_cast(6)); + // signed spectrum: one +expect, five zeros (sorted); the zeros carry + // zheev roundoff of order sqrt(eps_mach * lambda_max) in frequency + std::vector sorted = freq; + std::sort(sorted.begin(), sorted.end()); + EXPECT_NEAR(sorted.back(), expect, 1.0e-6 * std::abs(expect)); + for (int i = 0; i < 5; ++i) + { + EXPECT_NEAR(sorted[i], 0.0, 1.0e-5); + } +} + +TEST_F(DFPTPhonSerialTest, FormatReportsRegression) +{ + // fixture q = (0.13, 0, 0.07) direct; three crafted frequencies + data_.set_phon_freq(0, std::vector{-7.32457, 517.491, 0.0}); + const std::string qrep = phon_.format_q_report(0, data_); + const std::string expect_q + = " DFPT phonon frequencies at q #0 = (0.130000 0.000000 0.070000) " + "(direct) in cm^-1:\n" + " mode 0 : -7.324570 cm^-1\n" + " mode 1 : 517.491000 cm^-1\n" + " mode 2 : 0.000000 cm^-1\n"; + EXPECT_EQ(qrep, expect_q); + + // LO-TO report: empty before the corrected frequencies exist + EXPECT_TRUE(phon_.format_loto_report(data_).empty()); + data_.set_loto_dir(ModuleBase::Vector3(0.0, 3.0, 0.0)); + data_.set_phon_freq_loto(std::vector{0.0, 520.123456, 520.123457}); + const std::string lrep = phon_.format_loto_report(data_); + const std::string expect_l + = " DFPT LO-TO corrected frequencies at q #0 along q->0 direction " + "(0.000000 1.000000 0.000000) in cm^-1:\n" + " mode 0 : 0.000000 cm^-1\n" + " mode 1 : 520.123456 cm^-1\n" + " mode 2 : 520.123457 cm^-1\n"; + EXPECT_EQ(lrep, expect_l); +} diff --git a/source/source_pw/module_dfpt/test_serial/dfpt_q0_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_q0_serial_test.cpp new file mode 100644 index 00000000000..82182541595 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_q0_serial_test.cpp @@ -0,0 +1,695 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include + +// serial unit test of the q -> 0 response (C6): the position operator in +// the velocity (commutator) form, the dielectric tensor and the Born +// charges. Runs without __MPI on the shared FFT grid like the other DFPT +// serial tests; all references are closed-form or operator finite +// differences, no ground-state solver is involved. + +#define private public +#include "source_cell/atom_pseudo.h" +#include "source_cell/atom_spec.h" +#include "source_cell/pseudo.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_cell/magnetism.h" +#include "source_pw/module_pwdft/stru_fac.h" +#include "source_pw/module_dfpt/dfpt_pert.h" +#include "source_pw/module_dfpt/dfpt_q0.h" +#undef private + +#include "source_base/constants.h" +#include "source_base/matrix.h" +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_psi/psi.h" +#include "dfpt_serial_fixture.h" + +// ctor/dtor stubs for the cell/spepot/stru_fac link closures live in the +// shared test/dfpt_test_mocks.cpp compiled into every DFPT test binary. + +/************************************************ + * serial unit test of DFPT_Q0 (C6) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_Pert::build_vkb_dk: the three analytic derivative terms (atomic + * phase, radial chain rule, harmonic direction chain) against a central + * finite difference of build_vkb on a generic shifted gk list. + * - DFPT_Q0::pos_matrix: the kinetic velocity term, the -i commutator + * factor, the tpiba scaling and the Hermitian structure against + * closed-form plane-wave-combination states; the nonlocal contraction + * against an operator finite difference of ; exactly + * degenerate pairs are skipped. + * - DFPT_Q0::compute_eps: the SCF contraction (16 pi / Omega, wg, + * occupied-band sum) on synthetic pos_resp/dpsi_efield stashes; + * the empty-band rows must be skipped and indices/conj pinned by + * distinct per-direction complexes. + * - DFPT_Q0::compute_born: elementwise against the closed-form + * sums at q = 0 (Coulomb local part), + * the ionic Z delta_ab, and the dpsi-slot backup/restore. + */ + +class DFPTQ0SerialTest : public DFPTSerialBase +{ + protected: + Structure_Factor sf_; + ModuleDFPT::DFPT_Pert pert_; + ModuleDFPT::DFPT_Q0 q0_; + + // this fixture is Gamma-only: shadow the generic default k of the base + const ModuleBase::Vector3 k_d_{0.0, 0.0, 0.0}; + const ModuleBase::Vector3 gx_{0.1, 0.0, 0.0}; // 1/lat0 units + const ModuleBase::Vector3 gy_{0.0, 0.1, 0.0}; + + void SetUp() override + { + SetUpCell(); + SetupBases(k_d_, ModuleBase::Vector3(0.0, 0.0, 0.0), 4); + pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); + q0_.init(ucell_, &pw_rho_, &pw_wfc_, &pert_); + } + + // wfc-basis index of the reciprocal vector (ix, iy, iz)/a at Gamma + int IgOf(int ix, int iy, int iz) const + { + const int npwk = pw_wfc_.npwk[0]; + for (int ig = 0; ig < npwk; ++ig) + { + const ModuleBase::Vector3 g = pw_wfc_.getgpluskcar(0, ig); + if (std::llround(g.x * a_) == ix && std::llround(g.y * a_) == iy + && std::llround(g.z * a_) == iz) + { + return ig; + } + } + return -1; + } +}; + +// --------------------------------------------------------------------------- +// build_vkb_dk against a finite difference of build_vkb +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, BuildVkbDkMatchesFiniteDifference) +{ + MakeNCAtom(); + // generic shifted list without any |g| = 0 entry (the direction chain is + // singular at the origin for l >= 1 rows) + const int ng = 8; + std::vector> gk(ng); + gk[0] = ModuleBase::Vector3(0.1, 0.0, 0.0) + k_d_; + gk[1] = ModuleBase::Vector3(0.0, 0.1, 0.0) + k_d_; + gk[2] = ModuleBase::Vector3(0.07, 0.13, 0.05) + k_d_; + gk[3] = ModuleBase::Vector3(-0.11, 0.23, -0.17) + k_d_; + gk[4] = ModuleBase::Vector3(0.29, -0.19, 0.31) + k_d_; + gk[5] = ModuleBase::Vector3(-0.05, 0.0, 0.11) + k_d_; + gk[6] = ModuleBase::Vector3(0.13, -0.07, 0.03) + k_d_; + gk[7] = ModuleBase::Vector3(0.21, 0.11, -0.13) + k_d_; + + std::vector>> vkb; + pert_.build_vkb(0, 0, gk, vkb); + const int nh = ucell_.atoms[0].ncpp.nh; + ASSERT_EQ(vkb.size(), static_cast(nh)); + + const double eps = 1.0e-5; // gcar units + for (int d = 0; d < 3; ++d) + { + ModuleBase::Vector3 shift(0.0, 0.0, 0.0); + shift[d] = eps; + std::vector> gk_p(ng), gk_m(ng); + for (int i = 0; i < ng; ++i) + { + gk_p[i] = gk[i] + shift; + gk_m[i] = gk[i] - shift; + } + std::vector>> vkb_p, vkb_m; + pert_.build_vkb(0, 0, gk_p, vkb_p); + pert_.build_vkb(0, 0, gk_m, vkb_m); + + std::vector>> dvkb; + pert_.build_vkb_dk(0, 0, d, gk, vkb, dvkb); + ASSERT_EQ(dvkb.size(), static_cast(nh)); + for (int mu = 0; mu < nh; ++mu) + { + for (int i = 0; i < ng; ++i) + { + const std::complex fd = (vkb_p[mu][i] - vkb_m[mu][i]) / (2.0 * eps); + const double scale = std::max(1.0, std::abs(fd)); + EXPECT_NEAR(dvkb[mu][i].real(), fd.real(), 1.0e-5 * scale) + << "mu=" << mu << " i=" << i << " d=" << d; + EXPECT_NEAR(dvkb[mu][i].imag(), fd.imag(), 1.0e-5 * scale) + << "mu=" << mu << " i=" << i << " d=" << d; + } + } + } +} + +// --------------------------------------------------------------------------- +// pos_matrix: kinetic part, -i factor, Hermiticity, degenerate skip +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, PosMatrixKineticAndDegenerateSkip) +{ + const int npwk = pw_wfc_.npwk[0]; + const int ig0 = IgOf(0, 0, 0); + const int igx = IgOf(1, 0, 0); + const int igy = IgOf(0, 1, 0); + ASSERT_GE(ig0, 0); + ASSERT_GE(igx, 0); + ASSERT_GE(igy, 0); + + const double e1 = ucell_.tpiba2 * (gx_ * gx_); // |Gx|^2 in Ry + // b0 = sqrt(0.8)|G0> + sqrt(0.2)|Gx> (eps = 0.2 e1) + // b1 = sqrt(0.2)|G0> - sqrt(0.8)|Gx> (eps = 0.8 e1) + // b2 = (|Gx> + |Gy>)/sqrt(2) (eps = e1, degenerate with b3) + // b3 = (|Gx> - |Gy>)/sqrt(2) (eps = e1) + psi::Psi> psi(1, 4, npwk, npwk, true); + psi.zero_out(); + psi(0, 0, ig0) = std::sqrt(0.8); + psi(0, 0, igx) = std::sqrt(0.2); + psi(0, 1, ig0) = std::sqrt(0.2); + psi(0, 1, igx) = -std::sqrt(0.8); + psi(0, 2, igx) = std::complex(1.0 / std::sqrt(2.0), 0.0); + psi(0, 2, igy) = std::complex(1.0 / std::sqrt(2.0), 0.0); + psi(0, 3, igx) = std::complex(1.0 / std::sqrt(2.0), 0.0); + psi(0, 3, igy) = std::complex(-1.0 / std::sqrt(2.0), 0.0); + + ModuleBase::matrix eig(1, 4); + eig(0, 0) = 0.2 * e1; + eig(0, 1) = 0.8 * e1; + eig(0, 2) = e1; + eig(0, 3) = e1; + + std::vector>>>> r_mat; + q0_.pos_matrix(psi, eig, r_mat); + ASSERT_EQ(r_mat.size(), 1u); + ASSERT_EQ(r_mat[0].size(), 4u); + + // p_01^d = 2 tpiba^2 = 2 tpiba^2 (-sqrt(0.16)) Gx_d + for (int d = 0; d < 3; ++d) + { + const std::complex p01(2.0 * ucell_.tpiba2 * (-std::sqrt(0.16)) * gx_[d], 0.0); + const std::complex expect + = std::complex(0.0, -1.0) * p01 / (ucell_.tpiba * (0.2 * e1 - 0.8 * e1)); + EXPECT_NEAR(r_mat[0][0][1][d].real(), expect.real(), 1.0e-10); + EXPECT_NEAR(r_mat[0][0][1][d].imag(), expect.imag(), 1.0e-10); + // Hermiticity: r_10 = conj(r_01) + EXPECT_NEAR(r_mat[0][1][0][d].real(), expect.real(), 1.0e-10); + EXPECT_NEAR(r_mat[0][1][0][d].imag(), -expect.imag(), 1.0e-10); + // diagonal vanishes + EXPECT_EQ(r_mat[0][0][0][d], std::complex(0.0, 0.0)); + // exactly degenerate pairs are skipped + EXPECT_EQ(r_mat[0][2][3][d], std::complex(0.0, 0.0)); + EXPECT_EQ(r_mat[0][3][2][d], std::complex(0.0, 0.0)); + } +} + +// --------------------------------------------------------------------------- +// pos_matrix: nonlocal velocity against an operator finite difference +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, PosMatrixNonlocalMatchesOperatorFiniteDifference) +{ + MakeNCAtom(); + const int npwk = pw_wfc_.npwk[0]; + const int ig0 = IgOf(0, 0, 0); + ASSERT_GE(ig0, 0); + + // deterministic pseudo-random orthonormal bands with the |G| = 0 + // component forced to zero: the projector derivative is direction + // singular exactly at g = 0 (l >= 1 rows), so that single column is + // excluded from both sides of the comparison + const int nb = 4; + psi::Psi> psi(1, nb, npwk, npwk, true); + psi.zero_out(); + unsigned seed = 20260817u; + auto rnd = [&]() + { + seed = seed * 1664525u + 1013904223u; + return ((seed >> 8) & 0xffffff) / 16777216.0 * 2.0 - 1.0; + }; + std::vector>> c(nb, std::vector>(npwk)); + for (int b = 0; b < nb; ++b) + { + for (int ig = 0; ig < npwk; ++ig) + { + c[b][ig] = (ig == ig0) ? std::complex(0.0, 0.0) + : std::complex(rnd(), rnd()); + } + } + // Gram-Schmidt, skipping the zero column keeps the norm from column 1 on + for (int b = 0; b < nb; ++b) + { + for (int p = 0; p < b; ++p) + { + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npwk; ++ig) + { + dot += std::conj(c[p][ig]) * c[b][ig]; + } + for (int ig = 0; ig < npwk; ++ig) + { + c[b][ig] -= dot * c[p][ig]; + } + } + double nrm = 0.0; + for (int ig = 0; ig < npwk; ++ig) + { + nrm += std::norm(c[b][ig]); + } + nrm = std::sqrt(nrm); + for (int ig = 0; ig < npwk; ++ig) + { + c[b][ig] /= nrm; + psi(0, b, ig) = c[b][ig]; + } + } + + // non-degenerate fake eigenvalues (pos_matrix only uses them as divisors) + ModuleBase::matrix eig(1, nb); + for (int b = 0; b < nb; ++b) + { + eig(0, b) = 0.31 + 0.17 * b; + } + + std::vector>>>> r_mat; + q0_.pos_matrix(psi, eig, r_mat); + + // finite-difference reference of the full velocity operator + const pseudo& p = ucell_.atoms[0].ncpp; + const int nh = p.nh; + std::vector row_ib, row_m; + for (int ib = 0; ib < p.nbeta; ++ib) + { + for (int m = 0; m < 2 * p.lll[ib] + 1; ++m) + { + row_ib.push_back(ib); + row_m.push_back(m); + } + } + ASSERT_EQ(static_cast(row_ib.size()), nh); + + std::vector> gk(npwk); + for (int ig = 0; ig < npwk; ++ig) + { + gk[ig] = pw_wfc_.getgpluskcar(0, ig); + } + const double eps = 1.0e-5; + + // becp with the |G| = 0 column dropped on a shifted list + auto vnl_matrix = [&](const std::vector>& glist, + std::vector>>& mmat) + { + std::vector>> vkb; + pert_.build_vkb(0, 0, glist, vkb); + std::vector>> becp(nb); + for (int b = 0; b < nb; ++b) + { + becp[b].assign(nh, std::complex(0.0, 0.0)); + for (int mu = 0; mu < nh; ++mu) + { + for (int ig = 0; ig < npwk; ++ig) + { + if (ig == ig0) + { + continue; // the singular column, see above + } + becp[b][mu] += std::conj(vkb[mu][ig]) * psi(0, b, ig); + } + } + } + mmat.assign(nb, std::vector>(nb, std::complex(0.0, 0.0))); + for (int m = 0; m < nb; ++m) + { + for (int n = 0; n < nb; ++n) + { + for (int mu = 0; mu < nh; ++mu) + { + std::complex dc(0.0, 0.0); + for (int nu = 0; nu < nh; ++nu) + { + if (row_m[mu] == row_m[nu]) + { + dc += p.dion(row_ib[mu], row_ib[nu]) * becp[n][nu]; + } + } + mmat[m][n] += std::conj(becp[m][mu]) * dc; + } + } + } + }; + + for (int d = 0; d < 3; ++d) + { + ModuleBase::Vector3 shift(0.0, 0.0, 0.0); + shift[d] = eps; + std::vector> gk_p(npwk), gk_m(npwk); + for (int ig = 0; ig < npwk; ++ig) + { + gk_p[ig] = gk[ig] + shift; + gk_m[ig] = gk[ig] - shift; + } + std::vector>> mm_p, mm_m; + vnl_matrix(gk_p, mm_p); + vnl_matrix(gk_m, mm_m); + + for (int m = 0; m < nb; ++m) + { + for (int n = 0; n < nb; ++n) + { + if (m == n) + { + continue; + } + const double de = eig(0, m) - eig(0, n); + // recover p from r: r = -i p / (tpiba de) + const std::complex p_r + = std::complex(0.0, 1.0) * ucell_.tpiba * de * r_mat[0][m][n][d]; + // analytic kinetic + finite-difference nonlocal + std::complex p_kin(0.0, 0.0); + for (int ig = 0; ig < npwk; ++ig) + { + p_kin += 2.0 * ucell_.tpiba2 * gk[ig][d] * std::conj(psi(0, m, ig)) + * psi(0, n, ig); + } + const std::complex p_nl = (mm_p[m][n] - mm_m[m][n]) / (2.0 * eps); + const double scale = std::max(1.0, std::abs(p_kin) + std::abs(p_nl)); + EXPECT_NEAR(p_r.real(), (p_kin + p_nl).real(), 1.0e-6 * scale) + << "m=" << m << " n=" << n << " d=" << d; + EXPECT_NEAR(p_r.imag(), (p_kin + p_nl).imag(), 1.0e-6 * scale) + << "m=" << m << " n=" << n << " d=" << d; + } + } + } +} + +// --------------------------------------------------------------------------- +// compute_eps (SCF contraction) against synthetic response stashes: +// eps(a,b) = delta_ab - (16 pi/Omega) sum_k wg sum_occ Re +// with distinct complexes per direction catching transposed indices and +// conj placement; the empty-band rows stay unsolved and must be skipped +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, ComputeEpsScfSyntheticStash) +{ + const int npwk = pw_wfc_.npwk[0]; + const int ig0 = IgOf(0, 0, 0); + const int igx = IgOf(1, 0, 0); + ASSERT_GE(ig0, 0); + ASSERT_GE(igx, 0); + + ModuleBase::matrix wg(1, 2); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + + // synthetic bare position legs Y^a_{0,0} = P_c x_a|psi_0> + const std::complex gam[3] = {std::complex(0.15, -0.3), + std::complex(0.4, 0.05), + std::complex(-0.35, 0.2)}; + const std::complex del[3] = {std::complex(-0.25, 0.45), + std::complex(0.1, -0.1), + std::complex(0.3, 0.25)}; + for (int a = 0; a < 3; ++a) + { + std::vector>>> y( + 1, std::vector>>(2)); + y[0][0].assign(npwk, std::complex(0.0, 0.0)); + y[0][0][ig0] = gam[a]; + y[0][0][igx] = del[a]; + data_.set_pos_resp(a, y); + } + + // synthetic converged E-field responses dpsi^E,b_{0,0} + const std::complex mue[3] = {std::complex(0.3, 0.2), + std::complex(-0.1, 0.4), + std::complex(0.25, -0.15)}; + const std::complex nue[3] = {std::complex(0.2, -0.35), + std::complex(0.45, 0.1), + std::complex(-0.2, -0.05)}; + for (int b = 0; b < 3; ++b) + { + std::vector>>> e( + 1, std::vector>>(2)); + e[0][0].assign(npwk, std::complex(0.0, 0.0)); + e[0][0][ig0] = mue[b]; + e[0][0][igx] = nue[b]; + data_.set_dpsi_efield(b, e); + } + + q0_.compute_eps(wg, data_); + const ModuleBase::matrix eps = data_.get_dielectric(); + + for (int a = 0; a < 3; ++a) + { + for (int b = 0; b < 3; ++b) + { + // = conj(gam_a) mue_b + conj(del_a) nue_b over the + // shared G support, wg-weighted with the 16 pi/Omega prefactor + const std::complex dot = std::conj(gam[a]) * mue[b] + + std::conj(del[a]) * nue[b]; + const double expect = ((a == b) ? 1.0 : 0.0) + - 16.0 * ModuleBase::PI / ucell_.omega + * wg(0, 0) * dot.real(); + EXPECT_NEAR(eps(a, b), expect, 1.0e-12) << "a=" << a << " b=" << b; + } + } +} + +// --------------------------------------------------------------------------- +// compute_born against the closed-form screened-leg product (v4 QE anchor): +// Z*(a,idir) = zion delta - 2 sum wg Re with synthetic +// stashed displacement responses and position legs +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, ComputeBornTwoLevelAnalytic) +{ + const int npwk = pw_wfc_.npwk[0]; + const int ig0 = IgOf(0, 0, 0); + const int igx = IgOf(1, 0, 0); + ASSERT_GE(ig0, 0); + ASSERT_GE(igx, 0); + + const double e1 = ucell_.tpiba2 * (gx_ * gx_); + psi::Psi> psi(1, 2, npwk, npwk, true); + psi.zero_out(); + psi(0, 0, ig0) = std::sqrt(0.6); + psi(0, 0, igx) = std::sqrt(0.4); + psi(0, 1, ig0) = std::sqrt(0.2); + + ModuleBase::matrix wg(1, 2); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + ModuleBase::matrix eig(1, 2); + eig(0, 0) = 0.4 * e1; + eig(0, 1) = e1; + + // sentinel dpsi in the q = 0 slot: compute_born never touches the dpsi + // slots, the sentinel must survive verbatim + const std::vector> sentinel(npwk, std::complex(0.5, -0.25)); + data_.set_dpsi(0, 0, 0, sentinel); + + // synthetic converged displacement responses dpsi(scf)/du_{0,idir} for + // the occupied band (G0/Gx components, distinct complexes per idir + // catch transposed indices); the empty-band row stays unsolved + const std::complex alpha[3] = {std::complex(0.3, 0.2), + std::complex(-0.1, 0.4), + std::complex(0.25, -0.15)}; + const std::complex beta[3] = {std::complex(0.2, -0.35), + std::complex(0.45, 0.1), + std::complex(-0.2, -0.05)}; + for (int idir = 0; idir < 3; ++idir) + { + std::vector>>> disp( + 1, std::vector>>(2)); + disp[0][0].assign(npwk, std::complex(0.0, 0.0)); + disp[0][0][ig0] = alpha[idir]; + disp[0][0][igx] = beta[idir]; + data_.set_dpsi_disp(0, idir, disp); + } + + // synthetic solved position legs Y^a_{0,0} = P_c x_a|psi_0> + const std::complex gam[3] = {std::complex(0.15, -0.3), + std::complex(0.4, 0.05), + std::complex(-0.35, 0.2)}; + const std::complex del[3] = {std::complex(-0.25, 0.45), + std::complex(0.1, -0.1), + std::complex(0.3, 0.25)}; + for (int a = 0; a < 3; ++a) + { + std::vector>>> y( + 1, std::vector>>(2)); + y[0][0].assign(npwk, std::complex(0.0, 0.0)); + y[0][0][ig0] = gam[a]; + y[0][0][igx] = del[a]; + data_.set_pos_resp(a, y); + } + + q0_.compute_born(psi, wg, eig, data_); + const ModuleBase::matrix zstar = data_.get_born(0); + + const double zion = ucell_.atoms[0].ncpp.zv; + for (int idir = 0; idir < 3; ++idir) + { + for (int a = 0; a < 3; ++a) + { + // = conj(alpha)gam + conj(beta)del over the + // shared G support, wg-weighted with the -2 spin prefactor + const std::complex dot = std::conj(alpha[idir]) * gam[a] + + std::conj(beta[idir]) * del[a]; + const double expect = ((a == idir) ? zion : 0.0) + - 2.0 * wg(0, 0) * dot.real(); + EXPECT_NEAR(zstar(a, idir), expect, 1.0e-12) << "a=" << a << " idir=" << idir; + } + } + + // the q = 0 dpsi slot is untouched + const std::vector> after = data_.get_dpsi(0, 0, 0); + ASSERT_EQ(after.size(), sentinel.size()); + for (size_t i = 0; i < after.size(); ++i) + { + EXPECT_DOUBLE_EQ(after[i].real(), sentinel[i].real()); + EXPECT_DOUBLE_EQ(after[i].imag(), sentinel[i].imag()); + } +} + +// --------------------------------------------------------------------------- +// build_stars + rotate_tensor: C3 group on a simple-cubic 3-atom orbit cell. +// Atoms sit on the orbit tau = (0.1,0.2,0.3) under the cyclic permutation +// (x,y,z)->(z,x,y), so every group operation maps atom i -> i+1 (mod 3) and +// the star of k = (1/4,0,0) has the three members +// {(1/4,0,0),(0,1/4,0),(0,0,1/4)}. An anisotropic trace-6 tensor star- +// averages to 2*delta_ab, and the members carry the cyclic atom map. +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, StarRotationCyclicGroup) +{ + // rebuild the cell as 3 atoms on the C3 orbit of (0.1,0.2,0.3) + ucell_.nat = 3; + ucell_.atoms[0].na = 3; + ucell_.atoms[0].tau.resize(3); + ucell_.atoms[0].taud.resize(3); + const double t0[3] = {0.1, 0.2, 0.3}; + for (int i = 0; i < 3; ++i) + { + ucell_.atoms[0].taud[i] = ModuleBase::Vector3(t0[i], t0[(i + 1) % 3], t0[(i + 2) % 3]); + ucell_.atoms[0].tau[i] = ucell_.atoms[0].taud[i] * a_; + } + delete[] ucell_.iat2it; + delete[] ucell_.iat2ia; + ucell_.iat2it = new int[3]; + ucell_.iat2ia = new int[3]; + for (int i = 0; i < 3; ++i) + { + ucell_.iat2it[i] = 0; + ucell_.iat2ia[i] = i; + } + + // C3 about (111): direct-space row-convention matrices g with tau' = tau*g + // (the cycle (x,y,z)->(y,z,x) sends atom i -> i+1 mod 3); g2 = g*g. In + // reciprocal space kgmatrix = G*g*G^-1 = g for a cubic cell. + const ModuleBase::Matrix3 g1(0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0); + const ModuleBase::Matrix3 g2 = g1 * g1; + ucell_.symm.nrotk = 3; + ucell_.symm.gmatrix[0] = ModuleBase::Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + ucell_.symm.gmatrix[1] = g1; + ucell_.symm.gmatrix[2] = g2; + for (int j = 0; j < 3; ++j) + { + ucell_.symm.kgmatrix[j] = ucell_.symm.gmatrix[j]; + ucell_.symm.gtrans[j] = ModuleBase::Vector3(0.0, 0.0, 0.0); + } + + // single reduced k point (1/4,0,0) on its own wfc basis + ModulePW::PW_Basis_K kwfc; + const ModuleBase::Vector3 klist[1] + = {ModuleBase::Vector3(0.25, 0.0, 0.0)}; + kwfc.initgrids(lat0_, latvec_, pw_rho_.nx, pw_rho_.ny, pw_rho_.nz); + kwfc.initparameters(false, ecutwfc_, 1, klist); + kwfc.fft_bundle.initfftmode(0); + kwfc.setuptransform(); + kwfc.collect_local_pw(); + q0_.init(ucell_, &pw_rho_, &kwfc, &pert_); + + q0_.build_stars(1); + ASSERT_EQ(q0_.stars_[0].size(), (size_t)3); + + // star-average an anisotropic trace-6 tensor over the members + ModuleBase::matrix chi(3, 3, true); + chi(0, 0) = 1.0; + chi(1, 1) = 2.0; + chi(2, 2) = 3.0; + double avg[9] = {0.0}; + for (size_t im = 0; im < q0_.stars_[0].size(); ++im) + { + double rot[9]; + q0_.rotate_tensor(q0_.stars_[0][im].cart, chi, rot); + for (int i = 0; i < 9; ++i) + { + avg[i] += rot[i] / 3.0; + } + } + for (int a = 0; a < 3; ++a) + { + for (int b = 0; b < 3; ++b) + { + EXPECT_NEAR(avg[3 * a + b], (a == b) ? 2.0 : 0.0, 1.0e-12) << "a=" << a << " b=" << b; + } + } + + // every member carries a cyclic atom map: the identity member is the + // pre-filled one with an empty map, the two rotation members carry the + // g and g*g maps i -> (i+1)%3 and i -> (i+2)%3 + int seen_shift[3] = {1, 0, 0}; // identity shift 0 already seen + for (size_t im = 0; im < q0_.stars_[0].size(); ++im) + { + const std::vector& amap = q0_.stars_[0][im].atom_map; + if (amap.empty()) + { + // identity member: rotation must be the identity + EXPECT_DOUBLE_EQ(q0_.stars_[0][im].cart.e11, 1.0); + EXPECT_DOUBLE_EQ(q0_.stars_[0][im].cart.e12, 0.0); + continue; + } + ASSERT_EQ(amap.size(), (size_t)3); + int shift = -1; + for (int s = 0; s < 3; ++s) + { + bool ok = true; + for (int i = 0; i < 3; ++i) + { + ok = ok && amap[i] == (i + s) % 3; + } + if (ok) + { + shift = s; + break; + } + } + ASSERT_GE(shift, 0); + seen_shift[shift] = 1; + } + for (int s = 0; s < 3; ++s) + { + EXPECT_EQ(seen_shift[s], 1) << "atom-map shift " << s << " missing"; + } + + // with the point group unavailable the stars degenerate to identity + ucell_.symm.nrotk = 0; + q0_.build_stars(1); + ASSERT_EQ(q0_.stars_[0].size(), (size_t)1); + double rot[9]; + q0_.rotate_tensor(q0_.stars_[0][0].cart, chi, rot); + for (int a = 0; a < 3; ++a) + { + for (int b = 0; b < 3; ++b) + { + EXPECT_NEAR(rot[3 * a + b], chi(a, b), 1.0e-12); + } + } +} diff --git a/source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp new file mode 100644 index 00000000000..937af5c65eb --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp @@ -0,0 +1,540 @@ +#include "gtest/gtest.h" +#include +#include +#include +#include + +// serial unit test of the first-order density response (C3). +// Everything here runs without __MPI: the plane-wave bases are built through +// the real serial initgrids/initparameters/setuptransform path on a shared +// FFT grid, exactly like the production setup_pwrho/setup_pwwfc sequence. + +#define private public +#include "source_cell/qlist.h" +#undef private + +#include "source_base/constants.h" +#include "source_base/matrix.h" +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_basis/module_pw/pw_basis_k.h" +#include "source_psi/psi.h" +#include "source_pw/module_dfpt/dfpt_kq_basis.h" +#include "source_pw/module_dfpt/dfpt_pw_data.h" +#include "source_pw/module_dfpt/dfpt_rho.h" +#include "dfpt_serial_fixture.h" + +/************************************************ + * serial unit test of DFPT_Rho (C3) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_Rho::compute_drho - the q-shifted response density coefficients + * A_Delta = sum_{kn occ} wg sum_G c*_G d_{G+Delta} against a brute-force + * G-space double sum built from independent G-keyed coefficient maps, + * and the real-space manifest 2 Re[e^{iqr} u* du] against direct sums. + * - charge conservation: the Delta = -q harmonic is dropped at q = Gamma + * (drho_g[ig(|g|=0)] == 0, grid sum of drho_r ~ 0). + * - DFPT_Rho::mix_drho - plain mixing drho_in + beta (out - in) with a + * zero first input, and the second-step combination; residual formula. + * - occupation gate: bands with wg < 1e-8 do not contribute. + */ + +namespace { + +unsigned g_seed = 20260815u; +double test_rand() +{ + g_seed = g_seed * 1664525u + 1013904223u; + return ((g_seed >> 8) & 0xffffff) / 16777216.0 * 2.0 - 1.0; +} + +std::complex crand() +{ + return std::complex(test_rand(), test_rand()); +} + +} // namespace + +class DFPTRhoSerialTest : public DFPTSerialBase +{ + protected: + ModuleDFPT::DFPT_Rho rho_; + + static const int nbands_ = 2; + + void SetUp() override + { + DFPTSerialBase::SetUp(); + rho_.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "plain", 0.4, 0.0); + } + + void FillRandomStates(psi::Psi>& psi, + std::vector>>>& dpsi) + { + psi::Psi> p(1, nbands_, pw_wfc_.npwk_max, pw_wfc_.npwk[0], true); + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + dpsi.assign(1, std::vector>>(nbands_)); + for (int ib = 0; ib < nbands_; ++ib) + { + for (int igl = 0; igl < pw_wfc_.npwk[0]; ++igl) + { + p(0, ib, igl) = crand(); + } + dpsi[0][ib].assign(kq.get_npwk(), std::complex(0.0, 0.0)); + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + dpsi[0][ib][igl] = crand(); + } + data_.set_dpsi(0, 0, ib, dpsi[0][ib]); + } + psi = p; + } +}; + +TEST_F(DFPTRhoSerialTest, ComputeDrhoMatchesBruteForceGSpace) +{ + psi::Psi> psi; + std::vector>>> dpsi; + FillRandomStates(psi, dpsi); + ModuleBase::matrix wg(1, nbands_); + wg(0, 0) = 1.0; + wg(0, 1) = 0.0; // unoccupied: must not contribute + + rho_.compute_drho(psi, wg, 0, data_); + const std::vector> drho_g = data_.get_drho_g(0, 0); + ASSERT_EQ(drho_g.size(), static_cast(pw_rho_.npw)); + + // enumerate the occupied-band coefficient list G -> c (PW_Basis_K::gcar + // is per-k: entry igl pairs with psi(0,0,igl)) and the k+q list + std::vector> glist; + std::vector> clist; + for (int igl = 0; igl < pw_wfc_.npwk[0]; ++igl) + { + glist.push_back(pw_wfc_.gcar[igl]); + clist.push_back(psi(0, 0, igl)); + } + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + const std::vector> dvec = data_.get_dpsi(0, 0, 0); + + double err2 = 0.0; + double ref2 = 0.0; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + // rho-grid ig -> cartesian Delta through the stick/cell position + const int isz = pw_rho_.ig2isz[ig]; + const int iz = isz % pw_rho_.nz; + const int is = isz / pw_rho_.nz; + const int ixy = pw_rho_.is2fftixy[is]; + const int ix = ixy / pw_rho_.fftny; + const int iy = ixy % pw_rho_.fftny; + const int mx = (ix <= pw_rho_.nx / 2) ? ix : ix - pw_rho_.nx; + const int my = (iy <= pw_rho_.ny / 2) ? iy : iy - pw_rho_.ny; + const int mz = (iz <= pw_rho_.nz / 2) ? iz : iz - pw_rho_.nz; + const ModuleBase::Vector3 delta = + ModuleBase::Vector3(mx, my, mz) * G_; + // A_Delta = (2 w / omega) * sum_G c*_G d_{G+Delta}: the spin factor + // 2 sits in the band weight w1 = 2 w / omega (the QE incdrhoscf + // convention, a915352cd), brute-forced over the lists + std::complex aref(0.0, 0.0); + for (int jgl = 0; jgl < kq.get_npwk(); ++jgl) + { + const ModuleBase::Vector3 gq = kq.get_gcar(jgl); + for (size_t j = 0; j < glist.size(); ++j) + { + if (std::abs(glist[j].x - (gq.x - delta.x)) < 1.0e-6 && + std::abs(glist[j].y - (gq.y - delta.y)) < 1.0e-6 && + std::abs(glist[j].z - (gq.z - delta.z)) < 1.0e-6) + { + aref += std::conj(clist[j]) * dvec[jgl]; + break; + } + } + } + aref *= 2.0 * wg(0, 0) / pw_rho_.omega; + err2 += std::norm(drho_g[ig] - aref); + ref2 += std::norm(aref); + } + EXPECT_LT(std::sqrt(err2 / ref2), 1.0e-10); +} + +TEST_F(DFPTRhoSerialTest, ComputeDrhoRealSpaceMatchesDirectSum) +{ + psi::Psi> psi; + std::vector>>> dpsi; + FillRandomStates(psi, dpsi); + ModuleBase::matrix wg(1, nbands_); + wg(0, 0) = 1.0; + wg(0, 1) = 0.0; + + rho_.compute_drho(psi, wg, 0, data_); + const std::vector drho_r = data_.get_drho_r(0, 0); + ASSERT_EQ(drho_r.size(), static_cast(pw_rho_.nrxx)); + + // direct real-space sums u(r) = sum c_G e^{i 2pi G.r} at sample points; + // PW_Basis_K::gcar is a per-k array indexed by ik*npwk_max + igl, so for + // k=0 the entry igl pairs directly with the coefficient psi(0,0,igl) + std::vector> glist; + std::vector> clist; + for (int igl = 0; igl < pw_wfc_.npwk[0]; ++igl) + { + glist.push_back(pw_wfc_.gcar[igl]); + clist.push_back(psi(0, 0, igl)); + } + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + std::vector> dgl; + std::vector> dcl; + for (int jgl = 0; jgl < kq.get_npwk(); ++jgl) + { + dgl.push_back(kq.get_gcar(jgl)); + dcl.push_back(data_.get_dpsi(0, 0, 0)[jgl]); + } + + const int samples[5][3] = {{0, 0, 0}, {1, 0, 0}, {0, 2, 0}, {0, 0, 3}, {2, 1, 1}}; + // cartesian grid point: r = (fx,fy,fz) . latvec (row-vector convention) + for (int s = 0; s < 5; ++s) + { + const int ix = samples[s][0] % pw_rho_.nx; + const int iy = samples[s][1] % pw_rho_.ny; + const int iz = samples[s][2] % pw_rho_.nz; + const double fx = static_cast(ix) / pw_rho_.nx; + const double fy = static_cast(iy) / pw_rho_.ny; + const double fz = static_cast(iz) / pw_rho_.nz; + const ModuleBase::Vector3 r_cart = + ModuleBase::Vector3(fx, fy, fz) * latvec_; + std::complex u(0.0, 0.0); + for (size_t j = 0; j < glist.size(); ++j) + { + const double ph = ModuleBase::TWO_PI * (glist[j] * r_cart); + u += clist[j] * std::complex(std::cos(ph), std::sin(ph)); + } + std::complex du(0.0, 0.0); + for (size_t j = 0; j < dgl.size(); ++j) + { + const double ph = ModuleBase::TWO_PI * (dgl[j] * r_cart); + du += dcl[j] * std::complex(std::cos(ph), std::sin(ph)); + } + const double phq = ModuleBase::TWO_PI * (q_d_.x * fx + q_d_.y * fy + q_d_.z * fz); + const std::complex eq(std::cos(phq), std::sin(phq)); + // manifest density 2 Re[e^{iqr} A(r)] with A carrying the band + // weight w1 = 2 w / omega (spin factor 2, a915352cd): the outer 2 + // Re and the inner 2 w / omega combine to 4 w / omega + const double ref = 4.0 * (wg(0, 0) / pw_rho_.omega) * (std::conj(u) * du * eq).real(); + const int ir = (ix * pw_rho_.ny + iy) * pw_rho_.nz + iz; + EXPECT_NEAR(drho_r[ir], ref, 1.0e-9); + } +} + +TEST_F(DFPTRhoSerialTest, ChargeConservationAtGamma) +{ + // rebuild the fixture bases with q = k = 0 + ModuleBase::Vector3 klist0[1] = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + ModulePW::PW_Basis_K pw_wfc0; + pw_wfc0.initgrids(lat0_, latvec_, pw_rho_.nx, pw_rho_.ny, pw_rho_.nz); + pw_wfc0.initparameters(false, ecutwfc_, 1, klist0); + pw_wfc0.fft_bundle.initfftmode(0); + pw_wfc0.setuptransform(); + pw_wfc0.collect_local_pw(); + + ModuleCell::QList qlist0; + qlist0.nkstot = 1; + qlist0.kvec_d.push_back(ModuleBase::Vector3(0.0, 0.0, 0.0)); + + ModuleDFPT::DFPT_PW_Data data0; + data0.init(&qlist0, 1, nbands_, pw_wfc0.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + ModuleDFPT::DFPT_Rho rho0; + rho0.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc0, G_, "plain", 0.4, 0.0); + + psi::Psi> psi(1, nbands_, pw_wfc0.npwk_max, pw_wfc0.npwk[0], true); + ModuleDFPT::DFPT_KQ_Basis kq0; + kq0.init(&pw_wfc0, &pw_rho_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + for (int ib = 0; ib < nbands_; ++ib) + { + for (int igl = 0; igl < pw_wfc0.npwk[0]; ++igl) + { + psi(0, ib, igl) = crand(); + } + std::vector> dv(kq0.get_npwk()); + for (int igl = 0; igl < kq0.get_npwk(); ++igl) + { + dv[igl] = crand(); + } + data0.set_dpsi(0, 0, ib, dv); + } + ModuleBase::matrix wg(1, nbands_); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + + rho0.compute_drho(psi, wg, 0, data0); + const std::vector> drho_g = data0.get_drho_g(0, 0); + ASSERT_EQ(drho_g.size(), static_cast(pw_rho_.npw)); + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + if (pw_rho_.gcar[ig].norm() < 1.0e-10) + { + EXPECT_EQ(drho_g[ig], std::complex(0.0, 0.0)); + } + } + // the manifest density integrates (grid-sums) to zero + const std::vector drho_r = data0.get_drho_r(0, 0); + double sum = 0.0; + double absmax = 0.0; + for (int ir = 0; ir < pw_rho_.nrxx; ++ir) + { + sum += drho_r[ir]; + absmax = std::max(absmax, std::abs(drho_r[ir])); + } + EXPECT_LT(std::abs(sum) / (absmax * pw_rho_.nrxx), 1.0e-12); +} + +TEST_F(DFPTRhoSerialTest, MixDrhoFirstStepIsScaledOutput) +{ + psi::Psi> psi; + std::vector>>> dpsi; + FillRandomStates(psi, dpsi); + ModuleBase::matrix wg(1, nbands_); + wg(0, 0) = 1.0; + wg(0, 1) = 0.0; + + rho_.compute_drho(psi, wg, 0, data_); + const std::vector> out = data_.get_drho_g(0, 0); + rho_.mix_drho(0, data_); + const std::vector> mixed = data_.get_drho_g(0, 0); + ASSERT_EQ(mixed.size(), out.size()); + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + EXPECT_NEAR(mixed[ig].real(), 0.4 * out[ig].real(), 1.0e-12); + EXPECT_NEAR(mixed[ig].imag(), 0.4 * out[ig].imag(), 1.0e-12); + } + // zero input: ||out - 0|| / ||out|| == 1 exactly + EXPECT_NEAR(rho_.get_residual(0, data_), 1.0, 1.0e-12); +} + +TEST_F(DFPTRhoSerialTest, MixDrhoSecondStepCombinesCorrectly) +{ + psi::Psi> psi; + std::vector>>> dpsi; + FillRandomStates(psi, dpsi); + ModuleBase::matrix wg(1, nbands_); + wg(0, 0) = 1.0; + wg(0, 1) = 0.0; + + rho_.compute_drho(psi, wg, 0, data_); + rho_.mix_drho(0, data_); + const std::vector> in1 = data_.get_drho_g(0, 0); + + // new response from fresh dpsi + g_seed = 777u; + std::vector>>> dpsi2; + FillRandomStates(psi, dpsi2); + rho_.compute_drho(psi, wg, 0, data_); + const std::vector> out2 = data_.get_drho_g(0, 0); + rho_.mix_drho(0, data_); + const std::vector> mixed2 = data_.get_drho_g(0, 0); + + double dn2 = 0.0; + double o2 = 0.0; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + const std::complex ref = in1[ig] + 0.4 * (out2[ig] - in1[ig]); + EXPECT_NEAR(mixed2[ig].real(), ref.real(), 1.0e-12); + EXPECT_NEAR(mixed2[ig].imag(), ref.imag(), 1.0e-12); + dn2 += std::norm(out2[ig] - in1[ig]); + o2 += std::norm(out2[ig]); + } + EXPECT_NEAR(rho_.get_residual(0, data_), std::sqrt(dn2 / o2), 1.0e-12); +} + +TEST_F(DFPTRhoSerialTest, VHartreeQClosedFormAndZeroMode) +{ + // single-G amplitude: dv_ha_g[ig] = e2 4 pi / (tpiba2 |G+q|^2) drho_g[ig] + const int ig_star = [this]() + { + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + if ((pw_rho_.gcar[ig] + q_cart_) * (pw_rho_.gcar[ig] + q_cart_) > 1.0e-4) + { + return ig; + } + } + return -1; + }(); + ASSERT_GE(ig_star, 0); + + std::vector> drho_g(pw_rho_.npw, std::complex(0.0, 0.0)); + drho_g[ig_star] = std::complex(0.3, -0.7); + std::vector> dv; + rho_.v_hartree_q(q_cart_, drho_g, dv); + ASSERT_EQ(dv.size(), static_cast(pw_rho_.npw)); + const ModuleBase::Vector3 w = pw_rho_.gcar[ig_star] + q_cart_; + const std::complex expect + = ModuleBase::e2 * ModuleBase::FOUR_PI / (pw_rho_.tpiba2 * (w * w)) + * drho_g[ig_star]; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + if (ig == ig_star) + { + EXPECT_NEAR(dv[ig].real(), expect.real(), 1.0e-10); + EXPECT_NEAR(dv[ig].imag(), expect.imag(), 1.0e-10); + } + else + { + EXPECT_EQ(dv[ig], std::complex(0.0, 0.0)); + } + } + + // |G+q| = 0 (ig = -q) is skipped like v_hartree skips ig_gge0 + const ModuleBase::Vector3 q_minus = pw_rho_.gcar[ig_star] * (-1.0); + std::vector> dv0; + rho_.v_hartree_q(q_minus, drho_g, dv0); + EXPECT_EQ(dv0[ig_star], std::complex(0.0, 0.0)); + + // a wrong-size drho clears the output instead of aliasing it + std::vector> short_input(3, std::complex(1.0, 1.0)); + rho_.v_hartree_q(q_cart_, short_input, dv0); + EXPECT_TRUE(dv0.empty()); +} + +TEST_F(DFPTRhoSerialTest, MixDrhoKerkerFirstStepIsPreconditionedScaledOutput) +{ + // first step from the zero input: mixed[ig] = beta * f[ig] * out[ig] + // with the Kerker screen f[ig] = |G+q|^2 / (|G+q|^2 + a^2) built from + // gcar + q_frac * G (1/lat0^2 units), the v_hartree_q convention + ModuleDFPT::DFPT_Rho rho_k; + double w2_min = 0.0; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + const ModuleBase::Vector3 w = pw_rho_.gcar[ig] + q_cart_; + const double w2 = w * w; + if (w2 > 1.0e-12 && (w2_min == 0.0 || w2 < w2_min)) + { + w2_min = w2; + } + } + ASSERT_GT(w2_min, 0.0); + const double a2 = 4.0 * w2_min; + rho_k.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "kerker", 0.7, a2); + + std::vector> out(pw_rho_.npw); + int n_small = 0; + int n_large = 0; + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + out[ig] = 0.01 * std::complex(std::cos(0.7 * ig), std::sin(0.5 * ig)); + const ModuleBase::Vector3 w = pw_rho_.gcar[ig] + q_cart_; + const double w2 = w * w; + if (w2 < a2) + { + ++n_small; + } + if (w2 > 100.0 * a2) + { + ++n_large; + } + } + // sanity: the screen actually varies across the basis + ASSERT_GT(n_small, 0); + ASSERT_GT(n_large, 0); + + data_.set_drho_g(0, 0, out); + rho_k.mix_drho(0, data_); + const std::vector> mixed = data_.get_drho_g(0, 0); + ASSERT_EQ(mixed.size(), out.size()); + for (int ig = 0; ig < pw_rho_.npw; ++ig) + { + const ModuleBase::Vector3 w = pw_rho_.gcar[ig] + q_cart_; + const double w2 = w * w; + const double f = (w2 < 1.0e-12) ? 0.0 : w2 / (w2 + a2); + const std::complex ref = 0.7 * f * out[ig]; + EXPECT_NEAR(mixed[ig].real(), ref.real(), 1.0e-12); + EXPECT_NEAR(mixed[ig].imag(), ref.imag(), 1.0e-12); + } + EXPECT_NEAR(rho_k.get_residual(0, data_), 1.0, 1.0e-12); +} + +TEST_F(DFPTRhoSerialTest, MixDrhoKerkerStabilizesStiffModelProblem) +{ + // model SCF problem: out(g) = D(g) in(g) + s(g) with the measured + // diamond-smoke Coulomb-stiffness eigenvalue D = lambda ~ -2.2 on the + // smallest |G+q| shell and D = 0.3 elsewhere; the fixed point is a + // fixed target pattern t(g), s = (1 - D) t. Plain mixing must satisfy + // beta < 2 / (1 + |lambda|) = 0.625, so beta = 0.7 diverges + // (amplification |1 - beta (1 - D)| = 1.24), while the Kerker screen + // damps the stiff shell amplification below 1 and converges. + const int npw = pw_rho_.npw; + + double w2_min = 0.0; + for (int ig = 0; ig < npw; ++ig) + { + const ModuleBase::Vector3 w = pw_rho_.gcar[ig] + q_cart_; + const double w2 = w * w; + if (w2 > 1.0e-12 && (w2_min == 0.0 || w2 < w2_min)) + { + w2_min = w2; + } + } + ASSERT_GT(w2_min, 0.0); + + std::vector stiff(npw, 0.3); + for (int ig = 0; ig < npw; ++ig) + { + const ModuleBase::Vector3 w = pw_rho_.gcar[ig] + q_cart_; + const double w2 = w * w; + if (w2 > 1.0e-12 && w2 < 1.5 * w2_min) + { + stiff[ig] = -2.2; + } + } + + std::vector> target(npw); + for (int ig = 0; ig < npw; ++ig) + { + target[ig] = 0.01 * std::complex(std::cos(0.3 * ig), std::sin(0.9 * ig)); + } + + auto model_out = [&](const std::vector>& in) + { + std::vector> o(npw); + for (int ig = 0; ig < npw; ++ig) + { + o[ig] = stiff[ig] * in[ig] + (1.0 - stiff[ig]) * target[ig]; + } + return o; + }; + + // plain beta = 0.7 on the stiff model diverges + ModuleDFPT::DFPT_Rho rho_p; + rho_p.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "plain", 0.7, 0.0); + data_.set_drho_g(0, 0, std::vector>(npw, std::complex(0.0, 0.0))); + for (int it = 0; it < 40; ++it) + { + data_.set_drho_g(0, 0, model_out(data_.get_drho_g(0, 0))); + rho_p.mix_drho(0, data_); + } + const double residual_plain = rho_p.get_residual(0, data_); + EXPECT_GT(residual_plain, 1.0); + + // kerker beta = 0.7 with a^2 = 9 w2_min (f ~ 0.1 on the stiff shell) + // converges to the target + ModuleDFPT::DFPT_Rho rho_k; + rho_k.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "kerker", 0.7, 9.0 * w2_min); + data_.set_drho_g(0, 0, std::vector>(npw, std::complex(0.0, 0.0))); + for (int it = 0; it < 300; ++it) + { + data_.set_drho_g(0, 0, model_out(data_.get_drho_g(0, 0))); + rho_k.mix_drho(0, data_); + } + const double residual_kerker = rho_k.get_residual(0, data_); + EXPECT_LT(residual_kerker, 1.0e-8); + const std::vector> final_in = data_.get_drho_g(0, 0); + for (int ig = 0; ig < npw; ++ig) + { + EXPECT_NEAR(final_in[ig].real(), target[ig].real(), 1.0e-10); + EXPECT_NEAR(final_in[ig].imag(), target[ig].imag(), 1.0e-10); + } +} diff --git a/source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.cpp new file mode 100644 index 00000000000..9c99a1ae6e9 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.cpp @@ -0,0 +1,201 @@ +// Pull the whole standard-library closure in before the private->public +// define below: the cell/qlist headers drag in and friends whose +// internals break when compiled with `private` redefined (same pattern as +// the test translation units themselves). +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define private public +#include "dfpt_serial_fixture.h" +#undef private + +#include "source_base/constants.h" + +void DFPTSerialBase::SetUp() +{ + SetUpCell(); + SetupBases(k_d_, q_d_, 2); +} + +void DFPTSerialBase::SetUpCell() +{ + latvec_ = ModuleBase::Matrix3(a_, 0.0, 0.0, 0.0, a_, 0.0, 0.0, 0.0, a_); + ucell_.ntype = 1; + ucell_.nat = 1; + ucell_.atoms = new Atom[1]; + ucell_.atoms[0].na = 1; + ucell_.atoms[0].tau.resize(1); + ucell_.atoms[0].tau[0] = tau_; + ucell_.latvec = latvec_; + ucell_.GT = latvec_.Inverse(); + ucell_.G = ucell_.GT.Transpose(); + ucell_.lat0 = lat0_; + ucell_.tpiba = ModuleBase::TWO_PI / lat0_; + ucell_.tpiba2 = ucell_.tpiba * ucell_.tpiba; + ucell_.omega = a_ * a_ * a_ * lat0_ * lat0_ * lat0_; + ucell_.iat2it = new int[1]; + ucell_.iat2ia = new int[1]; + ucell_.iat2it[0] = 0; + ucell_.iat2ia[0] = 0; + MakeCoulombAtom(); +} + +void DFPTSerialBase::SetupBases(const ModuleBase::Vector3& k_d, + const ModuleBase::Vector3& q_d, + int nbands) +{ + G_ = ucell_.G; + pw_rho_.initgrids(lat0_, latvec_, rho_mult_ * ecutwfc_); + pw_rho_.initparameters(false, rho_mult_ * ecutwfc_); + pw_rho_.fft_bundle.initfftmode(0); + pw_rho_.setuptransform(); + pw_rho_.collect_local_pw(); + + const ModuleBase::Vector3 klist[1] = {k_d}; + pw_wfc_.initgrids(lat0_, latvec_, pw_rho_.nx, pw_rho_.ny, pw_rho_.nz); + pw_wfc_.initparameters(false, ecutwfc_, 1, klist); + pw_wfc_.fft_bundle.initfftmode(0); + pw_wfc_.setuptransform(); + pw_wfc_.collect_local_pw(); + + qlist_.nkstot = 1; + qlist_.kvec_d.clear(); + qlist_.kvec_d.push_back(q_d); + q_cart_ = q_d * ucell_.G; + + data_.init(&qlist_, 1, nbands, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); +} + +void DFPTSerialBase::TearDown() +{ + delete[] ucell_.atoms; + ucell_.atoms = nullptr; + delete[] ucell_.iat2it; + ucell_.iat2it = nullptr; + delete[] ucell_.iat2ia; + ucell_.iat2ia = nullptr; +} + +void DFPTSerialBase::MakeCoulombAtom() +{ + Atom& at = ucell_.atoms[0]; + at.label = "C"; + at.coulomb_potential = true; + at.ncpp.zv = 4.0; + at.ncpp.tvanp = false; + at.ncpp.has_so = false; + at.ncpp.nbeta = 0; + at.ncpp.nh = 0; + at.ncpp.msh = 0; + at.ncpp.kkbeta = 0; + at.mass = 12.0; +} + +void DFPTSerialBase::MakeNCAtom() +{ + Atom& at = ucell_.atoms[0]; + at.label = "Si"; + at.coulomb_potential = false; + pseudo& p = at.ncpp; + p.zv = 4.0; + p.tvanp = false; + p.has_so = false; + p.nbeta = 2; + p.lll = {0, 1}; + p.nh = 4; + p.msh = 121; + p.kkbeta = 121; + p.r.resize(121); + p.rab.resize(121); + p.vloc_at.assign(121, 0.0); + const double dx = 0.025; + for (int i = 0; i < 121; ++i) + { + p.r[i] = i * dx; + p.rab[i] = dx; + } + p.betar.create(2, 121); + for (int i = 0; i < 121; ++i) + { + const double r = p.r[i]; + p.betar(0, i) = std::exp(-std::pow(r - 1.0, 2) / (2.0 * 0.3 * 0.3)); + p.betar(1, i) = std::exp(-std::pow(r - 1.2, 2) / (2.0 * 0.35 * 0.35)); + } + p.dion.create(2, 2); + p.dion(0, 0) = 0.8; + p.dion(0, 1) = 0.15; + p.dion(1, 0) = -0.25; + p.dion(1, 1) = 1.1; +} + +void DFPTSerialBase::MakeTwoAtomCell() +{ + ucell_.ntype = 2; + ucell_.nat = 2; + delete[] ucell_.atoms; + ucell_.atoms = new Atom[2]; + ucell_.atoms[0].na = 1; + ucell_.atoms[1].na = 1; + ucell_.atoms[0].tau.resize(1); + ucell_.atoms[1].tau.resize(1); + ucell_.atoms[0].tau[0] = ModuleBase::Vector3(0.0, 0.0, 0.0); + ucell_.atoms[1].tau[0] = ModuleBase::Vector3(0.25, 0.31, 0.17); + for (int it = 0; it < 2; ++it) + { + Atom& at = ucell_.atoms[it]; + at.label = (it == 0) ? "A" : "B"; + at.coulomb_potential = true; + at.ncpp.zv = (it == 0) ? 4.0 : 2.0; + at.ncpp.tvanp = false; + at.ncpp.has_so = false; + at.ncpp.nbeta = 0; + at.ncpp.nh = 0; + at.mass = (it == 0) ? 12.0 : 4.0; + } + delete[] ucell_.iat2it; + delete[] ucell_.iat2ia; + ucell_.iat2it = new int[2]; + ucell_.iat2ia = new int[2]; + ucell_.iat2it[0] = 0; + ucell_.iat2ia[0] = 0; + ucell_.iat2it[1] = 1; + ucell_.iat2ia[1] = 0; +} + +long long DFPTSerialBase::FKey(int ix, int iy, int iz) const +{ + return (static_cast(ix + 64) * 128 + (iy + 64)) * 128 + (iz + 64); +} + +long long DFPTSerialBase::GKey(const ModuleBase::Vector3& g) const +{ + return FKey(static_cast(std::llround(g.x * a_)), + static_cast(std::llround(g.y * a_)), + static_cast(std::llround(g.z * a_))); +} + +double DFPTSerialBase::VlocCoulomb(double g2_bohr) const +{ + return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega / g2_bohr; +} + +std::complex DFPTSerialBase::AnalyticDVloc(int dir, const ModuleBase::Vector3& w) const +{ + const double w2 = w * w; + if (w2 < 1.0e-12) + { + return std::complex(0.0, 0.0); + } + const double arg = -ModuleBase::TWO_PI * (w * tau_); + return std::complex(0.0, -1.0) * (ucell_.tpiba * w[dir]) * VlocCoulomb(w2 * ucell_.tpiba2) + * std::complex(std::cos(arg), std::sin(arg)); +} diff --git a/source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.h b/source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.h new file mode 100644 index 00000000000..138a8c5013f --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.h @@ -0,0 +1,85 @@ +#ifndef DFPT_SERIAL_FIXTURE_H +#define DFPT_SERIAL_FIXTURE_H + +#include +#include "gtest/gtest.h" +#include "source_base/matrix3.h" +#include "source_base/vector3.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_basis/module_pw/pw_basis_k.h" +#include "source_cell/qlist.h" +#include "source_cell/unitcell.h" +#include "source_pw/module_dfpt/dfpt_pw_data.h" + +// Shared serial-side gtest fixture for the DFPT unit tests +// (dfpt_pert/rho/phon/q0_serial_test.cpp). Everything runs without +// __MPI: the plane-wave bases are built through the real serial +// initgrids/initparameters/setuptransform path on a shared FFT grid, +// exactly like the production setup_pwrho/setup_pwwfc sequence. +// +// NOTE ON INCLUDE ORDER: the tests that touch private members include +// the cell/qlist/dfpt headers with `#define private public` BEFORE this +// header; the include guards then keep this header's own includes inert. +// The fixture implementation (dfpt_serial_fixture.cpp) needs the same +// define for QList, so it wraps its include accordingly. + +class DFPTSerialBase : public testing::Test +{ + protected: + const double lat0_ = 1.8897261254578281; + const double ecutwfc_ = 2.5; // Ry + // rho cutoff inflated to 9x ecutwfc so every Delta = G''-G' of the + // convolution lies inside the rho ball and nothing aliases + const double rho_mult_ = 9.0; + const double a_ = 10.0; // cubic edge in lat0 units + + ModuleBase::Matrix3 latvec_; + UnitCell ucell_; + ModulePW::PW_Basis pw_rho_; + ModulePW::PW_Basis_K pw_wfc_; + ModuleCell::QList qlist_; + ModuleDFPT::DFPT_PW_Data data_; + ModuleBase::Matrix3 G_; // = latvec^-T, the reciprocal builder + + // default (k, q) of the pert/phon/rho fixtures: q is generic and + // k = -q so k+q = 0: the k+q ball then stays inside the ground-state + // G list (single-k limitation documented in DFPT_KQ_Basis) + const ModuleBase::Vector3 q_d_{0.13, 0.0, 0.07}; + const ModuleBase::Vector3 k_d_{-0.13, 0.0, -0.07}; + ModuleBase::Vector3 q_cart_; + const ModuleBase::Vector3 tau_{1.1, 2.3, 0.7}; // lat0 units + + // single-atom Coulomb cell + shared-grid bases at the default (k_d_, q_d_) + void SetUp() override; + void TearDown() override; + + // cubic single-atom Coulomb cell (iat2it/iat2ia allocated) + void SetUpCell(); + + // (re)initialize the bases and the shared data wiring for a given + // (k, q) pair and band count; SetUp uses the default fixture values + void SetupBases(const ModuleBase::Vector3& k_d, + const ModuleBase::Vector3& q_d, + int nbands); + + void MakeCoulombAtom(); + void MakeNCAtom(); + + // reconfigure the cell as a two-atom Z=4/Z=2 crystal breaking all symmetry + void MakeTwoAtomCell(); + + // key of an integer FFT triple (gcar * a is integral on the cubic cell) + long long FKey(int ix, int iy, int iz) const; + long long GKey(const ModuleBase::Vector3& g) const; + + // analytic Coulomb local potential (Ry) at |g|^2 in bohr^-2, mirroring + // vl_pw.cpp::vloc_coulomb independently of DFPT_Pert::vloc_at_g + double VlocCoulomb(double g2_bohr) const; + + // analytic dVloc/dtau_alpha coefficient at displacement vector w (1/lat0); + // GS structure-factor convention (stru_fac.cpp): exp(-i 2pi (g.tau)) and + // dV/dtau = -i (Delta+q)_alpha tpiba Vloc exp(-i 2pi (Delta+q).tau) + std::complex AnalyticDVloc(int dir, const ModuleBase::Vector3& w) const; +}; + +#endif // DFPT_SERIAL_FIXTURE_H diff --git a/source/source_pw/module_stodft/test/CMakeLists.txt b/source/source_pw/module_stodft/test/CMakeLists.txt index a83352348c1..15c5c93d507 100644 --- a/source/source_pw/module_stodft/test/CMakeLists.txt +++ b/source/source_pw/module_stodft/test/CMakeLists.txt @@ -8,6 +8,7 @@ AddTest( AddTest( TARGET MODULE_PW_Sto_Hamilt_UTs - LIBS parameter psi base device planewave_serial + LIBS parameter psi base device planewave_serial symmetry SOURCES ../hamilt_sdft_pw.cpp test_hamilt_sto.cpp ../../../source_hamilt/operator.cpp + ../../../source_cell/klist.cpp ../../../source_cell/parallel_kpoints.cpp ../../../source_cell/k_vector_utils.cpp ../../../source_cell/reciprocal_grid.cpp ) \ No newline at end of file